diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ae99e375..d26b529dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,15 @@ Expected: June 2021 * Yang deviation [deviation statement not yet support #211](https://github.com/clicon/clixon/issues/211) * See RFC7950 Sec 5.6.3 +### API changes on existing protocol/config features + +Users may have to change how they access the system + +* RESTCONF in Clixon used empty key as "wildchar". But according to RFC 8040 it should mean the "empty string". + * Example: `GET restconf/data/x:a=` + * Previous meaning (wrong): Return all `a` elements. + * New meaning (correct): Return the `a` instance with empty key string: "". + ### Minor features * Add default network namespace constant: `RESTCONF_NETNS_DEFAULT` with default value "default". @@ -46,6 +55,8 @@ Expected: June 2021 ### Corrected Bugs +* [RESTCONF GET request of single-key list with empty string returns all elements #213](https://github.com/clicon/clixon/issues/213) +* [RESTCONF GETof lists with empty string keys does not work #214](https://github.com/clicon/clixon/issues/214) * Fixed: [Multiple http requests in native restconf yields same reply #212](https://github.com/clicon/clixon/issues/212) ## 5.1.0 @@ -77,6 +88,8 @@ The 5.1 release contains more RESTCONF native mode restructuring, new multi-yang ### API changes on existing protocol/config features +Users may have to change how they access the system + * Native RESTCONF mode * Configure native mode changed to: `configure --with-restconf=native`, NOT `evhtp` * Use libevhtp from https://github.com/clixon/clixon-libevhtp.git, NOT from criticalstack diff --git a/lib/src/clixon_path.c b/lib/src/clixon_path.c index b90194d95..e3ec5ee33 100644 --- a/lib/src/clixon_path.c +++ b/lib/src/clixon_path.c @@ -713,7 +713,7 @@ api_path2xpath_cvv(cvec *api_path, goto done; } /* Check if has value, means '=' */ - if (cv2str(cv, NULL, 0) > 0){ + if (cv_type_get(cv) == CGV_STRING){ /* val is uri percent encoded, eg x%2Cy,z */ if ((val = cv2str_dup(cv)) == NULL) goto done; diff --git a/lib/src/clixon_string.c b/lib/src/clixon_string.c index 736489c74..d6004a32f 100644 --- a/lib/src/clixon_string.c +++ b/lib/src/clixon_string.c @@ -560,8 +560,9 @@ xml_chardata_cbuf_append(cbuf *cb, * err; * @endcode * - * a=b&c=d -> [[a,"b"][c="d"] - * kalle&c=d -> [[c="d"]] # Discard elements with no delim2 + * a=b&c=d -> [[a,"b"][c,"d"] + * a&b= -> [[a,null][b,""]] + * Note difference between empty (CGV_EMPTY) and empty string (CGV_STRING) * XXX differentiate between error and null cvec. */ int @@ -622,12 +623,11 @@ uri_str2cvec(char *string, } else{ if (strlen(s)){ - if ((cv = cvec_add(cvv, CGV_STRING)) == NULL){ + if ((cv = cvec_add(cvv, CGV_EMPTY)) == NULL){ clicon_err(OE_UNIX, errno, "cvec_add"); goto err; } cv_name_set(cv, s); - cv_string_set(cv, ""); } } s = snext; diff --git a/lib/src/clixon_xpath.c b/lib/src/clixon_xpath.c index 04eb34a1d..a1843e575 100644 --- a/lib/src/clixon_xpath.c +++ b/lib/src/clixon_xpath.c @@ -251,7 +251,7 @@ xpath_tree2cbuf(xpath_tree *xs, cprintf(xcb, "/"); break; case XP_PRIME_STR: - cprintf(xcb, "'%s'", xs->xs_s0); + cprintf(xcb, "'%s'", xs->xs_s0?xs->xs_s0:""); break; case XP_PRIME_NR: cprintf(xcb, "%s", xs->xs_strnr?xs->xs_strnr:"0"); diff --git a/lib/src/clixon_xpath_eval.c b/lib/src/clixon_xpath_eval.c index 0aa7ac239..62a5c032f 100644 --- a/lib/src/clixon_xpath_eval.c +++ b/lib/src/clixon_xpath_eval.c @@ -824,8 +824,12 @@ xp_relop(xp_ctx *xc1, s1 = xml_body(x); switch(op){ case XO_EQ: - if (s1 == NULL || s2 == NULL) - xr->xc_bool = (s1==NULL && s2 == NULL); + if (s1 == NULL && s2 == NULL) + xr->xc_bool = 1; + else if (s1 == NULL && strlen(s2) == 0) + xr->xc_bool = 1; + else if (strlen(s1) == 0 && s2 == NULL) + xr->xc_bool = 1; else xr->xc_bool = (strcmp(s1, s2)==0); break; diff --git a/lib/src/clixon_xpath_parse.y b/lib/src/clixon_xpath_parse.y index cfb452672..e016a4a02 100644 --- a/lib/src/clixon_xpath_parse.y +++ b/lib/src/clixon_xpath_parse.y @@ -441,9 +441,9 @@ predicates : predicates '[' expr ']' { $$=xp_new(XP_PRED,A_NAN,NULL, NULL, NULL primaryexpr : '(' expr ')' { $$=xp_new(XP_PRI0,A_NAN,NULL, NULL, NULL, $2, NULL); clicon_debug(3,"primaryexpr-> ( expr )"); } | NUMBER { $$=xp_new(XP_PRIME_NR,A_NAN, $1, NULL, NULL, NULL, NULL);clicon_debug(3,"primaryexpr-> NUMBER(%s)", $1); /*XXX*/} | QUOTE string QUOTE { $$=xp_new(XP_PRIME_STR,A_NAN,NULL, $2, NULL, NULL, NULL);clicon_debug(3,"primaryexpr-> \" string \""); } - | QUOTE QUOTE { $$=xp_new(XP_PRIME_STR,A_NAN,NULL, NULL, NULL, NULL, NULL);clicon_debug(3,"primaryexpr-> \" \""); } + | QUOTE QUOTE { $$=xp_new(XP_PRIME_STR,A_NAN,NULL, strdup(""), NULL, NULL, NULL);clicon_debug(3,"primaryexpr-> \" \""); } | APOST string APOST { $$=xp_new(XP_PRIME_STR,A_NAN,NULL, $2, NULL, NULL, NULL);clicon_debug(3,"primaryexpr-> ' string '"); } - | APOST APOST { $$=xp_new(XP_PRIME_STR,A_NAN,NULL, NULL, NULL, NULL, NULL);clicon_debug(3,"primaryexpr-> ' '"); } + | APOST APOST { $$=xp_new(XP_PRIME_STR,A_NAN,NULL, strdup(""), NULL, NULL, NULL);clicon_debug(3,"primaryexpr-> ' '"); } | FUNCTIONNAME ')' { if (($$ = xp_primary_function(_XPY, $1, NULL)) == NULL) YYERROR; clicon_debug(3,"primaryexpr-> functionname ()"); } | FUNCTIONNAME args ')' { if (($$ = xp_primary_function(_XPY, $1, $2)) == NULL) YYERROR; clicon_debug(3,"primaryexpr-> functionname (arguments)"); } ; diff --git a/test/test_restconf_listkey.sh b/test/test_restconf_listkey.sh index 821a6c711..c28eb847d 100755 --- a/test/test_restconf_listkey.sh +++ b/test/test_restconf_listkey.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash # Testcases for Restconf list and leaf-list keys, check matching keys for RFC8040 4.5: # the key values must match in URL and data +# Empty keys vs comma as a key # Magic line must be first in script (see README.md) s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi @@ -82,7 +83,7 @@ if [ $BE -ne 0 ]; then start_backend -s init -f $cfg fi -new "waiting" +new "wait backend" wait_backend if [ $RC -ne 0 ]; then @@ -91,11 +92,11 @@ if [ $RC -ne 0 ]; then new "start restconf daemon" start_restconf -f $cfg - - new "waiting" - wait_restconf fi +new "wait restconf" +wait_restconf + new "restconf PUT add whole list entry" expectpart "$(curl $CURLOPTS -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://localhost/restconf/data/list:c/a=x,y -d '{"list:a":{"b":"x","c":"y","nonkey":"0"}}')" 0 "HTTP/1.1 201 Created" @@ -112,6 +113,7 @@ expectpart "$(curl $CURLOPTS -X GET -H "Accept: application/yang-data+json" $RCP new "restconf PUT add whole list entry XML" expectpart "$(curl $CURLOPTS -X PUT -H 'Content-Type: application/yang-data+xml' -H 'Accept: application/yang-data+xml' -d 'xxxy0' $RCPROTO://localhost/restconf/data/list:c/a=xx,xy)" 0 "HTTP/1.1 201 Created" +new "restconf GET sub key" expectpart "$(curl $CURLOPTS -X GET -H "Accept: application/yang-data+json" $RCPROTO://localhost/restconf/data/list:c/a/b)" 0 'HTTP/1.1 400 Bad Request' '^{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"malformed key =a, expected ' new "restconf PUT change whole list entry (same keys)" @@ -147,6 +149,15 @@ expectpart "$(curl $CURLOPTS -X PUT -H "Content-Type: application/yang-data+json new "restconf PUT list-list" expectpart "$(curl $CURLOPTS -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://localhost/restconf/data/list:c/a=x,y/e=z -d '{"list:e":{"f":"z","nonkey":"0"}}')" 0 "HTTP/1.1 201 Created" +new "restconf GET e element with empty string key expect empty" +expectpart "$(curl $CURLOPTS -X GET -H "Accept: application/yang-data+json" $RCPROTO://localhost/restconf/data/list:c/a=x,y/e=)" 0 "HTTP/1.1 404 Not Found" '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"Instance does not exist"}}}' + +new "restconf PUT list-list empty string" +expectpart "$(curl $CURLOPTS -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://localhost/restconf/data/list:c/a=x,y/e= -d '{"list:e":{"f":"","nonkey":"42"}}')" 0 "HTTP/1.1 201 Created" + +new "restconf GET e element with empty string key" +expectpart "$(curl $CURLOPTS -X GET -H "Accept: application/yang-data+xml" $RCPROTO://localhost/restconf/data/list:c/a=x,y/e=)" 0 "HTTP/1.1 200 OK" '42' + new "restconf PUT change list-lst entry (wrong keys)(expect fail)" expectpart "$(curl $CURLOPTS -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://localhost/restconf/data/list:c/a=x,y/e=z -d '{"list:e":{"f":"wrong","nonkey":"0"}}')" 0 "HTTP/1.1 412 Precondition Failed" '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"api-path keys do not match data keys"}}}' @@ -174,6 +185,33 @@ expectpart "$(curl $CURLOPTS -X PUT -H "Content-Type: application/yang-data+json new "restconf GET check percent-encoded" expectpart "$(curl $CURLOPTS -X GET -H "Accept: application/yang-data+json" $RCPROTO://localhost/restconf/data/list:c/a=x%2Cy,z)" 0 "HTTP/1.1 200 OK" '{"list:a":\[{"b":"x,y","c":"z","nonkey":"foo"}\]}' +new "restconf PUT ,z empty string as first key" +expectpart "$(curl $CURLOPTS -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://localhost/restconf/data/list:c/a=,z -d '{"list:a":{"b":"","c":"z", "nonkey":"42"}}')" 0 "HTTP/1.1 201 Created" + +new "restconf GET ,z empty" +expectpart "$(curl $CURLOPTS -X GET -H "Accept: application/yang-data+json" $RCPROTO://localhost/restconf/data/list:c/a=,z)" 0 "HTTP/1.1 200 OK" '{"list:a":\[{"b":"","c":"z","nonkey":"42"}\]}' + +new "restconf PUT x, empty string as second key" +expectpart "$(curl $CURLOPTS -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://localhost/restconf/data/list:c/a=x, -d '{"list:a":{"b":"x","c":"", "nonkey":"43"}}')" 0 "HTTP/1.1 201 Created" + +new "restconf GET x, empty" +expectpart "$(curl $CURLOPTS -X GET -H "Accept: application/yang-data+json" $RCPROTO://localhost/restconf/data/list:c/a=x,)" 0 "HTTP/1.1 200 OK" '{"list:a":\[{"b":"x","c":"","nonkey":"43"}\]}' + +new "restconf PUT , empty string as second key" +expectpart "$(curl $CURLOPTS -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://localhost/restconf/data/list:c/a=, -d '{"list:a":{"b":"","c":"", "nonkey":"44"}}')" 0 "HTTP/1.1 201 Created" + +new "restconf GET , empty" +expectpart "$(curl $CURLOPTS -X GET -H "Accept: application/yang-data+json" $RCPROTO://localhost/restconf/data/list:c/a=,)" 0 "HTTP/1.1 200 OK" '{"list:a":\[{"b":"","c":"","nonkey":"44"}\]}' + +new "restconf PUT two commas as keys" +expectpart "$(curl $CURLOPTS -X PUT -H "Content-Type: application/yang-data+json" $RCPROTO://localhost/restconf/data/list:c/a=%2C,%2C -d '{"list:a":{"b":",","c":",", "nonkey":"45"}}')" 0 "HTTP/1.1 201 Created" + +new "restconf GET two commas" +expectpart "$(curl $CURLOPTS -X GET -H "Accept: application/yang-data+json" $RCPROTO://localhost/restconf/data/list:c/a=%2C,%2C)" 0 "HTTP/1.1 200 OK" '{"list:a":\[{"b":",","c":",","nonkey":"45"}\]}' + +new "restconf GET all empty strings" +expectpart "$(curl $CURLOPTS -X GET -H "Accept: application/yang-data+json" $RCPROTO://localhost/restconf/data/list:c)" 0 "HTTP/1.1 200 OK" '{"list:c":{"a":\[{"b":"","c":"","nonkey":"44"},{"b":"","c":"z","nonkey":"42"},{"b":",","c":",","nonkey":"45"},{"b":"x","c":"","nonkey":"43"}' + if [ $RC -ne 0 ]; then new "Kill restconf daemon" stop_restconf diff --git a/test/test_restconf_patch.sh b/test/test_restconf_patch.sh index 8511437a0..4351cdb9d 100755 --- a/test/test_restconf_patch.sh +++ b/test/test_restconf_patch.sh @@ -111,7 +111,7 @@ if [ $BE -ne 0 ]; then start_backend -s startup -f $cfg fi -new "waiting" +new "wait backend" wait_backend if [ $RC -ne 0 ]; then @@ -120,11 +120,11 @@ if [ $RC -ne 0 ]; then new "start restconf daemon" start_restconf -f $cfg - - new "waiting restconf" - wait_restconf fi +new "wait restconf" +wait_restconf + # also in test_restconf.sh new "MUST support the PATCH method for a plain patch" expectpart "$(curl -u andy:bar $CURLOPTS -X OPTIONS $RCPROTO://localhost/restconf/data)" 0 "HTTP/1.1 200 OK" "Allow: OPTIONS,HEAD,GET,POST,PUT,PATCH,DELETE" "Accept-Patch: application/yang-data+xml,application/yang-data+json" @@ -174,7 +174,7 @@ if [ $BE -ne 0 ]; then start_backend -s startup -f $cfg fi -new "waiting" +new "wait backend" wait_backend if [ $RC -ne 0 ]; then @@ -183,11 +183,11 @@ if [ $RC -ne 0 ]; then new "start restconf daemon" start_restconf -f $cfg - - new "waiting" - wait_restconf fi +new "wait restconf" +wait_restconf + # 4.6.1. Plain Patch new "Create album London Calling with PUT" expectpart "$(curl -u andy:bar $CURLOPTS -X PUT -H 'Content-Type: application/yang-data+json' $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d '{"example-jukebox:album":{"name":"London Calling"}}')" 0 "HTTP/1.1 201 Created"