* Implementation of "chunked framing" according to RFC6242 for Netconf 1.1.
* First hello is 1.0 EOM framing, then successing rpc is chunked framing
* See
* [Netconf framing](https://github.com/clicon/clixon/issues/50), and
* [Clixon does not switch to chunked framing after NETCONF 1.1 is negotiated](https://github.com/clicon/clixon/issues/314)
* C:
* Moved netconf framing code from netconf application to clixon lib
* Test:
* New expecteof_netconf and adjusted other expect scripts to handle NETCONF 1.1 framing
This commit is contained in:
parent
bf983d7ca4
commit
c038c9a27f
95 changed files with 1758 additions and 1208 deletions
|
|
@ -226,6 +226,7 @@ clixon_client_hello(int sock)
|
|||
cprintf(msg, "<hello xmlns=\"%s\">", NETCONF_BASE_NAMESPACE);
|
||||
cprintf(msg, "<capabilities><capability>%s</capability></capabilities>", NETCONF_BASE_CAPABILITY_1_1);
|
||||
cprintf(msg, "</hello>");
|
||||
cprintf(msg, "]]>]]>");
|
||||
if (clicon_msg_send1(sock, msg) < 0)
|
||||
goto done;
|
||||
retval = 0;
|
||||
|
|
@ -444,6 +445,8 @@ clixon_client_get_xdata(int sock,
|
|||
cprintf(msg, "/>");
|
||||
}
|
||||
cprintf(msg, "</get-config></rpc>");
|
||||
if (netconf_output_encap(NETCONF_SSH_CHUNKED, msg) < 0)
|
||||
goto done;
|
||||
if (clicon_rpc1(sock, msg, msgret, &eof) < 0)
|
||||
goto done;
|
||||
if (eof){
|
||||
|
|
|
|||
|
|
@ -426,7 +426,6 @@ clixon_event_loop(clicon_handle h)
|
|||
if ((*e->e_fn)(e->e_fd, e->e_arg) < 0){
|
||||
clicon_debug(1, "%s Error in: %s", __FUNCTION__, e->e_string);
|
||||
goto err;
|
||||
|
||||
}
|
||||
if (_ee_unreg){
|
||||
_ee_unreg = 0;
|
||||
|
|
@ -440,6 +439,8 @@ clixon_event_loop(clicon_handle h)
|
|||
clicon_debug(1, "%s err", __FUNCTION__);
|
||||
break;
|
||||
}
|
||||
if (clixon_exit_get() == 1)
|
||||
retval = 0;
|
||||
clicon_debug(1, "%s done:%d", __FUNCTION__, retval);
|
||||
return retval;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1462,7 +1462,7 @@ _json_parse(char *str,
|
|||
/*! Parse string containing JSON and return an XML tree
|
||||
*
|
||||
* @param[in] str String containing JSON
|
||||
* @param[in] rfc7951 Do sanity checks according to RFC 7951 JSON Encoding of Data Modeled with YANG
|
||||
* @param[in] rfc7951 Do sanity checks according to RFC 7951 JSON Encoding of Data Modeled with YANG
|
||||
* @param[in] yb How to bind yang to XML top-level when parsing
|
||||
* @param[in] yspec Yang specification, mandatory to make module->xmlns translation
|
||||
* @param[in,out] xt Top object, if not exists, on success it is created with name 'top'
|
||||
|
|
|
|||
|
|
@ -1515,7 +1515,6 @@ netconf_module_load(clicon_handle h)
|
|||
/* Load restconf yang. Note this is also a part of clixon-config */
|
||||
if (yang_spec_parse_module(h, "clixon-restconf", NULL, yspec)< 0)
|
||||
goto done;
|
||||
#if 1
|
||||
/* XXX: Both the following settings are because clicon-handle is not part of all API
|
||||
* functions
|
||||
* Treat unknown XML as anydata */
|
||||
|
|
@ -1524,13 +1523,21 @@ netconf_module_load(clicon_handle h)
|
|||
/* Make message-id attribute optional */
|
||||
if (clicon_option_bool(h, "CLICON_NETCONF_MESSAGE_ID_OPTIONAL") == 1)
|
||||
xml_bind_netconf_message_id_optional(1);
|
||||
#endif
|
||||
/* Load ietf list pagination */
|
||||
if (yang_spec_parse_module(h, "ietf-list-pagination", NULL, yspec)< 0)
|
||||
goto done;
|
||||
/* Load ietf list pagination netconf */
|
||||
if (yang_spec_parse_module(h, "ietf-list-pagination-nc", NULL, yspec)< 0)
|
||||
goto done;
|
||||
/* Framing: If hello protocol skipped, set framing direct, ie fix chunked framing if NETCONF-1.1
|
||||
* But start with default: RFC 4741 EOM ]]>]]>
|
||||
* For now this only applies to external protocol
|
||||
*/
|
||||
clicon_option_int_set(h, "netconf-framing", NETCONF_SSH_EOM);
|
||||
if (clicon_option_bool(h, "CLICON_NETCONF_HELLO_OPTIONAL")){
|
||||
if (clicon_option_int(h, "CLICON_NETCONF_BASE_CAPABILITY") > 0) /* RFC 6241 */
|
||||
clicon_option_int_set(h, "netconf-framing", NETCONF_SSH_CHUNKED);
|
||||
}
|
||||
retval = 0;
|
||||
done:
|
||||
return retval;
|
||||
|
|
@ -1694,7 +1701,8 @@ netconf_hello_server(clicon_handle h,
|
|||
/* Each peer MUST send at least the base NETCONF capability, "urn:ietf:params:netconf:base:1.1"
|
||||
* RFC 6241 Sec 8.1
|
||||
*/
|
||||
cprintf(cb, "<capability>%s</capability>", NETCONF_BASE_CAPABILITY_1_1);
|
||||
if (clicon_option_int(h, "CLICON_NETCONF_BASE_CAPABILITY") > 0) /* RFC 6241 */
|
||||
cprintf(cb, "<capability>%s</capability>", NETCONF_BASE_CAPABILITY_1_1);
|
||||
}
|
||||
/* A peer MAY include capabilities for previous NETCONF versions, to indicate
|
||||
that it supports multiple protocol versions. */
|
||||
|
|
@ -1716,7 +1724,6 @@ netconf_hello_server(clicon_handle h,
|
|||
if (session_id)
|
||||
cprintf(cb, "<session-id>%lu</session-id>", (long unsigned int)session_id);
|
||||
cprintf(cb, "</hello>");
|
||||
cprintf(cb, "]]>]]>");
|
||||
retval = 0;
|
||||
done:
|
||||
if (encstr)
|
||||
|
|
@ -1724,22 +1731,6 @@ netconf_hello_server(clicon_handle h,
|
|||
return retval;
|
||||
}
|
||||
|
||||
int
|
||||
netconf_hello_req(clicon_handle h,
|
||||
cbuf *cb)
|
||||
{
|
||||
int retval = -1;
|
||||
|
||||
cprintf(cb, "<hello xmlns=\"%s\">", NETCONF_BASE_NAMESPACE);
|
||||
cprintf(cb, "<capabilities>");
|
||||
cprintf(cb, "<capability>%s</capability>", NETCONF_BASE_CAPABILITY_1_1);
|
||||
cprintf(cb, "</capabilities>");
|
||||
cprintf(cb, "</hello>");
|
||||
cprintf(cb, "]]>]]>");
|
||||
retval = 0;
|
||||
return retval;
|
||||
}
|
||||
|
||||
/*! Generate textual error log from Netconf error message
|
||||
*
|
||||
* Get a text error message from netconf error message and generate error on the form:
|
||||
|
|
@ -1931,3 +1922,259 @@ netconf_parse_uint32_xml(char *name,
|
|||
retval = 0;
|
||||
goto done;
|
||||
}
|
||||
|
||||
/*! Add netconf xml postamble of message. I.e, xml after the body of the message.
|
||||
*
|
||||
* @param[in] framing Netconf framing
|
||||
* @param[in,out] cb Netconf packet (cligen buffer)
|
||||
* XXX: copies body
|
||||
*/
|
||||
int
|
||||
netconf_framing_preamble(netconf_framing_type framing,
|
||||
cbuf *cb)
|
||||
{
|
||||
int retval = -1;
|
||||
char *body = NULL;
|
||||
|
||||
switch (framing){
|
||||
case NETCONF_SSH_EOM:
|
||||
break;
|
||||
case NETCONF_SSH_CHUNKED:
|
||||
if ((body = strdup(cbuf_get(cb))) == NULL){
|
||||
clicon_err(OE_UNIX, errno, "strdup");
|
||||
goto done;
|
||||
}
|
||||
cbuf_reset(cb);
|
||||
cprintf(cb, "\n#%zu\n", strlen(body)); /* Add RFC6242 chunked-end */
|
||||
cbuf_append_str(cb, body);
|
||||
break;
|
||||
}
|
||||
retval = 0;
|
||||
done:
|
||||
if (body)
|
||||
free(body);
|
||||
return retval;
|
||||
}
|
||||
|
||||
/*! Add netconf xml postamble of message. I.e, xml after the body of the message.
|
||||
*
|
||||
* @param[in] framing Netconf framing
|
||||
* @param[in,out] cb Netconf packet (cligen buffer)
|
||||
*/
|
||||
int
|
||||
netconf_framing_postamble(netconf_framing_type framing,
|
||||
cbuf *cb)
|
||||
{
|
||||
switch (framing){
|
||||
case NETCONF_SSH_EOM:
|
||||
cprintf(cb, "]]>]]>"); /* Add RFC4742 end-of-message marker */
|
||||
break;
|
||||
case NETCONF_SSH_CHUNKED:
|
||||
cprintf(cb, "\n##\n"); /* Add RFC6242 chunked-end */
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*! Send netconf message from cbuf on socket
|
||||
* @param[in] s
|
||||
* @param[in] cb Cligen buffer that contains the XML message
|
||||
* @param[in] msg Only for debug
|
||||
* @retval 0 OK
|
||||
* @retval -1 Error
|
||||
* @see netconf_output_encap for function with encapsulation
|
||||
*/
|
||||
int
|
||||
netconf_output(int s,
|
||||
cbuf *cb,
|
||||
char *msg)
|
||||
{
|
||||
int retval = -1;
|
||||
char *buf = cbuf_get(cb);
|
||||
int len = cbuf_len(cb);
|
||||
|
||||
clicon_debug(1, "SEND %s", msg);
|
||||
if (clicon_debug_get() > 1){ /* XXX: below only works to stderr, clicon_debug may log to syslog */
|
||||
cxobj *xt = NULL;
|
||||
if (clixon_xml_parse_string(buf, YB_NONE, NULL, &xt, NULL) == 0){
|
||||
clicon_xml2file(stderr, xml_child_i(xt, 0), 0, 0);
|
||||
fprintf(stderr, "\n");
|
||||
xml_free(xt);
|
||||
}
|
||||
}
|
||||
if (write(s, buf, len) < 0){
|
||||
if (errno == EPIPE)
|
||||
;
|
||||
else
|
||||
clicon_log(LOG_ERR, "%s: write: %s", __FUNCTION__, strerror(errno));
|
||||
goto done;
|
||||
}
|
||||
retval = 0;
|
||||
done:
|
||||
return retval;
|
||||
}
|
||||
|
||||
/*! Encapsulate and send outgoing netconf packet as cbuf on socket
|
||||
*
|
||||
* @param[in] h Clixon handle
|
||||
* @param[in] cb Cligen buffer that contains the XML message
|
||||
* @retval 0 OK
|
||||
* @retval -1 Error
|
||||
* @note Assumes "cb" contains valid XML
|
||||
* @see netconf_output without encapsulation
|
||||
* @see netconf_hello_msg where framing is set
|
||||
*/
|
||||
int
|
||||
netconf_output_encap(netconf_framing_type framing,
|
||||
cbuf *cb)
|
||||
{
|
||||
int retval = -1;
|
||||
|
||||
if (netconf_framing_preamble(framing, cb) < 0)
|
||||
goto done;
|
||||
if (netconf_framing_postamble(framing, cb) < 0)
|
||||
goto done;
|
||||
retval = 0;
|
||||
done:
|
||||
return retval;
|
||||
}
|
||||
|
||||
/*! Track chunked framing defined in RFC6242
|
||||
*
|
||||
* The function works by calling sequentially each received char and using
|
||||
* two state variables: state and size
|
||||
* The function traverses the states 0-7, where state 4 also uses countdown of size
|
||||
* The function returns one of four results: framing/control; chunk-data; end-of-frame; or error
|
||||
* Anything not conforming is returned as error.
|
||||
* Possibly one should accept whitespaces between frames?
|
||||
* RFC6242 Sec 4.2
|
||||
* <Chunked-Message> = <chunk>+ end-of-chunks
|
||||
* <chunk> = LF HASH <chunk-size> LF <chunk-data>
|
||||
* Example: \n#4\n
|
||||
* data
|
||||
* <end-of-chunks> = LF HASH HASH LF
|
||||
* Example: \n##\n
|
||||
* <chunk-size> = [1-9][0-9]*
|
||||
*
|
||||
* State: 0 No frame (begin/ended)
|
||||
* 1 \n received
|
||||
* 2 # received
|
||||
* 3 read [1-9]
|
||||
* 4 read chunk-size read: chunk-data
|
||||
* @param[in] ch New input character
|
||||
* @param[in,out] state State machine state
|
||||
* @param[in,out] size Remaining expecting chunk bytes.
|
||||
* @retval -1 Error
|
||||
* @retval 0 Framing char, not data
|
||||
* @retval 1 Chunk-data
|
||||
* @retval 2 End-of-frame
|
||||
* Example:
|
||||
C: \n#4\n
|
||||
C: <rpc
|
||||
C: \n#18\n
|
||||
C: message-id="102"\n
|
||||
C: \n#79\n
|
||||
C: xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">\n
|
||||
C: <close-session/>\n
|
||||
C: </rpc>
|
||||
C: \n##\n
|
||||
*/
|
||||
int
|
||||
netconf_input_chunked_framing(char ch,
|
||||
int *state,
|
||||
size_t *size)
|
||||
{
|
||||
int retval = 0;
|
||||
|
||||
clicon_debug(1, "%s ch:%c(%d) state:%d size:%lu", __FUNCTION__, ch, ch, *state, *size);
|
||||
switch (*state){
|
||||
case 0:
|
||||
if (ch == '\n'){
|
||||
(*state)++;
|
||||
break;
|
||||
}
|
||||
clicon_err(OE_NETCONF, 0, "NETCONF framing error chunk-start: expected \\n but received %c (state:%d)", ch, *state);
|
||||
goto err;
|
||||
break;
|
||||
case 1:
|
||||
case 5:
|
||||
if (ch == '#'){
|
||||
(*state)++;
|
||||
break;
|
||||
}
|
||||
clicon_err(OE_NETCONF, 0, "NETCONF framing error: expected # but received %c (state:%d)", ch, *state);
|
||||
goto err;
|
||||
break;
|
||||
case 2:
|
||||
if (ch == '#'){
|
||||
(*state) = 0;
|
||||
retval = 2; /* end of frame (empty data) */
|
||||
break;
|
||||
}
|
||||
else if (ch >= '1' && ch <= '9'){ /* first num */
|
||||
(*state)++;
|
||||
*size = ch-'0';
|
||||
break;
|
||||
}
|
||||
clicon_err(OE_NETCONF, 0, "NETCONF framing error chunk-start: expected 1-9 or # but received %c (state:%d)", ch, *state);
|
||||
goto err;
|
||||
break;
|
||||
case 3:
|
||||
if (ch >= '0' && ch <= '9'){ /* other nums */
|
||||
*size = (*size)*10 + ch-'0';
|
||||
break;
|
||||
}
|
||||
else if (ch == '\n'){
|
||||
(*state)++;
|
||||
break;
|
||||
}
|
||||
clicon_err(OE_NETCONF, 0, "NETCONF framing error chunk-size: expected 0-9 or \\n but received %c (state:%d)", ch, *state);
|
||||
goto err;
|
||||
break;
|
||||
case 4:
|
||||
if (*size > 0){ /* chunk-data */
|
||||
(*size)--;
|
||||
retval = 1; /* chunk-data */
|
||||
break;
|
||||
}
|
||||
else if (*size == 0 && ch == '\n'){
|
||||
(*state)++;
|
||||
break;
|
||||
}
|
||||
clicon_err(OE_NETCONF, 0, "NETCONF framing error chunk-end: expected \\n but received %c (state:%d)", ch, *state);
|
||||
goto err;
|
||||
break;
|
||||
case 6:
|
||||
if (ch == '#'){
|
||||
(*state)++;
|
||||
break;
|
||||
}
|
||||
else if (ch >= '1' && ch <= '9'){ /* first num */
|
||||
*state=3;
|
||||
*size = ch-'0';
|
||||
break;
|
||||
}
|
||||
clicon_err(OE_NETCONF, 0, "NETCONF framing error: expected # but received %c (state:%d)", ch, *state);
|
||||
goto err;
|
||||
break;
|
||||
case 7:
|
||||
if (ch == '\n'){
|
||||
(*state) = 0;
|
||||
retval = 2; /* end of frame */
|
||||
break;
|
||||
}
|
||||
clicon_err(OE_NETCONF, 0, "NETCONF framing error chunk-end: expected \\n but received %c (state:%d)", ch, *state);
|
||||
goto err;
|
||||
break;
|
||||
default:
|
||||
clicon_err(OE_NETCONF, 0, "NETCONF framing error %c , invalid state:%d", ch, *state);
|
||||
goto err;
|
||||
break;
|
||||
}
|
||||
done:
|
||||
return retval;
|
||||
err:
|
||||
*state = 0;
|
||||
retval = -1; /* Error */
|
||||
goto done;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@
|
|||
#include "clixon_sig.h"
|
||||
#include "clixon_xml.h"
|
||||
#include "clixon_xml_io.h"
|
||||
#include "clixon_netconf_lib.h"
|
||||
#include "clixon_options.h"
|
||||
#include "clixon_proto.h"
|
||||
|
||||
|
|
@ -427,7 +428,7 @@ clicon_msg_rcv(int s,
|
|||
/*! Receive a message using plain NETCONF
|
||||
*
|
||||
* @param[in] s socket (unix or inet) to communicate with backend
|
||||
* @param[out] cb1 cligen buf struct containing the incoming message
|
||||
* @param[out] cb cligen buf struct containing the incoming message
|
||||
* @param[out] eof Set if eof encountered
|
||||
* @see netconf_input_cb()
|
||||
* @see clicon_msg_rcv using IPC message struct
|
||||
|
|
@ -499,7 +500,6 @@ clicon_msg_send1(int s,
|
|||
{
|
||||
int retval = -1;
|
||||
|
||||
cprintf(cb, "]]>]]>");
|
||||
if (atomicio((ssize_t (*)(int, void *, size_t))write,
|
||||
s, cbuf_get(cb), cbuf_len(cb)+1) < 0){
|
||||
clicon_err(OE_CFG, errno, "atomicio");
|
||||
|
|
@ -667,6 +667,10 @@ clicon_rpc1(int sock,
|
|||
int retval = -1;
|
||||
|
||||
clicon_debug(1, "%s", __FUNCTION__);
|
||||
if (netconf_framing_preamble(NETCONF_SSH_CHUNKED, msg) < 0)
|
||||
goto done;
|
||||
if (netconf_framing_postamble(NETCONF_SSH_CHUNKED, msg) < 0)
|
||||
goto done;
|
||||
if (clicon_msg_send1(sock, msg) < 0)
|
||||
goto done;
|
||||
if (clicon_msg_rcv1(sock, msgret, eof) < 0)
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@
|
|||
#include "clixon_xpath.h"
|
||||
#include "clixon_proto.h"
|
||||
#include "clixon_err.h"
|
||||
#include "clixon_event.h"
|
||||
#include "clixon_stream.h"
|
||||
#include "clixon_err_string.h"
|
||||
#include "clixon_xml_nsctx.h"
|
||||
|
|
@ -187,7 +188,7 @@ clicon_rpc_msg(clicon_handle h,
|
|||
#endif
|
||||
clicon_debug(1, "%s request:%s", __FUNCTION__, msg->op_body);
|
||||
/* Create a socket and connect to it, either UNIX, IPv4 or IPv6 per config options */
|
||||
if (clicon_rpc_msg_once(h, msg, &retdata, &eof, NULL) < 0)
|
||||
if (clicon_rpc_msg_once(h, msg, &retdata, &eof, &s) < 0)
|
||||
goto done;
|
||||
if (eof){
|
||||
/* 2. check socket shutdown AFTER rpc */
|
||||
|
|
@ -195,17 +196,20 @@ clicon_rpc_msg(clicon_handle h,
|
|||
s = -1;
|
||||
clicon_client_socket_set(h, -1);
|
||||
#ifdef PROTO_RESTART_RECONNECT
|
||||
if (clicon_rpc_msg_once(h, msg, &retdata, &eof, NULL) < 0)
|
||||
goto done;
|
||||
if (eof){
|
||||
close(s);
|
||||
s = -1;
|
||||
clicon_client_socket_set(h, -1);
|
||||
clicon_err(OE_PROTO, ESHUTDOWN, "Unexpected close of CLICON_SOCK. Clixon backend daemon may have crashed.");
|
||||
goto done;
|
||||
if (!clixon_exit_get()) { /* May be part of termination */
|
||||
if (clicon_rpc_msg_once(h, msg, &retdata, &eof, NULL) < 0)
|
||||
goto done;
|
||||
if (eof){
|
||||
close(s);
|
||||
s = -1;
|
||||
clicon_client_socket_set(h, -1);
|
||||
|
||||
clicon_err(OE_PROTO, ESHUTDOWN, "Unexpected close of CLICON_SOCK. Clixon backend daemon may have crashed.");
|
||||
goto done;
|
||||
}
|
||||
/* To disable this restart, unset PROTO_RESTART_RECONNECT */
|
||||
clicon_log(LOG_WARNING, "The backend was probably restarted and the client has reconnected to the backend. Any locks or candidate edits are lost.");
|
||||
}
|
||||
/* To disable this restart, unset PROTO_RESTART_RECONNECT */
|
||||
clicon_log(LOG_WARNING, "The backend was probably restarted and the client has reconnected to the backend. Any locks or candidate edits are lost.");
|
||||
#else
|
||||
clicon_err(OE_PROTO, ESHUTDOWN, "Unexpected close of CLICON_SOCK. Clixon backend daemon may have crashed.");
|
||||
goto done;
|
||||
|
|
@ -269,6 +273,9 @@ clicon_rpc_msg_persistent(clicon_handle h,
|
|||
close(s);
|
||||
s = -1;
|
||||
clicon_client_socket_set(h, -1);
|
||||
/* Note here one could try a restart as done in clicon_rpc_msg, but seems not
|
||||
* right since if backend is restarted, the notification stream is gone.
|
||||
*/
|
||||
clicon_err(OE_PROTO, ESHUTDOWN, "Unexpected close of CLICON_SOCK. Clixon backend daemon may have crashed.");
|
||||
goto done;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue