/* * ***** BEGIN LICENSE BLOCK ***** Copyright (C) 2009-2019 Olof Hagsand Copyright (C) 2020 Olof Hagsand and Rubicon Communications, LLC This file is part of CLIXON. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Alternatively, the contents of this file may be used under the terms of the GNU General Public License Version 3 or later (the "GPL"), in which case the provisions of the GPL are applicable instead of those above. If you wish to allow use of your version of this file only under the terms of the GPL, and not to allow others to use your version of this file under the terms of Apache License version 2, indicate your decision by deleting the provisions above and replace them with the notice and other provisions required by the GPL. If you do not delete the provisions above, a recipient may use your version of this file under the terms of any one of the Apache License version 2 or the GPL. ***** END LICENSE BLOCK ***** * Restconf method implementation for post: operation(rpc) and data * From RFC 8040 Section 4.4. POST */ #ifdef HAVE_CONFIG_H #include "clixon_config.h" /* generated by config & autoconf */ #endif #include #include #include #include #include #include #include #include #include #include #include #include /* cligen */ #include /* clicon */ #include #include /* Need to be after clixon_xml.h due to attribute format */ #include "restconf_lib.h" #include "restconf_methods_post.h" /*! Generic REST POST method * @param[in] h CLIXON handle * @param[in] r Fastcgi request handle * @param[in] api_path According to restconf (Sec 3.5.3.1 in rfc8040) * @param[in] pcvec Vector of path ie DOCUMENT_URI element * @param[in] pi Offset, where to start pcvec * @param[in] qvec Vector of query string (QUERY_STRING) * @param[in] data Stream input data * @param[in] pretty Set to 1 for pretty-printed xml/json output * @param[in] media_out Output media * 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 The message-body MUST contain exactly one instance of the expected data resource. The data model for the child tree is the subtree, as defined by YANG for the child resource. If the POST method succeeds, a "201 Created" status-line is returned and there is no response message-body. A "Location" header identifying the child resource that was created MUST be present in the response in this case. If the data resource already exists, then the POST request MUST fail and a "409 Conflict" status-line MUST be returned. * @see RFC8040 Section 4.4 * @see api_data_put */ int api_data_post(clicon_handle h, FCGX_Request *r, char *api_path, int pi, cvec *qvec, char *data, int pretty, restconf_media media_out) { int retval = -1; enum operation_type op = OP_CREATE; cxobj *xdata = NULL; /* The actual data object to modify */ int i; cbuf *cbx = NULL; cxobj *xtop = NULL; /* top of api-path */ cxobj *xbot = NULL; /* bottom of api-path */ yang_stmt *ybot = NULL; /* yang of xbot */ yang_stmt *ymoddata = NULL; /* yang module of data (-d) */ yang_stmt *yspec; yang_stmt *ydata; cxobj *xa; cxobj *xret = NULL; cxobj *xretcom = NULL; /* return from commit */ cxobj *xretdis = NULL; /* return from discard-changes */ cxobj *xerr = NULL; /* malloced must be freed */ cxobj *xe; /* dont free */ cxobj *x; char *username; int ret; restconf_media media_in; int nrchildren0 = 0; yang_bind yb; clicon_debug(1, "%s api_path:\"%s\"", __FUNCTION__, api_path); clicon_debug(1, "%s data:\"%s\"", __FUNCTION__, data); if ((yspec = clicon_dbspec_yang(h)) == NULL){ clicon_err(OE_FATAL, 0, "No DB_SPEC"); goto done; } for (i=0; i", username?username:"", NETCONF_BASE_PREFIX, NETCONF_BASE_NAMESPACE); /* bind nc to netconf namespace */ cprintf(cbx, ""); cprintf(cbx, "none"); if (clicon_xml2cbuf(cbx, xtop, 0, 0, -1) < 0) goto done; cprintf(cbx, ""); clicon_debug(1, "%s xml: %s api_path:%s",__FUNCTION__, cbuf_get(cbx), api_path); if (clicon_rpc_netconf(h, cbuf_get(cbx), &xret, NULL) < 0) goto done; if ((xe = xpath_first(xret, NULL, "//rpc-error")) != NULL){ if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } /* Assume this is validation failed since commit includes validate */ cbuf_reset(cbx); /* commit/discard should be done automaticaly by the system, therefore * recovery user is used here (edit-config but not commit may be permitted by NACM */ cprintf(cbx, "", clicon_nacm_recovery_user(h)); cprintf(cbx, ""); if (clicon_rpc_netconf(h, cbuf_get(cbx), &xretcom, NULL) < 0) goto done; if ((xe = xpath_first(xretcom, NULL, "//rpc-error")) != NULL){ cbuf_reset(cbx); cprintf(cbx, "", username?username:""); cprintf(cbx, ""); if (clicon_rpc_netconf(h, cbuf_get(cbx), &xretdis, NULL) < 0) goto done; /* log errors from discard, but ignore */ if ((xpath_first(xretdis, NULL, "//rpc-error")) != NULL) clicon_log(LOG_WARNING, "%s: discard-changes failed which may lead candidate in an inconsistent state", __FUNCTION__); if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) /* Use original xe */ goto done; goto ok; } if (xretcom){ /* Clear: can be reused again below */ xml_free(xretcom); xretcom = NULL; } if (if_feature(yspec, "ietf-netconf", "startup")){ /* RFC8040 Sec 1.4: * If the NETCONF server supports :startup, the RESTCONF server MUST * automatically update the non-volatile startup configuration * datastore, after the "running" datastore has been altered as a * consequence of a RESTCONF edit operation. */ cbuf_reset(cbx); cprintf(cbx, "", clicon_nacm_recovery_user(h)); cprintf(cbx, ""); if (clicon_rpc_netconf(h, cbuf_get(cbx), &xretcom, NULL) < 0) goto done; /* If copy-config failed, log and ignore (already committed) */ if ((xe = xpath_first(xretcom, NULL, "//rpc-error")) != NULL){ clicon_log(LOG_WARNING, "%s: copy-config running->startup failed", __FUNCTION__); } } FCGX_SetExitStatus(201, r->out); FCGX_FPrintF(r->out, "Status: 201 Created\r\n"); http_location(r, xdata); FCGX_GetParam("HTTP_ACCEPT", r->envp); FCGX_FPrintF(r->out, "\r\n"); ok: retval = 0; done: clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); if (xret) xml_free(xret); if (xerr) xml_free(xerr); if (xretcom) xml_free(xretcom); if (xretdis) xml_free(xretdis); if (xtop) xml_free(xtop); if (cbx) cbuf_free(cbx); return retval; } /* api_data_post */ /*! Handle input data to api_operations_post * @param[in] h CLIXON handle * @param[in] r Fastcgi request handle * @param[in] data Stream input data * @param[in] yspec Yang top-level specification * @param[in] yrpc Yang rpc spec * @param[in] xrpc XML pointer to rpc method * @param[in] pretty Set to 1 for pretty-printed xml/json output * @param[in] media_out Output media * @retval 1 OK * @retval 0 Fail, Error message sent * @retval -1 Fatal error, clicon_err called * * RFC8040 3.6.1 * If the "rpc" or "action" statement has an "input" section, then * instances of these input parameters are encoded in the module * namespace where the "rpc" or "action" statement is defined, in an XML * element or JSON object named "input", which is in the module * namespace where the "rpc" or "action" statement is defined. * (Any other input is assumed as error.) */ static int api_operations_post_input(clicon_handle h, FCGX_Request *r, char *data, yang_stmt *yspec, yang_stmt *yrpc, cxobj *xrpc, int pretty, restconf_media media_out) { int retval = -1; cxobj *xdata = NULL; cxobj *xerr = NULL; /* malloced must be freed */ cxobj *xe; cxobj *xinput; cxobj *x; cbuf *cbret = NULL; int ret; restconf_media media_in; clicon_debug(1, "%s %s", __FUNCTION__, data); if ((cbret = cbuf_new()) == NULL){ clicon_err(OE_UNIX, 0, "cbuf_new"); goto done; } /* Parse input data as json or xml into xml */ media_in = restconf_content_type(r); switch (media_in){ case YANG_DATA_XML: /* XXX: Here data is on the form: and has no proper yang binding * support */ if ((ret = clixon_xml_parse_string(data, YB_NONE, yspec, &xdata, &xerr)) < 0){ if (netconf_malformed_message_xml(&xerr, clicon_err_reason) < 0) goto done; if ((xe = xpath_first(xerr, NULL, "rpc-error")) == NULL){ clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto fail; } if (ret == 0){ if ((xe = xpath_first(xerr, NULL, "rpc-error")) == NULL){ clicon_debug(1, "%s F", __FUNCTION__); clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto fail; } break; case YANG_DATA_JSON: /* XXX: Here data is on the form: {"clixon-example:input":null} and has no proper yang binding * support */ if ((ret = clixon_json_parse_string(data, YB_NONE, yspec, &xdata, &xerr)) < 0){ if (netconf_malformed_message_xml(&xerr, clicon_err_reason) < 0) goto done; if ((xe = xpath_first(xerr, NULL, "rpc-error")) == NULL){ clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto fail; } if (ret == 0){ if ((xe = xpath_first(xerr, NULL, "rpc-error")) == NULL){ clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto fail; } break; default: restconf_unsupported_media(r); goto fail; break; } /* switch media_in */ clicon_debug(1, "%s F", __FUNCTION__); xml_name_set(xdata, "data"); /* Here xdata is: * ... */ #if 1 if (debug) clicon_log_xml(LOG_DEBUG, xdata, "%s xdata:", __FUNCTION__); #endif /* Validate that exactly only tag */ if ((xinput = xml_child_i_type(xdata, 0, CX_ELMNT)) == NULL || strcmp(xml_name(xinput),"input") != 0 || xml_child_nr_type(xdata, CX_ELMNT) != 1){ if (xml_child_nr_type(xdata, CX_ELMNT) == 0){ if (netconf_malformed_message_xml(&xerr, "restconf RPC does not have input statement") < 0) goto done; } else if (netconf_malformed_message_xml(&xerr, "restconf RPC has malformed input statement (multiple or not called input)") < 0) goto done; if ((xe = xpath_first(xerr, NULL, "rpc-error")) == NULL){ clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto fail; } // clicon_debug(1, "%s input validation passed", __FUNCTION__); /* Add all input under path */ x = NULL; while ((x = xml_child_i_type(xinput, 0, CX_ELMNT)) != NULL) if (xml_addsub(xrpc, x) < 0) goto done; /* Here xrpc is: 42 */ // ok: retval = 1; done: clicon_debug(1, "%s retval: %d", __FUNCTION__, retval); if (cbret) cbuf_free(cbret); if (xerr) xml_free(xerr); if (xdata) xml_free(xdata); return retval; fail: retval = 0; goto done; } /*! Handle output data to api_operations_post * @param[in] h CLIXON handle * @param[in] r Fastcgi request handle * @param[in] xret XML reply messages from backend/handler * @param[in] yspec Yang top-level specification * @param[in] youtput Yang rpc output specification * @param[in] pretty Set to 1 for pretty-printed xml/json output * @param[in] media_out Output media * @param[out] xoutputp Restconf JSON/XML output * @retval 1 OK * @retval 0 Fail, Error message sent * @retval -1 Fatal error, clicon_err called * xret should like: 0 */ static int api_operations_post_output(clicon_handle h, FCGX_Request *r, cxobj *xret, yang_stmt *yspec, yang_stmt *youtput, char *namespace, int pretty, restconf_media media_out, cxobj **xoutputp) { int retval = -1; cxobj *xoutput = NULL; cxobj *xerr = NULL; /* assumed malloced, will be freed */ cxobj *xe; /* just pointer */ cxobj *xa; /* xml attribute (xmlns) */ cxobj *x; cxobj *xok; int isempty; // clicon_debug(1, "%s", __FUNCTION__); /* Validate that exactly only tag */ if ((xoutput = xml_child_i_type(xret, 0, CX_ELMNT)) == NULL || strcmp(xml_name(xoutput),"rpc-reply") != 0 || xml_child_nr_type(xret, CX_ELMNT) != 1){ if (netconf_malformed_message_xml(&xerr, "restconf RPC does not have single input") < 0) goto done; if ((xe = xpath_first(xerr, NULL, "rpc-error")) == NULL){ clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto fail; } /* xoutput should now look: 0 */ /* 9. Translate to restconf RPC data */ xml_name_set(xoutput, "output"); /* xoutput should now look: 0 */ #if 1 if (debug) clicon_log_xml(LOG_DEBUG, xoutput, "%s xoutput:", __FUNCTION__); #endif /* Sanity check of outgoing XML * For now, skip outgoing checks. * (1) Does not handle properly * (2) Uncertain how validation errors should be logged/handled */ if (youtput != NULL){ xml_spec_set(xoutput, youtput); /* needed for xml_bind_yang */ #ifdef notyet if (xml_bind_yang(xoutput, YB_MODULE, yspec, NULL) < 0) goto done; if ((ret = xml_yang_validate_all(xoutput, &xerr)) < 0) goto done; if (ret == 1 && (ret = xml_yang_validate_add(h, xoutput, &xerr)) < 0) goto done; if (ret == 0){ /* validation failed */ if ((xe = xpath_first(xerr, NULL, "rpc-reply/rpc-error")) == NULL){ clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto fail; } #endif } /* Special case, no yang output (single - or empty body?) * RFC 7950 7.14.4 * If the RPC operation invocation succeeded and no output parameters * are returned, the contains a single element * RFC 8040 3.6.2 * If the "rpc" statement has no "output" section, the response message * MUST NOT include a message-body and MUST send a "204 No Content" * status-line instead. */ isempty = xml_child_nr_type(xoutput, CX_ELMNT) == 0 || (xml_child_nr_type(xoutput, CX_ELMNT) == 1 && (xok = xml_child_i_type(xoutput, 0, CX_ELMNT)) != NULL && strcmp(xml_name(xok),"ok")==0); if (isempty) { /* Internal error - invalid output from rpc handler */ FCGX_SetExitStatus(204, r->out); /* OK */ FCGX_FPrintF(r->out, "Status: 204 No Content\r\n"); FCGX_FPrintF(r->out, "\r\n"); goto fail; } /* Clear namespace of parameters */ x = NULL; while ((x = xml_child_each(xoutput, x, CX_ELMNT)) != NULL) { if ((xa = xml_find_type(x, NULL, "xmlns", CX_ATTR)) != NULL) if (xml_purge(xa) < 0) goto done; } /* Set namespace on output */ if (xmlns_set(xoutput, NULL, namespace) < 0) goto done; *xoutputp = xoutput; retval = 1; done: clicon_debug(1, "%s retval: %d", __FUNCTION__, retval); if (xerr) xml_free(xerr); return retval; fail: retval = 0; goto done; } /*! REST operation POST method * @param[in] h CLIXON handle * @param[in] r Fastcgi request handle * @param[in] api_path According to restconf (Sec 3.5.3.1 in rfc8040) * @param[in] qvec Vector of query string (QUERY_STRING) * @param[in] data Stream input data * @param[in] pretty Set to 1 for pretty-printed xml/json output * @param[in] media_out Output media * See RFC 8040 Sec 3.6 / 4.4.2 * @note We map post to edit-config create. * POST {+restconf}/operations/ * 1. Initialize * 2. Get rpc module and name from uri (oppath) and find yang spec * 3. Build xml tree with user and rpc: * 4. Parse input data (arguments): * JSON: {"example:input":{"x":0}} * XML: 0 * 5. Translate input args to Netconf RPC, add to xml tree: * 42 * 6. Validate outgoing RPC and fill in default values * 4299 * 7. Send to RPC handler, either local or backend * 8. Receive reply from local/backend handler as Netconf RPC * 0 * 9. Translate to restconf RPC data: * JSON: {"example:output":{"x":0}} * XML: 0 * 10. Validate and send reply to originator */ int api_operations_post(clicon_handle h, FCGX_Request *r, char *api_path, int pi, cvec *qvec, char *data, int pretty, restconf_media media_out) { int retval = -1; int i; char *oppath = api_path; yang_stmt *yspec; yang_stmt *youtput = NULL; yang_stmt *yrpc = NULL; cxobj *xret = NULL; cxobj *xerr = NULL; /* malloced must be freed */ cxobj *xtop = NULL; /* xpath root */ cxobj *xbot = NULL; yang_stmt *y = NULL; cxobj *xoutput = NULL; cxobj *xa; cxobj *xe; char *username; cbuf *cbret = NULL; int ret = 0; char *prefix = NULL; char *id = NULL; yang_stmt *ys = NULL; char *namespace = NULL; clicon_debug(1, "%s json:\"%s\" path:\"%s\"", __FUNCTION__, data, api_path); /* 1. Initialize */ if ((yspec = clicon_dbspec_yang(h)) == NULL){ clicon_err(OE_FATAL, 0, "No DB_SPEC"); goto done; } if ((cbret = cbuf_new()) == NULL){ clicon_err(OE_UNIX, 0, "cbuf_new"); goto done; } for (i=0; i * * The field identifies the module name and rpc identifier * string for the desired operation. */ if (nodeid_split(oppath+1, &prefix, &id) < 0) /* +1 skip / */ goto done; if ((ys = yang_find(yspec, Y_MODULE, prefix)) == NULL){ if (netconf_operation_failed_xml(&xerr, "protocol", "yang module not found") < 0) goto done; if ((xe = xpath_first(xerr, NULL, "rpc-error")) == NULL){ clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } if ((yrpc = yang_find(ys, Y_RPC, id)) == NULL){ if (netconf_missing_element_xml(&xerr, "application", id, "RPC not defined") < 0) goto done; if ((xe = xpath_first(xerr, NULL, "rpc-error")) == NULL){ clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } /* 3. Build xml tree with user and rpc: * */ if ((xtop = xml_new("rpc", NULL, CX_ELMNT)) == NULL) goto done; xbot = xtop; /* Here xtop is: */ if ((username = clicon_username_get(h)) != NULL){ if ((xa = xml_new("username", xtop, CX_ATTR)) == NULL) goto done; if (xml_value_set(xa, username) < 0) goto done; /* Here xtop is: */ } if ((ret = api_path2xml(oppath, yspec, xtop, YC_SCHEMANODE, 1, &xbot, &y, &xerr)) < 0) goto done; if (ret == 0){ /* validation failed */ if ((xe = xpath_first(xerr, NULL, "rpc-error")) == NULL){ clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto done; } if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } /* Here xtop is: * xbot is * 4. Parse input data (arguments): * JSON: {"example:input":{"x":0}} * XML: 0 */ namespace = xml_find_type_value(xbot, NULL, "xmlns", CX_ATTR); clicon_debug(1, "%s : 4. Parse input data: %s", __FUNCTION__, data); if (data && strlen(data)){ if ((ret = api_operations_post_input(h, r, data, yspec, yrpc, xbot, pretty, media_out)) < 0) goto done; if (ret == 0) goto ok; } /* Here xtop is: 42 */ #if 1 if (debug) clicon_log_xml(LOG_DEBUG, xtop, "%s 5. Translate input args:", __FUNCTION__); #endif /* 6. Validate outgoing RPC and fill in defaults */ if (xml_bind_yang_rpc(xtop, yspec, NULL) < 0) /* */ goto done; if ((ret = xml_yang_validate_rpc(h, xtop, &xret)) < 0) goto done; if (ret == 0){ if ((xe = xpath_first(xret, NULL, "rpc-error")) == NULL){ clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); goto ok; } if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } /* Here xtop is (default values): * 4299 */ #if 0 if (debug) clicon_log_xml(LOG_DEBUG, xtop, "%s 6. Validate and defaults:", __FUNCTION__); #endif /* 7. Send to RPC handler, either local or backend * Note (1) xtop is xbot is * (2) local handler wants and backend wants */ /* Look for local (client-side) restconf plugins. * -1:Error, 0:OK local, 1:OK backend */ if ((ret = rpc_callback_call(h, xbot, cbret, r)) < 0) goto done; if (ret > 0){ /* Handled locally */ if (clixon_xml_parse_string(cbuf_get(cbret), YB_NONE, NULL, &xret, NULL) < 0) goto done; /* Local error: return it and quit */ if ((xe = xpath_first(xret, NULL, "rpc-reply/rpc-error")) != NULL){ if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } } else { /* Send to backend */ if (clicon_rpc_netconf_xml(h, xtop, &xret, NULL) < 0) goto done; if ((xe = xpath_first(xret, NULL, "rpc-reply/rpc-error")) != NULL){ if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) goto done; goto ok; } } /* 8. Receive reply from local/backend handler as Netconf RPC * 0 */ #if 1 if (debug) clicon_log_xml(LOG_DEBUG, xret, "%s Receive reply:", __FUNCTION__); #endif youtput = yang_find(yrpc, Y_OUTPUT, NULL); if ((ret = api_operations_post_output(h, r, xret, yspec, youtput, namespace, pretty, media_out, &xoutput)) < 0) goto done; if (ret == 0) goto ok; /* xoutput should now look: 0 */ FCGX_SetExitStatus(200, r->out); /* OK */ FCGX_FPrintF(r->out, "Content-Type: %s\r\n", restconf_media_int2str(media_out)); FCGX_FPrintF(r->out, "\r\n"); cbuf_reset(cbret); switch (media_out){ case YANG_DATA_XML: if (clicon_xml2cbuf(cbret, xoutput, 0, pretty, -1) < 0) goto done; /* xoutput should now look: 0 */ break; case YANG_DATA_JSON: if (xml2json_cbuf(cbret, xoutput, pretty) < 0) goto done; /* xoutput should now look: {"example:output": {"x":0,"y":42}} */ break; default: break; } FCGX_FPrintF(r->out, "%s", cbuf_get(cbret)); FCGX_FPrintF(r->out, "\r\n\r\n"); ok: retval = 0; done: clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); if (prefix) free(prefix); if (id) free(id); if (xtop) xml_free(xtop); if (xret) xml_free(xret); if (xerr) xml_free(xerr); if (cbret) cbuf_free(cbret); return retval; }