diff --git a/apps/restconf/restconf_main.c b/apps/restconf/restconf_main.c index d8cdb27d..309c5223 100644 --- a/apps/restconf/restconf_main.c +++ b/apps/restconf/restconf_main.c @@ -212,7 +212,7 @@ api_well_known(clicon_handle h, FCGX_SetExitStatus(200, r->out); /* OK */ FCGX_FPrintF(r->out, "\n"); FCGX_FPrintF(r->out, " \n"); - FCGX_FPrintF(r->out, "\n"); + FCGX_FPrintF(r->out, "\r\n"); return 0; } @@ -260,7 +260,7 @@ api_root(clicon_handle h, if (xml2json_cbuf(cb, xt, pretty) < 0) goto done; FCGX_FPrintF(r->out, "%s", cb?cbuf_get(cb):""); - FCGX_FPrintF(r->out, "\n\n"); + FCGX_FPrintF(r->out, "\r\n\r\n"); retval = 0; done: if (cb) diff --git a/apps/restconf/restconf_methods.c b/apps/restconf/restconf_methods.c index 78f96ece..fde9c4dc 100644 --- a/apps/restconf/restconf_methods.c +++ b/apps/restconf/restconf_methods.c @@ -189,12 +189,12 @@ api_return_err(clicon_handle h, if (pretty){ FCGX_FPrintF(r->out, " \n", cbuf_get(cb)); FCGX_FPrintF(r->out, "%s", cbuf_get(cb)); - FCGX_FPrintF(r->out, " \n"); + FCGX_FPrintF(r->out, " \r\n"); } else { FCGX_FPrintF(r->out, "", cbuf_get(cb)); FCGX_FPrintF(r->out, "%s", cbuf_get(cb)); - FCGX_FPrintF(r->out, "\n"); + FCGX_FPrintF(r->out, "\r\n"); } } else{ @@ -202,13 +202,13 @@ api_return_err(clicon_handle h, FCGX_FPrintF(r->out, "{\n"); FCGX_FPrintF(r->out, " \"ietf-restconf:errors\" : %s\n", cbuf_get(cb)); - FCGX_FPrintF(r->out, "}\n"); + FCGX_FPrintF(r->out, "}\r\n"); } else{ FCGX_FPrintF(r->out, "{"); FCGX_FPrintF(r->out, "\"ietf-restconf:errors\" : "); FCGX_FPrintF(r->out, "%s", cbuf_get(cb)); - FCGX_FPrintF(r->out, "}\n"); + FCGX_FPrintF(r->out, "}\r\n"); } } ok: @@ -234,6 +234,7 @@ api_return_err(clicon_handle h, * @code * curl -G http://localhost/restconf/data/interfaces/interface=eth0 * @endcode + * See RFC8040 Sec 4.2 and 4.3 * XXX: cant find a way to use Accept request field to choose Content-Type * I would like to support both xml and json. * Request may contain @@ -337,10 +338,9 @@ api_data_get2(clicon_handle h, if (xml2json_cbuf_vec(cbx, xvec, xlen, pretty) < 0) goto done; } - clicon_debug(1, "%s cbuf:%s", __FUNCTION__, cbuf_get(cbx)); FCGX_FPrintF(r->out, "%s", cbx?cbuf_get(cbx):""); - FCGX_FPrintF(r->out, "\n\n"); + FCGX_FPrintF(r->out, "\r\n\r\n"); ok: retval = 0; done: @@ -437,6 +437,8 @@ api_data_get(clicon_handle h, * @param[in] parse_xml Set to 0 for JSON and 1 for XML for input data * @note restconf POST is mapped to edit-config create. + * See RFC8040 Sec 4.4.1 + POST: target resource type is datastore --> create a top-level resource target resource type is data resource --> create child resource @@ -520,7 +522,7 @@ api_data_post(clicon_handle h, badrequest(r); goto ok; } - /* The message-body MUST contain exactly one instance of the + /* 4.4.1: The message-body MUST contain exactly one instance of the * expected data resource. */ if (xml_child_nr(xdata) != 1){ @@ -649,6 +651,7 @@ match_list_keys(yang_stmt *y, * @param[in] parse_xml Set to 0 for JSON and 1 for XML for input data * @note restconf PUT is mapped to edit-config replace. + * See RFC8040 Sec 4.5 * @example curl -X PUT -d '{"enabled":"false"}' http://127.0.0.1/restconf/data/interfaces/interface=eth1 * @@ -823,6 +826,7 @@ api_data_put(clicon_handle h, * @param[in] data Stream input data * @param[in] username Authenticated user * Netconf: (nc:operation="merge") + * See RFC8040 Sec 4.6 */ int api_data_patch(clicon_handle h, @@ -846,6 +850,7 @@ api_data_patch(clicon_handle h, * @param[in] username Authenticated user * @param[in] pretty Set to 1 for pretty-printed xml/json output * @param[in] use_xml Set to 0 for JSON and 1 for XML + * See RFC 8040 Sec 4.7 * Example: * curl -X DELETE http://127.0.0.1/restconf/data/interfaces/interface=eth0 * Netconf: (nc:operation="delete") @@ -1020,9 +1025,9 @@ api_operations_get(clicon_handle h, clicon_debug(1, "%s ret:%s", __FUNCTION__, cbuf_get(cbx)); FCGX_SetExitStatus(200, r->out); /* OK */ FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); - FCGX_FPrintF(r->out, "\n"); + FCGX_FPrintF(r->out, "\r\n"); FCGX_FPrintF(r->out, "%s", cbx?cbuf_get(cbx):""); - FCGX_FPrintF(r->out, "\n\n"); + FCGX_FPrintF(r->out, "\r\n\r\n"); // ok: retval = 0; done: @@ -1046,7 +1051,7 @@ api_operations_get(clicon_handle h, * @param[in] pretty Set to 1 for pretty-printed xml/json output * @param[in] use_xml Set to 0 for JSON and 1 for XML for output data * @param[in] parse_xml Set to 0 for JSON and 1 for XML for input data - + * See RFC 8040 Sec 3.6 / 4.4.2 * @note We map post to edit-config create. POST {+restconf}/operations/ */ @@ -1193,7 +1198,7 @@ api_operations_post(clicon_handle h, goto done; clicon_debug(1, "%s xoutput:%s", __FUNCTION__, cbuf_get(cbx)); FCGX_FPrintF(r->out, "%s", cbx?cbuf_get(cbx):""); - FCGX_FPrintF(r->out, "\n\n"); + FCGX_FPrintF(r->out, "\r\n\r\n"); } ok: retval = 0; diff --git a/doc/FAQ.md b/doc/FAQ.md index ee04a409..0debf435 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -83,11 +83,11 @@ configuration file is installed at /usr/local/etc/routing.xml. The YANG specification for the configuration file is clixon-config.yang. You can change where CLixon looks for the configuration FILE as follows: - # Provide -f FILE option when starting a program (eg clixon_backend -f FILE) - # Provide --with-configfile=FILE when configuring - # Provide --with-sysconfig= when configuring, then FILE is /clixon.xml - # Provide --sysconfig= when configuring then FILE is /etc/clixon.xml - # FILE is /usr/local/etc/clixon.xml + - Provide -f FILE option when starting a program (eg clixon_backend -f FILE) + - Provide --with-configfile=FILE when configuring + - Provide --with-sysconfig= when configuring, then FILE is /clixon.xml + - Provide --sysconfig= when configuring then FILE is /etc/clixon.xml + - FILE is /usr/local/etc/clixon.xml ## Can I run Clixon as docker containers? Yes, the example works as docker containers as well. backend and cli needs a diff --git a/lib/clixon/clixon_plugin.h b/lib/clixon/clixon_plugin.h index 22ef53b5..f102de3d 100644 --- a/lib/clixon/clixon_plugin.h +++ b/lib/clixon/clixon_plugin.h @@ -73,12 +73,16 @@ typedef int (plgstart_t)(clicon_handle, int, char **); /* Plugin start */ #define PLUGIN_EXIT "plugin_exit" typedef int (plgexit_t)(clicon_handle); /* Plugin exit */ -/*! Called by restconf - * Returns 0 if credentials OK, -1 if failed +/*! Called by restconf to ceck credentials and return username */ #define PLUGIN_CREDENTIALS "plugin_credentials" + /* Plugin credentials - * username should be freed after use + * @param[in] Clicon handle + * @param[in] void*, eg Fastcgihandle request restconf + * @param[out] username should be freed after use + * @retval 0 if credentials OK + * @retval -1 credentials not OK */ typedef int (plgcredentials_t)(clicon_handle, void *, char **username); diff --git a/test/lib.sh b/test/lib.sh index 61dc3c84..b6964e2e 100755 --- a/test/lib.sh +++ b/test/lib.sh @@ -50,12 +50,14 @@ new(){ expectfn(){ cmd=$1 expect=$2 + if [ $# = 3 ]; then expect2=$3 else expect2= fi - ret=`$cmd` + ret=$($cmd) + # if [ $? -ne 0 ]; then # err "wrong args" # fi @@ -84,6 +86,7 @@ expectfn(){ expecteq2(){ ret=$1 expect=$2 + # Match if both are empty string if [ -z "$ret" -a -z "$expect" ]; then return diff --git a/test/test_restconf.sh b/test/test_restconf.sh index c6b22734..7562a8f4 100755 --- a/test/test_restconf.sh +++ b/test/test_restconf.sh @@ -81,17 +81,20 @@ new "restconf tests" new "restconf root discovery. RFC 8040 3.1 (xml+xrd)" expecteq2 "$(curl -s -X GET http://localhost/.well-known/host-meta)" " -" + " new "restconf get restconf resource. RFC 8040 3.3 (json)" -expecteq2 "$(curl -sG http://localhost/restconf)" '{"restconf": {"data": null,"operations": null,"yang-library-version": "2016-06-21"}}' +expecteq2 "$(curl -sG http://localhost/restconf)" '{"restconf": {"data": null,"operations": null,"yang-library-version": "2016-06-21"}} + ' new "restconf get restconf resource. RFC 8040 3.3 (xml)" # Get XML instead of JSON? -expecteq2 $(curl -s -H "Accept: application/yang-data+xml" -G http://localhost/restconf) '2016-06-21' +expecteq2 "$(curl -s -H 'Accept: application/yang-data+xml' -G http://localhost/restconf)" '2016-06-21 + ' -new "restconf get restconf/operations. RFC8040 3.3.2" -expecteq2 "$(curl -sG http://localhost/restconf/operations)" '{"operations": {"ex:empty": null,"ex:input": null,"ex:output": null,"rt:fib-route": null,"rt:route-count": null}}' +new "restconf get restconf/operations. RFC8040 3.3.2 (json)" +expecteq2 "$(curl -sG http://localhost/restconf/operations)" '{"operations": {"ex:empty": null,"ex:input": null,"ex:output": null,"rt:fib-route": null,"rt:route-count": null}} + ' new "restconf get restconf/operations. RFC8040 3.3.2 (xml)" ret=$(curl -s -H "Accept: application/yang-data+xml" -G http://localhost/restconf/operations) @@ -120,10 +123,12 @@ expectfn "curl -s -I http://localhost/restconf/data" "HTTP/1.1 200 OK" #Content-Type: application/yang-data+json" new "restconf empty rpc" -expecteq2 "$(curl -s -X POST -d {\"input\":{\"name\":\"\"}} http://localhost/restconf/operations/ex:empty)" '{"output": null}' +expecteq2 "$(curl -s -X POST -d {\"input\":{\"name\":\"\"}} http://localhost/restconf/operations/ex:empty)" '{"output": null} + ' new "restconf get empty config + state json" -expecteq2 "$(curl -sSG http://localhost/restconf/data)" '{"data": {"interfaces-state": {"interface": [{"name": "eth0","type": "eth","if-index": 42}]}}}' +expecteq2 "$(curl -sSG http://localhost/restconf/data)" '{"data": {"interfaces-state": {"interface": [{"name": "eth0","type": "eth","if-index": 42}]}}} + ' new "restconf get empty config + state xml" ret=$(curl -s -H "Accept: application/yang-data+xml" -G http://localhost/restconf/data) @@ -134,7 +139,8 @@ if [ -z "$match" ]; then fi new "restconf get data/interfaces-state/interface=eth0 json" -expecteq2 "$(curl -s -G http://localhost/restconf/data/interfaces-state/interface=eth0)" '{"interface": [{"name": "eth0","type": "eth","if-index": 42}]}' +expecteq2 "$(curl -s -G http://localhost/restconf/data/interfaces-state/interface=eth0)" '{"interface": [{"name": "eth0","type": "eth","if-index": 42}]} + ' new "restconf get state operation eth0 xml" # Cant get shell macros to work, inline matching from lib.sh @@ -146,7 +152,8 @@ if [ -z "$match" ]; then fi new "restconf get state operation eth0 type json" -expecteq2 "$(curl -s -G http://localhost/restconf/data/interfaces-state/interface=eth0/type)" '{"type": "eth"}' +expecteq2 "$(curl -s -G http://localhost/restconf/data/interfaces-state/interface=eth0/type)" '{"type": "eth"} + ' new "restconf get state operation eth0 type xml" # Cant get shell macros to work, inline matching from lib.sh @@ -158,7 +165,8 @@ if [ -z "$match" ]; then fi new "restconf GET datastore" -expecteq2 "$(curl -s -X GET http://localhost/restconf/data)" '{"data": {"interfaces-state": {"interface": [{"name": "eth0","type": "eth","if-index": 42}]}}}' +expecteq2 "$(curl -s -X GET http://localhost/restconf/data)" '{"data": {"interfaces-state": {"interface": [{"name": "eth0","type": "eth","if-index": 42}]}}} + ' # Exact match new "restconf Add subtree to datastore using POST" @@ -171,8 +179,8 @@ expectfn 'curl -s -X POST -d {"interfaces":{"interface":{"name":"eth/0/0","type" #expecteq2 "$(curl -s -X POST -d {\"interfaces\":{\"interface\":{\"name\":\"eth/0/0\",\"type\":\"eth\",\"enabled\":true}}} http://localhost/restconf/data)" '{"ietf-restconf:errors" : {"error": {"error-tag": "data-exists","error-type": "application","error-severity": "error","error-message": "Data already exists; cannot create new resource"}}}' new "restconf Check interfaces eth/0/0 added" -expectfn "curl -s -G http://localhost/restconf/data" '{"interfaces": {"interface": \[{"name": "eth/0/0","type": "eth","enabled": true}\]},"interfaces-state": {"interface": \[{"name": "eth0","type": "eth","if-index": 42}\]}} -$' +expectfn "curl -s -G http://localhost/restconf/data" '{"interfaces": {"interface": \[{"name": "eth/0/0","type": "eth","enabled": true}\]},"interfaces-state": {"interface": \[{"name": "eth0","type": "eth","if-index": 42}\]}} + ' new "restconf delete interfaces" expecteq2 $(curl -s -X DELETE http://localhost/restconf/data/interfaces) "" @@ -186,10 +194,11 @@ expectfn 'curl -s -X POST -d {"interface":{"name":"eth/0/0","type":"eth","enable #expecteq2 "$(curl -s -X POST -d '{"interface":{"name":"eth/0/0","type\":"eth","enabled":true}}' http://localhost/restconf/data/interfaces)" "" new "restconf Check eth/0/0 added" -expecteq 'curl -s -G http://localhost/restconf/data' '{"data": {"interfaces": {"interface": [{"name": "eth/0/0","type": "eth","enabled": true}]},"interfaces-state": {"interface": [{"name": "eth0","type": "eth","if-index": 42}]}}}' +expecteq 'curl -s -G http://localhost/restconf/data' '{"data": {"interfaces": {"interface": [{"name": "eth/0/0","type": "eth","enabled": true}]},"interfaces-state": {"interface": [{"name": "eth0","type": "eth","if-index": 42}]}}} + ' new "restconf Re-post eth/0/0 which should generate error" -expecteq 'curl -s -X POST -d {"interface":{"name":"eth/0/0","type":"eth","enabled":true}} http://localhost/restconf/data/interfaces' '{"ietf-restconf:errors" : {"error": {"error-tag": "data-exists","error-type": "application","error-severity": "error","error-message": "Data already exists; cannot create new resource"}}}' +expecteq 'curl -s -X POST -d {"interface":{"name":"eth/0/0","type":"eth","enabled":true}} http://localhost/restconf/data/interfaces' '{"ietf-restconf:errors" : {"error": {"error-tag": "data-exists","error-type": "application","error-severity": "error","error-message": "Data already exists; cannot create new resource"}}} ' new "Add leaf description using POST" expecteq 'curl -s -X POST -d {"description":"The-first-interface"} http://localhost/restconf/data/interfaces/interface=eth%2f0%2f0' "" @@ -198,7 +207,8 @@ new "Add nothing using POST" expectfn 'curl -s -X POST http://localhost/restconf/data/interfaces/interface=eth%2f0%2f0' "data is in some way badly formed" new "restconf Check description added" -expecteq "curl -s -G http://localhost/restconf/data" '{"data": {"interfaces": {"interface": [{"name": "eth/0/0","description": "The-first-interface","type": "eth","enabled": true}]},"interfaces-state": {"interface": [{"name": "eth0","type": "eth","if-index": 42}]}}}' +expecteq "curl -s -G http://localhost/restconf/data" '{"data": {"interfaces": {"interface": [{"name": "eth/0/0","description": "The-first-interface","type": "eth","enabled": true}]},"interfaces-state": {"interface": [{"name": "eth0","type": "eth","if-index": 42}]}}} + ' new "restconf delete eth/0/0" expecteq 'curl -s -X DELETE http://localhost/restconf/data/interfaces/interface=eth%2f0%2f0' "" @@ -207,16 +217,18 @@ new "Check deleted eth/0/0" expectfn 'curl -s -G http://localhost/restconf/data' $state new "restconf Re-Delete eth/0/0 using none should generate error" -expecteq 'curl -s -X DELETE http://localhost/restconf/data/interfaces/interface=eth%2f0%2f0' '{"ietf-restconf:errors" : {"error": {"error-tag": "data-missing","error-type": "application","error-severity": "error","error-message": "Data does not exist; cannot delete resource"}}}' +expecteq 'curl -s -X DELETE http://localhost/restconf/data/interfaces/interface=eth%2f0%2f0' '{"ietf-restconf:errors" : {"error": {"error-tag": "data-missing","error-type": "application","error-severity": "error","error-message": "Data does not exist; cannot delete resource"}}} ' new "restconf Add subtree eth/0/0 using PUT" expecteq 'curl -s -X PUT -d {"interface":{"name":"eth/0/0","type":"eth","enabled":true}} http://localhost/restconf/data/interfaces/interface=eth%2f0%2f0' "" new "restconf get subtree" -expecteq 'curl -s -G http://localhost/restconf/data' '{"data": {"interfaces": {"interface": [{"name": "eth/0/0","type": "eth","enabled": true}]},"interfaces-state": {"interface": [{"name": "eth0","type": "eth","if-index": 42}]}}}' +expecteq 'curl -s -G http://localhost/restconf/data' '{"data": {"interfaces": {"interface": [{"name": "eth/0/0","type": "eth","enabled": true}]},"interfaces-state": {"interface": [{"name": "eth0","type": "eth","if-index": 42}]}}} + ' new "restconf rpc using POST json" -expecteq 'curl -s -X POST -d {"input":{"routing-instance-name":"ipv4"}} http://localhost/restconf/operations/rt:fib-route' '{"output": {"route": {"address-family": "ipv4","next-hop": {"next-hop-list": "2.3.4.5"}}}}' +expecteq 'curl -s -X POST -d {"input":{"routing-instance-name":"ipv4"}} http://localhost/restconf/operations/rt:fib-route' '{"output": {"route": {"address-family": "ipv4","next-hop": {"next-hop-list": "2.3.4.5"}}}} + ' new "restconf rpc using POST xml" ret=$(curl -s -X POST -H "Accept: application/yang-data+xml" -d '{"input":{"routing-instance-name":"ipv4"}}' http://localhost/restconf/operations/rt:fib-route) diff --git a/test/test_restconf2.sh b/test/test_restconf2.sh index 70e84bfa..37bb8708 100755 --- a/test/test_restconf2.sh +++ b/test/test_restconf2.sh @@ -84,10 +84,10 @@ new "restconf POST interface" expectfn 'curl -s -X POST -d {"interface":{"name":"TEST","type":"eth0"}} http://localhost/restconf/data/cont1' "" new "restconf POST again" -expecteq 'curl -s -X POST -d {"interface":{"name":"TEST","type":"eth0"}} http://localhost/restconf/data/cont1' '{"ietf-restconf:errors" : {"error": {"error-tag": "data-exists","error-type": "application","error-severity": "error","error-message": "Data already exists; cannot create new resource"}}}' +expecteq 'curl -s -X POST -d {"interface":{"name":"TEST","type":"eth0"}} http://localhost/restconf/data/cont1' '{"ietf-restconf:errors" : {"error": {"error-tag": "data-exists","error-type": "application","error-severity": "error","error-message": "Data already exists; cannot create new resource"}}} ' new "restconf POST from top" -expecteq 'curl -s -X POST -d {"cont1":{"interface":{"name":"TEST","type":"eth0"}}} http://localhost/restconf/data' '{"ietf-restconf:errors" : {"error": {"error-tag": "data-exists","error-type": "application","error-severity": "error","error-message": "Data already exists; cannot create new resource"}}}' +expecteq 'curl -s -X POST -d {"cont1":{"interface":{"name":"TEST","type":"eth0"}}} http://localhost/restconf/data' '{"ietf-restconf:errors" : {"error": {"error-tag": "data-exists","error-type": "application","error-severity": "error","error-message": "Data already exists; cannot create new resource"}}} ' new "restconf DELETE" expectfn 'curl -s -X DELETE http://localhost/restconf/data/cont1' ""