/* * ***** BEGIN LICENSE BLOCK ***** Copyright (C) 2009-2019 Olof Hagsand Copyright (C) 2020-2022 Olof Hagsand and Rubicon Communications, LLC(Netgate) 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 ***** * @see https://nginx.org/en/docs/http/ngx_http_core_module.html#var_https * @note The response payload for errors uses text_html. RFC7231 is vague * on the response payload (and its media). Maybe it should be omitted * altogether? */ #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 #include #include #include /* cligen */ #include /* clixon */ #include #include "restconf_api.h" #include "restconf_lib.h" #include "restconf_err.h" #include "restconf_handle.h" /* +----------------------------+--------------------------------------+ | 100 Continue | POST accepted, 201 should follow | | 200 OK | Success with response message-body | | 201 Created | POST to create a resource success | | 204 No Content | Success without response message- | | | body | | 304 Not Modified | Conditional operation not done | | 400 Bad Request | Invalid request message | | 401 Unauthorized | Client cannot be authenticated | | 403 Forbidden | Access to resource denied | | 404 Not Found | Resource target or resource node not | | | found | | 405 Method Not Allowed | Method not allowed for target | | | resource | | 409 Conflict | Resource or lock in use | | 412 Precondition Failed | Conditional method is false | | 413 Request Entity Too | too-big error | | Large | | | 414 Request-URI Too Large | too-big error | | 415 Unsupported Media Type | non RESTCONF media type | | 500 Internal Server Error | operation-failed | | 501 Not Implemented | unknown-operation | | 503 Service Unavailable | Recoverable server error | +----------------------------+--------------------------------------+ Mapping netconf error-tag -> status code +-------------------------+-------------+ | | status code | +-------------------------+-------------+ | in-use | 409 | | invalid-value | 400 | | too-big | 413 | | missing-attribute | 400 | | bad-attribute | 400 | | unknown-attribute | 400 | | bad-element | 400 | | unknown-element | 400 | | unknown-namespace | 400 | | access-denied | 403 | | lock-denied | 409 | | resource-denied | 409 | | rollback-failed | 500 | | data-exists | 409 | | data-missing | 409 | | operation-not-supported | 405 or 501 | | operation-failed | 500 | | partial-operation | 500 | | malformed-message | 400 | +-------------------------+-------------+ * See RFC 8040 Section 7: Mapping from NETCONF to Status Code * and RFC 6241 Appendix A. NETCONF Error list */ static const map_str2int netconf_restconf_map[] = { {"in-use", 409}, {"invalid-value", 400}, /* or 404 special case if msg is: "Invalid HTTP data method" handled in api_return_err */ {"invalid-value", 404}, {"invalid-value", 406}, {"too-big", 413}, /* request */ {"too-big", 400}, /* response */ {"missing-attribute", 400}, {"bad-attribute", 400}, {"unknown-attribute", 400}, {"missing-element", 400}, {"bad-element", 400}, {"unknown-element", 400}, {"unknown-namespace", 400}, {"access-denied", 403}, /* or 401 special case if msg is: "The requested URL was unauthorized" handled in api_return_err */ {"access-denied", 401}, {"lock-denied", 409}, {"resource-denied", 409}, {"rollback-failed", 500}, {"data-exists", 409}, {"data-missing", 409}, {"operation-not-supported",405}, {"operation-not-supported",501}, {"operation-failed", 412}, {"operation-failed", 500}, {"partial-operation", 500}, {"malformed-message", 400}, {NULL, -1} }; /* See 7231 Section 6.1 */ static const map_str2int http_reason_phrase_map[] = { {"Continue", 100}, {"Switching Protocols", 101}, {"OK", 200}, {"Created", 201}, {"Accepted", 202}, {"Non-Authoritative Information", 203}, {"No Content", 204}, {"Reset Content", 205}, {"Partial Content", 206}, {"Multiple Choices", 300}, {"Moved Permanently", 301}, {"Found", 302}, {"See Other", 303}, {"Not Modified", 304}, {"Use Proxy", 305}, {"Temporary Redirect", 307}, {"Bad Request", 400}, {"Unauthorized", 401}, {"Payment Required", 402}, {"Forbidden", 403}, {"Not Found", 404}, {"Method Not Allowed", 405}, {"Not Acceptable", 406}, {"Proxy Authentication Required", 407}, {"Request Timeout", 408}, {"Conflict", 409}, {"Gone", 410}, {"Length Required", 411}, {"Precondition Failed", 412}, {"Payload Too Large", 413}, {"URI Too Long", 414}, {"Unsupported Media Type", 415}, {"Range Not Satisfiable", 416}, {"Expectation Failed", 417}, {"Upgrade Required", 426}, {"Internal Server Error", 500}, {"Not Implemented", 501}, {"Bad Gateway", 502}, {"Service Unavailable", 503}, {"Gateway Timeout", 504}, {"HTTP Version Not Supported", 505}, {NULL, -1} }; /* See RFC 8040 * @see restconf_media_str2int */ static const map_str2int http_media_map[] = { {"application/yang-data+xml", YANG_DATA_XML}, {"application/yang-data+json", YANG_DATA_JSON}, {"application/yang-patch+xml", YANG_PATCH_XML}, {"application/yang-patch+json", YANG_PATCH_JSON}, {"application/yang-data+xml-list", YANG_PAGINATION_XML}, /* draft-wwlh-netconf-list-pagination-rc-02 */ {NULL, -1} }; /* Mapping to http proto types */ static const map_str2int http_proto_map[] = { {"http/1.0", HTTP_10}, {"http/1.1", HTTP_11}, {"http/2", HTTP_2}, {NULL, -1} }; int restconf_err2code(char *tag) { return clicon_str2int(netconf_restconf_map, tag); } const char * restconf_code2reason(int code) { return clicon_int2str(http_reason_phrase_map, code); } const restconf_media restconf_media_str2int(char *media) { return clicon_str2int(http_media_map, media); } const char * restconf_media_int2str(restconf_media media) { return clicon_int2str(http_media_map, media); } int restconf_str2proto(char *str) { return clicon_str2int(http_proto_map, str); } const char * restconf_proto2str(int proto) { return clicon_int2str(http_proto_map, proto); } /*! Return media_in from Content-Type, -1 if not found or unrecognized * * @note media-type syntax does not support parameters * @see RFC7231 Sec 3.1.1.1 for media-type syntax type: * media-type = type "/" subtype *( OWS ";" OWS parameter ) * type = token * subtype = token * */ restconf_media restconf_content_type(clixon_handle h) { char *str = NULL; restconf_media m; if ((str = restconf_param_get(h, "HTTP_CONTENT_TYPE")) == NULL) return -1; if ((int)(m = restconf_media_str2int(str)) == -1) return -1; return m; } /*! Translate http header by capitalizing, prepend w HTTP_ and - -> _ * * Example: Host -> HTTP_HOST */ int restconf_convert_hdr(clixon_handle h, char *name, char *val) { int retval = -1; cbuf *cb = NULL; int i; char c; size_t len; if ((cb = cbuf_new()) == NULL){ clixon_err(OE_UNIX, errno, "cbuf_new"); goto done; } /* convert key name */ cprintf(cb, "HTTP_"); len = strlen(name); for (i=0; i []*/ if ((p = rindex(xpath,'/')) == NULL) p = xpath; p = index(p, '['); cprintf(cb, "%s", p); } else{ /* LEAF_LIST */ /* translate /../x[.='x'] --> x */ if ((p = rindex(xpath,'\'')) == NULL){ clixon_err(OE_YANG, 0, "Translated api->xpath %s->%s not on leaf-list canonical form: ../[.='x']", pstr, xpath); goto done; } *p = '\0'; if ((p = rindex(xpath,'\'')) == NULL){ clixon_err(OE_YANG, 0, "Translated api->xpath %s->%s not on leaf-list canonical form: ../[.='x']", pstr, xpath); goto done; } p++; cprintf(cb, "%s", p); } if (xml_add_attr(xdata, attrname, cbuf_get(cb), "yang", NULL) == NULL) goto done; } /* Add prefix/namespaces used in attributes */ cv = NULL; while ((cv = cvec_each(nsc, cv)) != NULL){ char *ns = cv_string_get(cv); if (xmlns_set(xdata, cv_name_get(cv), ns) < 0) goto done; } if (nsc) xml_sort(xdata); /* Ensure attr is first */ cprintf(cb, "/>"); retval = 0; done: if (xpath) free(xpath); if (nsc) xml_nsctx_free(nsc); if (cb) cbuf_free(cb); return retval; } /*! Callback for yang extensions ietf-restconf:yang-data * * @param[in] h Clixon handle * @param[in] yext Yang node of extension * @param[in] ys Yang node of (unknown) statement belonging to extension * @retval 0 OK, all callbacks executed OK * @retval -1 Error in one callback * @note This extension adds semantics to YANG according to RFC8040 as follows: * - The list-stmt is not required to have a key-stmt defined.(NB!!) * - The if-feature-stmt is ignored if present. * - The config-stmt is ignored if present. * - The available identity values for any 'identityref' * leaf or leaf-list nodes are limited to the module containing this extension statement and * the modules imported into that module. * @see ietf-restconf.yang */ int restconf_main_extension_cb(clixon_handle h, yang_stmt *yext, yang_stmt *ys) { int retval = -1; char *extname; char *modname; yang_stmt *ymod; yang_stmt *yc; yang_stmt *yn = NULL; ymod = ys_module(yext); modname = yang_argument_get(ymod); extname = yang_argument_get(yext); if (strcmp(modname, "ietf-restconf") != 0 || strcmp(extname, "yang-data") != 0) goto ok; clixon_debug(CLIXON_DBG_DEFAULT, "%s Enabled extension:%s:%s", __FUNCTION__, modname, extname); if ((yc = yang_find(ys, 0, NULL)) == NULL) goto ok; if ((yn = ys_dup(yc)) == NULL) goto done; /* yang-data extension: The list-stmt is not required to have a key-stmt defined. */ yang_flag_set(yn, YANG_FLAG_NOKEY); if (yn_insert(yang_parent_get(ys), yn) < 0) goto done; ok: retval = 0; done: return retval; } /*! Extract uri-encoded uri-path without arguments * * Use REQUEST_URI parameter and strip ?args * eg /interface=eth%2f0%2f0?insert=first -> /interface=eth%2f0%2f0 * @retval path malloced, need free */ char * restconf_uripath(clixon_handle h) { char *path = NULL; char *path2 = NULL; char *q; if ((path = restconf_param_get(h, "REQUEST_URI")) == NULL){ clixon_err(OE_RESTCONF, 0, "No REQUEST_URI"); return NULL; } if ((path2 = strdup(path)) == NULL){ clixon_err(OE_UNIX, errno, "strdup"); return NULL; } if ((q = index(path2, '?')) != NULL) *q = '\0'; return path2; } /*! Drop privileges from root to user (or already at user) * * @param[in] h Clixon handle * @retval 0 OK * @retval -1 Error * Group set to CLICON_SOCK_GROUP to communicate with backend */ int restconf_drop_privileges(clixon_handle h) { int retval = -1; uid_t newuid = -1; uid_t uid; char *group; gid_t gid = -1; char *user; enum priv_mode_t priv_mode = PM_NONE; clixon_debug(CLIXON_DBG_DEFAULT, "%s", __FUNCTION__); /* Sanity check: backend group exists */ if ((group = clicon_sock_group(h)) == NULL){ clixon_err(OE_FATAL, 0, "clicon_sock_group option not set"); return -1; } if (group_name2gid(group, &gid) < 0){ clixon_log(h, LOG_ERR, "'%s' does not seem to be a valid user group." /* \n required here due to multi-line log */ "The config daemon requires a valid group to create a server UNIX socket\n" "Define a valid CLICON_SOCK_GROUP in %s or via the -g option\n" "or create the group and add the user to it. Check documentation for how to do this on your platform", group, clicon_configfile(h)); goto done; } /* Get privileges mode (for dropping privileges) */ if ((priv_mode = clicon_restconf_privileges_mode(h)) == PM_NONE) goto ok; if ((user = clicon_option_str(h, "CLICON_RESTCONF_USER")) == NULL) goto ok; /* Get (wanted) new www user id */ if (name2uid(user, &newuid) < 0){ clixon_err(OE_DAEMON, errno, "'%s' is not a valid user .\n", user); goto done; } /* get current userid, if already at this level OK */ if ((uid = getuid()) == newuid) goto ok; if (uid != 0){ clixon_err(OE_DAEMON, EPERM, "Privileges can only be dropped from root user (uid is %u)\n", uid); goto done; } if (setgid(gid) == -1) { clixon_err(OE_DAEMON, errno, "setgid %d", gid); goto done; } switch (priv_mode){ case PM_DROP_PERM: if (drop_priv_perm(newuid) < 0) goto done; /* Verify you cannot regain root privileges */ if (setuid(0) != -1){ clixon_err(OE_DAEMON, EPERM, "Could regain root privilieges"); goto done; } break; case PM_DROP_TEMP: if (drop_priv_temp(newuid) < 0) goto done; break; case PM_NONE: break; /* catched above */ } clixon_debug(CLIXON_DBG_DEFAULT, "%s dropped privileges from root to %s(%d)", __FUNCTION__, user, newuid); ok: retval = 0; done: return retval; } /*! restconf auth cb * * @param[in] h Clixon handle * @param[in] req Generic Www handle (can be part of clixon handle) * @param[in] pretty Pretty-print * @param[in] media_out Restconf output media * @retval 1 Authenticated * @retval 0 Not authenticated * @retval -1 Error */ int restconf_authentication_cb(clixon_handle h, void *req, int pretty, restconf_media media_out) { int retval = -1; clixon_auth_type_t auth_type; int authenticated; int ret; char *username = NULL; /* Assume malloced if set */ cxobj *xret = NULL; cxobj *xerr; char *anonymous = NULL; auth_type = restconf_auth_type_get(h); clixon_debug(CLIXON_DBG_DEFAULT, "%s auth-type:%s", __FUNCTION__, clixon_auth_type_int2str(auth_type)); ret = 0; authenticated = 0; /* ret: -1 Error, 0: Ignore/not handled, 1: OK see authenticated parameter */ if ((ret = clixon_plugin_auth_all(h, req, auth_type, &username)) < 0) goto done; if (ret == 1){ /* OK, tag username to handle */ if (username != NULL){ authenticated = 1; clicon_username_set(h, username); } } else { /* Default behaviour */ switch (auth_type){ case CLIXON_AUTH_NONE: /* if not handled by callback, use anonymous user */ if ((anonymous = clicon_option_str(h, "CLICON_ANONYMOUS_USER")) == NULL){ break; /* not authenticated */ } clicon_username_set(h, anonymous); authenticated = 1; break; case CLIXON_AUTH_CLIENT_CERTIFICATE: { char *cn; /* Check for cert subject common name (CN) */ if ((cn = restconf_param_get(h, "SSL_CN")) != NULL){ clicon_username_set(h, cn); authenticated = 1; } break; } case CLIXON_AUTH_USER: authenticated = 0; break; } } if (authenticated == 0){ /* Message is not authenticated (401 returned) */ if (netconf_access_denied_xml(&xret, "protocol", "The requested URL was unauthorized") < 0) goto done; if ((xerr = xpath_first(xret, NULL, "//rpc-error")) != NULL){ if (api_return_err(h, req, xerr, pretty, media_out, 0) < 0) goto done; goto notauth; } retval = 0; goto notauth; } /* If set but no user, set a dummy user */ retval = 1; done: clixon_debug(CLIXON_DBG_DEFAULT, "%s retval:%d authenticated:%d user:%s", __FUNCTION__, retval, authenticated, clicon_username_get(h)); if (username) free(username); if (xret) xml_free(xret); return retval; notauth: retval = 0; goto done; } /*! Basic config init, set auth-type, pretty, etc * * @param[in] h Clixon handle * @param[in] xrestconf XML config containing clixon-restconf top-level * @retval 1 OK * @retval 0 Restconf is disable * @retval -1 Error */ int restconf_config_init(clixon_handle h, cxobj *xrestconf) { int retval = -1; char *enable; cxobj *x; char *bstr; cvec *nsc = NULL; int auth_type; yang_stmt *yspec; yang_stmt *y; if ((yspec = clicon_dbspec_yang(h)) == NULL){ clixon_err(OE_FATAL, 0, "No DB_SPEC"); goto done; } /* Apply default values (removed in clear function) */ if (xml_default_recurse(xrestconf, 0) < 0) goto done; if ((x = xpath_first(xrestconf, nsc, "enable")) != NULL && (enable = xml_body(x)) != NULL){ if (strcmp(enable, "false") == 0){ clixon_debug(CLIXON_DBG_DEFAULT, "%s restconf disabled", __FUNCTION__); goto disable; } } /* get common fields */ if ((x = xpath_first(xrestconf, nsc, "auth-type")) != NULL && (bstr = xml_body(x)) != NULL){ if ((auth_type = clixon_auth_type_str2int(bstr)) < 0){ clixon_err(OE_CFG, EFAULT, "Invalid restconf auth-type: %s", bstr); goto done; } restconf_auth_type_set(h, auth_type); } if ((x = xpath_first(xrestconf, nsc, "pretty")) != NULL && (bstr = xml_body(x)) != NULL){ if (strcmp(bstr, "true") == 0) restconf_pretty_set(h, 1); else if (strcmp(bstr, "false") == 0) restconf_pretty_set(h, 0); } /* Check if enable-http-data is true and that feature is enabled * It is protected by if-feature http-data, which means if the feature is not enabled, its * YANG spec will exist but by ANYDATA */ if ((x = xpath_first(xrestconf, nsc, "enable-http-data")) != NULL && (y = xml_spec(x)) != NULL && yang_keyword_get(y) != Y_ANYDATA && (bstr = xml_body(x)) != NULL && strcmp(bstr, "true") == 0) { restconf_http_data_set(h, 1); } else restconf_http_data_set(h, 0); /* Check if fcgi-socket is true and that feature is enabled * It is protected by if-feature fcgi, which means if the feature is not enabled, then * YANG spec will exist but by ANYDATA */ if ((x = xpath_first(xrestconf, nsc, "fcgi-socket")) != NULL && (y = xml_spec(x)) != NULL && yang_keyword_get(y) != Y_ANYDATA && (bstr = xml_body(x)) != NULL){ if (restconf_fcgi_socket_set(h, bstr) < 0) goto done; } retval = 1; done: return retval; disable: retval = 0; goto done; } /*! Create and bind restconf socket * * @param[in] netns0 Network namespace, special value "default" is same as NULL * @param[in] addrstr Address as string, eg "0.0.0.0", "::" * @param[in] addrtype One of inet:ipv4-address or inet:ipv6-address * @param[in] port TCP port * @param[in] backlog Listen backlog, queie of pending connections * @param[in] flags Socket flags OR:ed in with the socket(2) type parameter * @param[out] ss Server socket (bound for accept) * @retval 0 OK * @retval -1 Error */ int restconf_socket_init(const char *netns0, const char *addrstr, const char *addrtype, uint16_t port, int backlog, int flags, int *ss) { int retval = -1; struct sockaddr_in6 sin6 = {0,}; // because its larger than sin and sa struct sockaddr *sa = (struct sockaddr *)&sin6; size_t sa_len; const char *netns; clixon_debug(CLIXON_DBG_DEFAULT, "%s %s %s %s %hu", __FUNCTION__, netns0, addrtype, addrstr, port); /* netns default -> NULL */ if (netns0 != NULL && strcmp(netns0, RESTCONF_NETNS_DEFAULT)==0) netns = NULL; else netns = netns0; if (clixon_inet2sin(addrtype, addrstr, port, sa, &sa_len) < 0) goto done; if (clixon_netns_socket(netns, sa, sa_len, backlog, flags, addrstr, ss) < 0) goto done; clixon_debug(CLIXON_DBG_DEFAULT, "%s ss=%d", __FUNCTION__, *ss); retval = 0; done: clixon_debug(CLIXON_DBG_DEFAULT, "%s %d", __FUNCTION__, retval); return retval; }