From 6eb18da5e90a587b25f7dc265cd82ed123aae327 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Sun, 15 Nov 2020 12:33:20 +0100 Subject: [PATCH] * Multi-socket feature (eg IPv4/IPv6 http/https) to restconf evhtp * Added by-ref parameter to `ys_cv_validate()` returning which sub-yang spec was validated in a union. --- CHANGELOG.md | 2 + apps/backend/backend_socket.c | 2 +- apps/restconf/restconf_main_evhtp.c | 568 +++++++++++++------- docker/main/Makefile.in | 4 + lib/clixon/clixon_yang_type.h | 2 +- lib/src/clixon_validate.c | 2 +- lib/src/clixon_yang_type.c | 35 +- test/certs.sh | 67 +++ test/lib.sh | 5 +- test/test_restconf.sh | 218 +++++--- test/test_restconf2.sh | 2 + test/test_ssl_certs.sh | 101 +--- yang/clixon/clixon-restconf@2020-10-30.yang | 57 +- 13 files changed, 683 insertions(+), 382 deletions(-) create mode 100644 test/certs.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index ff87fc5d..19c92ed2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ ### New features +* Initial NBMA functionality (thanks: @benavrhm): "ds" resource * Restconf configuration has a new configure model: `clixon-restconf.yang` enabling restconf daemon configuration from datastore instead of from config file. * Restconf config data, such as addresses, authentication type, etc, is read from the backend datastore instead of the clixon-config file on startup. * This is enabled by setting `CLIXON_RESTCONF_CONFIG` to true (or start clixon-restconf with `-b`), in which case restconf data can be set in the datastore. @@ -48,6 +49,7 @@ Users may have to change how they access the system Developers may need to change their code +* Added by-ref parameter to `ys_cv_validate()` returning which sub-yang spec was validated in a union. * Changed first parameter from `int fd` to `FILE *f` in the following functions: * clixon_xml_parse_file(), clixon_json_parse_file(), yang_parse_file() * See [Bytewise read() of files is slow #146](https://github.com/clicon/clixon/issues/146) diff --git a/apps/backend/backend_socket.c b/apps/backend/backend_socket.c index 75837e61..3fbd1dee 100644 --- a/apps/backend/backend_socket.c +++ b/apps/backend/backend_socket.c @@ -75,7 +75,7 @@ /*! Open an INET stream socket and bind it to a file descriptor * -o * @param[in] h Clicon handle + * @param[in] h Clicon handle * @param[in] dst IPv4 address (see inet_pton(3)) * @retval s Socket file descriptor (see socket(2)) * @retval -1 Error diff --git a/apps/restconf/restconf_main_evhtp.c b/apps/restconf/restconf_main_evhtp.c index fab24396..27077497 100644 --- a/apps/restconf/restconf_main_evhtp.c +++ b/apps/restconf/restconf_main_evhtp.c @@ -60,6 +60,9 @@ #include #include #include /* chmod */ +#include +#include +#include /* evhtp */ #include @@ -88,8 +91,9 @@ /* clixon evhtp handle */ typedef struct { - evhtp_t *eh_htp; - struct event_base *eh_evbase; + evhtp_t **eh_htpvec; /* One per socket */ + int eh_htplen; /* Number of sockets */ + struct event_base *eh_evbase; /* Change to list */ evhtp_ssl_cfg_t *eh_ssl_config; } cx_evhtp_handle; @@ -105,12 +109,16 @@ static void evhtp_terminate(cx_evhtp_handle *eh) { evhtp_ssl_cfg_t *sc; + int i; if (eh == NULL) return; - if (eh->eh_htp){ - evhtp_unbind_socket(eh->eh_htp); - evhtp_free(eh->eh_htp); + if (eh->eh_htpvec){ + for (i=0; ieh_htplen; i++){ + evhtp_unbind_socket(eh->eh_htpvec[i]); + evhtp_free(eh->eh_htpvec[i]); + } + free(eh->eh_htpvec); } if (eh->eh_evbase) event_base_free(eh->eh_evbase); @@ -554,9 +562,9 @@ cx_get_ssl_server_certs(clicon_handle h, * @retval -1 Error */ static int -cx_get_ssl_client_certs(clicon_handle h, - const char *server_ca_cert_path, - evhtp_ssl_cfg_t *ssl_config) +cx_get_ssl_client_ca_certs(clicon_handle h, + const char *server_ca_cert_path, + evhtp_ssl_cfg_t *ssl_config) { int retval = -1; struct stat f_stat; @@ -643,7 +651,7 @@ static int cx_verify_certs(int pre_verify, evhtp_x509_store_ctx_t *store) { -#ifdef NOTYET +#if 0 //def NOTYET char buf[256]; X509 * err_cert; int err; @@ -667,6 +675,88 @@ cx_verify_certs(int pre_verify, return pre_verify; } +/*! + * + * @param[out] addr Address as string, eg "0.0.0.0", "::" + * @param[in] addrtype One of inet:ipv4-address or inet:ipv6-address + * @param[out] ss Server socket (bound for accept) + */ +static int +restconf_socket_init(clicon_handle h, + const char *addr, + const char *addrtype, + uint16_t port, + int *ss) +{ + int retval = -1; + int s = -1; + struct sockaddr * sa; + struct sockaddr_in6 sin6 = { 0 }; + struct sockaddr_in sin = { 0 }; + size_t sin_len; + int on = 1; + + if (strcmp(addrtype, "inet:ipv6-address") == 0) { + sin_len = sizeof(struct sockaddr_in6); + sin6.sin6_port = htons(port); + sin6.sin6_family = AF_INET6; + + evutil_inet_pton(AF_INET6, addr, &sin6.sin6_addr); + sa = (struct sockaddr *)&sin6; + } + else if (strcmp(addrtype, "inet:ipv4-address") == 0) { + sin_len = sizeof(struct sockaddr_in); + sin.sin_family = AF_INET; + sin.sin_port = htons(port); + sin.sin_addr.s_addr = inet_addr(addr); + + sa = (struct sockaddr *)&sin; + } + else{ + clicon_err(OE_XML, EINVAL, "Unexpected addrtype: %s", addrtype); + return -1; + } + /* create inet socket */ + if ((s = socket(sa->sa_family, SOCK_STREAM, 0)) < 0) { + clicon_err(OE_UNIX, errno, "socket"); + goto done; + } + // evutil_make_socket_closeonexec(s); // XXX + // evutil_make_socket_nonblocking(s); // XXX + + if (setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, (void *)&on, sizeof(on)) == -1) { + clicon_err(OE_UNIX, errno, "setsockopt SO_KEEPALIVE"); + goto done; + } + if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (void *)&on, sizeof(on)) == -1) { + clicon_err(OE_UNIX, errno, "setsockopt SO_REUSEADDR"); + goto done; + } + /* only bind ipv6, otherwise it may bind to ipv4 as well which is strange but seems default */ + if (sa->sa_family == AF_INET6 && + setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) == -1) { + clicon_err(OE_UNIX, errno, "setsockopt IPPROTO_IPV6"); + goto done; + } + if (bind(s, sa, sin_len) == -1) { + clicon_err(OE_UNIX, errno, "bind port %u", port); + goto done; + } + if (listen(s, SOCKET_LISTEN_BACKLOG) < 0){ + clicon_err(OE_UNIX, errno, "listen"); + goto done; + } + if (ss) + *ss = s; + retval = 0; + done: + if (retval != 0 && s != -1) + evutil_closesocket(s); + return retval; + // return evhtp_bind_sockaddr(htp, sa, sin_len, SOCKET_LISTEN_BACKLOG); +} + + /*! Usage help routine * @param[in] argv0 command line * @param[in] h Clicon handle @@ -701,31 +791,35 @@ usage(clicon_handle h, exit(0); } -/*! Main routine for libevhtp restconf - */ -/*! Phase 2 of start per-socket config +/*! Extract socket info from backend config * @param[in] h Clicon handle * @param[in] xs socket config * @param[in] nsc Namespace context * @param[out] namespace - * @param[out] address + * @param[out] address Address as string, eg "0.0.0.0", "::" + * @param[out] addrtype One of inet:ipv4-address or inet:ipv6-address * @param[out] port * @param[out] ssl */ static int -cx_evhtp_socket(clicon_handle h, - cxobj *xs, - cvec *nsc, - char **namespace, - char **address, - uint16_t *port, - uint16_t *ssl) +cx_evhtp_socket_extract(clicon_handle h, + cxobj *xs, + cvec *nsc, + char **namespace, + char **address, + char **addrtype, + uint16_t *port, + uint16_t *ssl) { - int retval = -1; - cxobj *x; - char *str = NULL; - char *reason = NULL; - int ret; + int retval = -1; + cxobj *x; + char *str = NULL; + char *reason = NULL; + int ret; + char *body; + cg_var *cv = NULL; + yang_stmt *y; + yang_stmt *ysub = NULL; if ((x = xpath_first(xs, nsc, "namespace")) == NULL){ clicon_err(OE_XML, EINVAL, "Mandatory namespace not given"); @@ -736,7 +830,35 @@ cx_evhtp_socket(clicon_handle h, clicon_err(OE_XML, EINVAL, "Mandatory address not given"); goto done; } - *address = xml_body(x); + /* address is a union type and needs a special investigation to see which type (ipv4 or ipv6) + * the address is + */ + body = xml_body(x); + y = xml_spec(x); + if ((cv = cv_dup(yang_cv_get(y))) == NULL){ + clicon_err(OE_UNIX, errno, "cv_dup"); + goto done; + } + if ((ret = cv_parse1(body, cv, &reason)) < 0){ + clicon_err(OE_XML, errno, "cv_parse1"); + goto done; + } + if (ret == 0){ + clicon_err(OE_XML, EFAULT, "%s", reason); + goto done; + } + if ((ret = ys_cv_validate(h, cv, y, &ysub, &reason)) < 0) + goto done; + if (ret == 0){ + clicon_err(OE_XML, EFAULT, "Validation os address: %s", reason); + goto done; + } + if (ysub == NULL){ + clicon_err(OE_XML, EFAULT, "No address union type"); + goto done; + } + *address = body; + *addrtype = yang_argument_get(ysub); if ((x = xpath_first(xs, nsc, "port")) != NULL && (str = xml_body(x)) != NULL){ if ((ret = parse_uint16(str, port, &reason)) < 0){ @@ -767,7 +889,110 @@ cx_evhtp_socket(clicon_handle h, return retval; } -/*! Phase 2 of evhtp init, config has been retrieved from backend +static int +cx_htp_add(cx_evhtp_handle *eh, + evhtp_t *htp) +{ + eh->eh_htplen++; + if ((eh->eh_htpvec = realloc(eh->eh_htpvec, eh->eh_htplen*sizeof(htp))) == NULL){ + clicon_err(OE_UNIX, errno, "realloc"); + return -1; + } + eh->eh_htpvec[eh->eh_htplen-1] = htp; + return 0; +} + +/*! Phase 2 of backend evhtp init, config single socket + * @param[in] h Clicon handle + * @param[in] eh Evhtp handle + * @param[in] ssl_enable Server is SSL enabled + * @param[in] xs XML config of single restconf socket + * @param[in] nsc Namespace context + */ +static int +cx_evhtp_socket(clicon_handle h, + cx_evhtp_handle *eh, + int ssl_enable, + cxobj *xs, + cvec *nsc, + char *server_cert_path, + char *server_key_path, + char *server_ca_cert_path, + int auth_type_client_certificate) +{ + int retval = -1; + char *namespace = NULL; + char *address = NULL; + char *addrtype = NULL; + uint16_t ssl = 0; + uint16_t port = 0; + int ss; + evhtp_t *htp = NULL; + + /* This is socket create a new evhtp_t instance */ + if ((htp = evhtp_new(eh->eh_evbase, NULL)) == NULL){ + clicon_err(OE_UNIX, errno, "evhtp_new"); + goto done; + } + +#ifndef EVHTP_DISABLE_EVTHR /* threads */ + evhtp_use_threads_wexit(htp, NULL, NULL, 4, NULL); +#endif + /* Callback before the connection is accepted. */ + evhtp_set_pre_accept_cb(htp, cx_pre_accept, h); + /* Callback right after a connection is accepted. */ + evhtp_set_post_accept_cb(htp, cx_post_accept, h); + /* Callback to be executed for all /restconf api calls */ + if (evhtp_set_cb(htp, "/" RESTCONF_API, cx_path_restconf, h) == NULL){ + clicon_err(OE_EVENTS, errno, "evhtp_set_cb"); + goto done; + } + /* Callback to be executed for all /restconf api calls */ + if (evhtp_set_cb(htp, RESTCONF_WELL_KNOWN, cx_path_wellknown, h) == NULL){ + clicon_err(OE_EVENTS, errno, "evhtp_set_cb"); + goto done; + } + /* Generic callback called if no other callbacks are matched */ + evhtp_set_gencb(htp, cx_gencb, h); + + /* Extract socket parameters from single socket config: ns, addr, port, ssl */ + if (cx_evhtp_socket_extract(h, xs, nsc, &namespace, &address, &addrtype, &port, &ssl) < 0) + goto done; + /* Sanity checks of socket parameters */ + if (ssl){ + if (ssl_enable == 0 || server_cert_path==NULL || server_key_path == NULL){ + clicon_err(OE_XML, EINVAL, "Enabled SSL server requires server_cert_path and server_key_path"); + goto done; + } + // ssl_verify_mode = htp_sslutil_verify2opts(optarg); + if (evhtp_ssl_init(htp, eh->eh_ssl_config) < 0){ + clicon_err(OE_UNIX, errno, "evhtp_new"); + goto done; + } + } + /* Open restconf socket and bind */ + if (restconf_socket_init(h, address, addrtype, port, &ss) < 0) + goto done; + /* ss is a server socket that the clients connect to. The callback + therefore accepts clients on ss */ + /* XXX address in evhtp should be prefixed with eg "ipv4:" */ + evutil_make_socket_closeonexec(ss); // XXX + evutil_make_socket_nonblocking(ss); // XXX + if (evhtp_accept_socket(htp, ss, SOCKET_LISTEN_BACKLOG) < 0) { + /* accept_socket() does not close the descriptor + * on error, but this function does. + */ + evutil_closesocket(ss); + goto done; + } + if (cx_htp_add(eh, htp) < 0) + goto done; + retval = 0; + done: + return retval; +} + +/*! Phase 2 of backend evhtp init, config has been retrieved from backend * @param[in] h Clicon handle * @param[in] xconfig XML config * @param[in] nsc Namespace context @@ -780,30 +1005,29 @@ cx_evhtp_init(clicon_handle h, cxobj *xconfig, cvec *nsc, cx_evhtp_handle *eh) - { - int retval = -1; - int auth_type_client_certificate = 0; - uint16_t port = 0; - cxobj *xrestconf; - cxobj **vec = NULL; - size_t veclen; - char *auth_type = NULL; - char *server_cert_path = NULL; - char *server_key_path = NULL; - char *server_ca_cert_path = NULL; + int retval = -1; + cxobj *xrestconf; + cxobj **vec = NULL; + size_t veclen; + char *server_cert_path = NULL; + char *server_key_path = NULL; + char *server_ca_cert_path = NULL; + char *auth_type = NULL; + int auth_type_client_certificate = 0; //XXX char *client_cert_ca = NULL; - cxobj *x; - char *namespace = NULL; - char *address = NULL; - uint16_t use_ssl_server = 0; - + cxobj *x; + int i; + int ssl_enable = 0; + /* Extract socket fields from xconfig */ if ((xrestconf = xpath_first(xconfig, nsc, "restconf")) == NULL){ clicon_err(OE_CFG, ENOENT, "restconf top symbol not found"); goto done; } /* get common fields */ + if ((x = xpath_first(xrestconf, nsc, "ssl-enable")) != NULL) + ssl_enable = (strcmp(xml_body(x),"true")==0); if ((x = xpath_first(xrestconf, nsc, "auth-type")) != NULL) /* XXX: leaf-list? */ auth_type = xml_body(x); if (auth_type && strcmp(auth_type, "client-certificate") == 0) @@ -814,42 +1038,9 @@ cx_evhtp_init(clicon_handle h, server_key_path = xml_body(x); if ((x = xpath_first(xrestconf, nsc, "server-ca-cert-path")) != NULL) server_ca_cert_path = xml_body(x); - /* get the list of socket config-data */ - if (xpath_vec(xrestconf, nsc, "socket", &vec, &veclen) < 0) - goto done; - /* Accept only a single socket XXX */ - if (veclen != 1){ - clicon_err(OE_XML, EINVAL, "Only single socket supported"); /* XXX warning: accept more? */ - goto done; - } - if (cx_evhtp_socket(h, vec[0], nsc, &namespace, &address, &port, &use_ssl_server) < 0) - goto done; - if (use_ssl_server && - (server_cert_path==NULL || server_key_path == NULL)){ - clicon_err(OE_XML, EINVAL, "Enabled SSL server requires server_cert_path and server_key_path"); - goto done; - } - if (auth_type_client_certificate){ - if (!use_ssl_server){ - clicon_err(OE_XML, EINVAL, "Client certificate authentication type requires SSL"); - goto done; - } - if (server_ca_cert_path == NULL){ - clicon_err(OE_XML, EINVAL, "Client certificate authentication type requires server-ca-cert-path"); - goto done; - } - } - /* Init evhtp */ - if ((eh->eh_evbase = event_base_new()) == NULL){ - clicon_err(OE_UNIX, errno, "event_base_new"); - goto done; - } - /* create a new evhtp_t instance */ - if ((eh->eh_htp = evhtp_new(eh->eh_evbase, NULL)) == NULL){ - clicon_err(OE_UNIX, errno, "evhtp_new"); - goto done; - } - if (use_ssl_server){ + + /* Here the daemon either uses SSL or not, ie you cant seem to mix http and https :-( */ + if (ssl_enable){ /* Init evhtp ssl config struct */ if ((eh->eh_ssl_config = malloc(sizeof(evhtp_ssl_cfg_t))) == NULL){ clicon_err(OE_UNIX, errno, "malloc"); @@ -858,12 +1049,12 @@ cx_evhtp_init(clicon_handle h, memset(eh->eh_ssl_config, 0, sizeof(evhtp_ssl_cfg_t)); eh->eh_ssl_config->ssl_opts = SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1; - if (cx_get_ssl_server_certs(h, server_cert_path, - server_key_path, - eh->eh_ssl_config) < 0) + /* Read server ssl files cert and key */ + if (cx_get_ssl_server_certs(h, server_cert_path, server_key_path, eh->eh_ssl_config) < 0) goto done; + /* If client auth get client CA cert */ if (auth_type_client_certificate) - if (cx_get_ssl_client_certs(h, server_ca_cert_path, eh->eh_ssl_config) < 0) + if (cx_get_ssl_client_ca_certs(h, server_ca_cert_path, eh->eh_ssl_config) < 0) goto done; eh->eh_ssl_config->x509_verify_cb = cx_verify_certs; /* Is extra verification necessary? */ if (auth_type_client_certificate){ @@ -873,52 +1064,15 @@ cx_evhtp_init(clicon_handle h, } // ssl_verify_mode = htp_sslutil_verify2opts(optarg); } - assert(SSL_VERIFY_NONE == 0); - - /* Init evhtp */ - if ((eh->eh_evbase = event_base_new()) == NULL){ - clicon_err(OE_UNIX, errno, "event_base_new"); - goto done; - } - /* create a new evhtp_t instance */ - if ((eh->eh_htp = evhtp_new(eh->eh_evbase, NULL)) == NULL){ - clicon_err(OE_UNIX, errno, "evhtp_new"); - goto done; - } - /* Here the daemon either uses SSL or not, ie you cant seem to mix http and https :-( */ - if (use_ssl_server){ - if (evhtp_ssl_init(eh->eh_htp, eh->eh_ssl_config) < 0){ - clicon_err(OE_UNIX, errno, "evhtp_new"); - goto done; - } - } -#ifndef EVHTP_DISABLE_EVTHR /* threads */ - evhtp_use_threads_wexit(eh->eh_htp, NULL, NULL, 4, NULL); -#endif - /* Callback before the connection is accepted. */ - evhtp_set_pre_accept_cb(eh->eh_htp, cx_pre_accept, h); - /* Callback right after a connection is accepted. */ - evhtp_set_post_accept_cb(eh->eh_htp, cx_post_accept, h); - /* Callback to be executed for all /restconf api calls */ - if (evhtp_set_cb(eh->eh_htp, "/" RESTCONF_API, cx_path_restconf, h) == NULL){ - clicon_err(OE_EVENTS, errno, "evhtp_set_cb"); - goto done; - } - /* Callback to be executed for all /restconf api calls */ - if (evhtp_set_cb(eh->eh_htp, RESTCONF_WELL_KNOWN, cx_path_wellknown, h) == NULL){ - clicon_err(OE_EVENTS, errno, "evhtp_set_cb"); - goto done; - } - /* Generic callback called if no other callbacks are matched */ - evhtp_set_gencb(eh->eh_htp, cx_gencb, h); - if (evhtp_bind_socket(eh->eh_htp, /* evhtp handle */ - address, /* string address, eg ipv4: */ - port, /* port */ - SOCKET_LISTEN_BACKLOG /* backlog flag, see listen(5) */ - ) < 0){ - clicon_err(OE_UNIX, errno, "evhtp_bind_socket"); + /* get the list of socket config-data */ + if (xpath_vec(xrestconf, nsc, "socket", &vec, &veclen) < 0) goto done; + for (i=0; ieh_evbase = event_base_new()) == NULL){ + clicon_err(OE_UNIX, errno, "event_base_new"); + goto done; + } + if (cx_evhtp_init(h, xconfig, nsc, eh) < 0) goto done; /* Drop privileges after evhtp and server key/cert read */ @@ -1069,7 +1227,6 @@ restconf_config_backend(clicon_handle h, } /* libevent main loop */ event_base_loop(eh->eh_evbase, 0); /* XXX: replace with clixon_event_loop() */ - retval = 0; done: if (xconfig) @@ -1083,6 +1240,7 @@ restconf_config_backend(clicon_handle h, /*! Read config locally */ int restconf_config_local(clicon_handle h, + cx_evhtp_handle *eh, int argc, char **argv, uint16_t port, @@ -1101,7 +1259,7 @@ restconf_config_local(clicon_handle h, size_t cligen_bufthreshold; char *restconf_ipv4_addr = NULL; char *restconf_ipv6_addr = NULL; - cx_evhtp_handle *eh = NULL; + evhtp_t *htp; /* port = defaultport unless explicitly set -P */ if (port == 0){ @@ -1111,13 +1269,6 @@ restconf_config_local(clicon_handle h, /* Set default namespace according to CLICON_NAMESPACE_NETCONF_DEFAULT */ xml_nsctx_namespace_netconf_default(h); - /* Initialize evhtp handle */ - if ((eh = malloc(sizeof *eh)) == NULL){ - clicon_err(OE_UNIX, errno, "malloc"); - goto done; - } - memset(eh, 0, sizeof *eh); - _EVHTP_HANDLE = eh; /* global */ /* Check server ssl certs */ if (use_ssl){ /* Init evhtp ssl config struct */ @@ -1148,39 +1299,6 @@ restconf_config_local(clicon_handle h, clicon_err(OE_UNIX, errno, "event_base_new"); goto done; } - /* create a new evhtp_t instance */ - if ((eh->eh_htp = evhtp_new(eh->eh_evbase, NULL)) == NULL){ - clicon_err(OE_UNIX, errno, "evhtp_new"); - goto done; - } - /* Here the daemon either uses SSL or not, ie you cant seem to mix http and https :-( */ - if (use_ssl){ - if (evhtp_ssl_init(eh->eh_htp, eh->eh_ssl_config) < 0){ - clicon_err(OE_UNIX, errno, "evhtp_new"); - goto done; - } - } -#ifndef EVHTP_DISABLE_EVTHR - evhtp_use_threads_wexit(eh->eh_htp, NULL, NULL, 4, NULL); -#endif - /* Callback before the connection is accepted. */ - evhtp_set_pre_accept_cb(eh->eh_htp, cx_pre_accept, h); - - /* Callback right after a connection is accepted. */ - evhtp_set_post_accept_cb(eh->eh_htp, cx_post_accept, h); - - /* Callback to be executed for all /restconf api calls */ - if (evhtp_set_cb(eh->eh_htp, "/" RESTCONF_API, cx_path_restconf, h) == NULL){ - clicon_err(OE_EVENTS, errno, "evhtp_set_cb"); - goto done; - } - /* Callback to be executed for all /restconf api calls */ - if (evhtp_set_cb(eh->eh_htp, RESTCONF_WELL_KNOWN, cx_path_wellknown, h) == NULL){ - clicon_err(OE_EVENTS, errno, "evhtp_set_cb"); - goto done; - } - /* Generic callback called if no other callbacks are matched */ - evhtp_set_gencb(eh->eh_htp, cx_gencb, h); /* bind to a socket, optionally with specific protocol support formatting */ @@ -1193,12 +1311,47 @@ restconf_config_local(clicon_handle h, } if (restconf_ipv4_addr != NULL && strlen(restconf_ipv4_addr)){ cbuf *cb; + + /* create a new evhtp_t instance */ + if ((htp = evhtp_new(eh->eh_evbase, NULL)) == NULL){ + clicon_err(OE_UNIX, errno, "evhtp_new"); + goto done; + } + /* Here the daemon either uses SSL or not, ie you cant seem to mix http and https :-( */ + if (use_ssl){ + if (evhtp_ssl_init(htp, eh->eh_ssl_config) < 0){ + clicon_err(OE_UNIX, errno, "evhtp_new"); + goto done; + } + } +#ifndef EVHTP_DISABLE_EVTHR + evhtp_use_threads_wexit(htp, NULL, NULL, 4, NULL); +#endif + /* Callback before the connection is accepted. */ + evhtp_set_pre_accept_cb(htp, cx_pre_accept, h); + + /* Callback right after a connection is accepted. */ + evhtp_set_post_accept_cb(htp, cx_post_accept, h); + + /* Callback to be executed for all /restconf api calls */ + if (evhtp_set_cb(htp, "/" RESTCONF_API, cx_path_restconf, h) == NULL){ + clicon_err(OE_EVENTS, errno, "evhtp_set_cb"); + goto done; + } + /* Callback to be executed for all /restconf api calls */ + if (evhtp_set_cb(htp, RESTCONF_WELL_KNOWN, cx_path_wellknown, h) == NULL){ + clicon_err(OE_EVENTS, errno, "evhtp_set_cb"); + goto done; + } + /* Generic callback called if no other callbacks are matched */ + evhtp_set_gencb(htp, cx_gencb, h); + if ((cb = cbuf_new()) == NULL){ clicon_err(OE_UNIX, errno, "cbuf_new"); goto done; } cprintf(cb, "ipv4:%s", restconf_ipv4_addr); - if (evhtp_bind_socket(eh->eh_htp, /* evhtp handle */ + if (evhtp_bind_socket(htp, /* evhtp handle */ cbuf_get(cb), /* string address, eg ipv4: */ port, /* port */ SOCKET_LISTEN_BACKLOG /* backlog flag, see listen(5) */ @@ -1208,16 +1361,51 @@ restconf_config_local(clicon_handle h, } if (cb) cbuf_free(cb); + if (cx_htp_add(eh, htp) < 0) + goto done; } /* Eeh can only bind one */ - if (0 && restconf_ipv6_addr != NULL && strlen(restconf_ipv6_addr)){ + if (restconf_ipv6_addr != NULL && strlen(restconf_ipv6_addr)){ cbuf *cb; + /* create a new evhtp_t instance */ + if ((htp = evhtp_new(eh->eh_evbase, NULL)) == NULL){ + clicon_err(OE_UNIX, errno, "evhtp_new"); + goto done; + } + /* Here the daemon either uses SSL or not, ie you cant seem to mix http and https :-( */ + if (use_ssl){ + if (evhtp_ssl_init(htp, eh->eh_ssl_config) < 0){ + clicon_err(OE_UNIX, errno, "evhtp_new"); + goto done; + } + } +#ifndef EVHTP_DISABLE_EVTHR + evhtp_use_threads_wexit(htp, NULL, NULL, 4, NULL); +#endif + /* Callback before the connection is accepted. */ + evhtp_set_pre_accept_cb(htp, cx_pre_accept, h); + + /* Callback right after a connection is accepted. */ + evhtp_set_post_accept_cb(htp, cx_post_accept, h); + + /* Callback to be executed for all /restconf api calls */ + if (evhtp_set_cb(htp, "/" RESTCONF_API, cx_path_restconf, h) == NULL){ + clicon_err(OE_EVENTS, errno, "evhtp_set_cb"); + goto done; + } + /* Callback to be executed for all /restconf api calls */ + if (evhtp_set_cb(htp, RESTCONF_WELL_KNOWN, cx_path_wellknown, h) == NULL){ + clicon_err(OE_EVENTS, errno, "evhtp_set_cb"); + goto done; + } + /* Generic callback called if no other callbacks are matched */ + evhtp_set_gencb(htp, cx_gencb, h); if ((cb = cbuf_new()) == NULL){ clicon_err(OE_UNIX, errno, "cbuf_new"); goto done; } cprintf(cb, "ipv6:%s", restconf_ipv6_addr); - if (evhtp_bind_socket(eh->eh_htp, /* evhtp handle */ + if (evhtp_bind_socket(htp, /* evhtp handle */ cbuf_get(cb), /* string address, eg ipv6: */ port, /* port */ SOCKET_LISTEN_BACKLOG /* backlog flag, see listen(5) */ @@ -1227,6 +1415,8 @@ restconf_config_local(clicon_handle h, } if (cb) cbuf_free(cb); + if (cx_htp_add(eh, htp) < 0) + goto done; } if (drop_privileges){ @@ -1329,7 +1519,7 @@ restconf_config_local(clicon_handle h, clicon_debug(1, "restconf_main_evhtp done"); return retval; } - + int main(int argc, char **argv) @@ -1491,10 +1681,16 @@ main(int argc, /* port = defaultport unless explicitly set -P */ if (port == 0) port = defaultport; + if ((eh = malloc(sizeof *eh)) == NULL){ + clicon_err(OE_UNIX, errno, "malloc"); + goto done; + } + memset(eh, 0, sizeof *eh); + _EVHTP_HANDLE = eh; /* global */ if (clicon_option_bool(h, "CLICON_RESTCONF_CONFIG") == 0){ /* Read config locally */ - if (restconf_config_local(h, argc, argv, + if (restconf_config_local(h, eh, argc, argv, port, ssl_verify_clients, use_ssl, @@ -1504,12 +1700,10 @@ main(int argc, } else { /* Read config from backend */ - if (restconf_config_backend(h, argc, argv, drop_privileges) < 0) + if (restconf_config_backend(h, eh, argc, argv, drop_privileges) < 0) goto done; } - event_base_loop(eh->eh_evbase, 0); - retval = 0; done: clicon_debug(1, "restconf_main_evhtp done"); diff --git a/docker/main/Makefile.in b/docker/main/Makefile.in index 2c3e4ccd..393fbe2f 100644 --- a/docker/main/Makefile.in +++ b/docker/main/Makefile.in @@ -31,6 +31,10 @@ # # ***** END LICENSE BLOCK ***** # +# Build, compile and test a clixon-system container +# This is used in CI +# NOTE: restconf config is controlled at install time with ./configure --with-restconf=... option + VPATH = @srcdir@ srcdir = @srcdir@ top_srcdir = @top_srcdir@ diff --git a/lib/clixon/clixon_yang_type.h b/lib/clixon/clixon_yang_type.h index b92ec4ae..30f6be46 100644 --- a/lib/clixon/clixon_yang_type.h +++ b/lib/clixon/clixon_yang_type.h @@ -61,7 +61,7 @@ int yang2cv_type(char *ytype, enum cv_type *cv_type); char *cv2yang_type(enum cv_type cv_type); yang_stmt *yang_find_identity(yang_stmt *ys, char *identity); yang_stmt *yang_find_identity_nsc(yang_stmt *yspec, char *identity, cvec *nsc); -int ys_cv_validate(clicon_handle h, cg_var *cv, yang_stmt *ys, char **reason); +int ys_cv_validate(clicon_handle h, cg_var *cv, yang_stmt *ys, yang_stmt **ysub, char **reason); int clicon_type2cv(char *type, char *rtype, yang_stmt *ys, enum cv_type *cvtype); int yang_type_get(yang_stmt *ys, char **otype, yang_stmt **restype, int *options, cvec **cvv, diff --git a/lib/src/clixon_validate.c b/lib/src/clixon_validate.c index ee4c7586..520eb426 100644 --- a/lib/src/clixon_validate.c +++ b/lib/src/clixon_validate.c @@ -1024,7 +1024,7 @@ xml_yang_validate_add(clicon_handle h, goto fail; } } - if ((ret = ys_cv_validate(h, cv, yt, &reason)) < 0) + if ((ret = ys_cv_validate(h, cv, yt, NULL, &reason)) < 0) goto done; if (ret == 0){ if (netconf_bad_element_xml(xret, "application", yang_argument_get(yt), reason) < 0) diff --git a/lib/src/clixon_yang_type.c b/lib/src/clixon_yang_type.c index 80eaaa27..bbb287a5 100644 --- a/lib/src/clixon_yang_type.c +++ b/lib/src/clixon_yang_type.c @@ -221,7 +221,7 @@ compile_pattern2regexp(clicon_handle h, /*! Resolve types: populate type caches * @param[in] ys This is a type statement * @param[in] arg Not used - * Typically only called once when loading te yang type system. + * Typically only called once when loading the yang type system. * @note unions not cached */ int @@ -741,7 +741,7 @@ cv_validate1(clicon_handle h, /* Forward */ static int ys_cv_validate_union(clicon_handle h,yang_stmt *ys, char **reason, - yang_stmt *yrestype, char *type, char *val); + yang_stmt *yrestype, char *type, char *val, yang_stmt **ysubp); /*! * @param[out] reason If given and return val is 0, contains a malloced string @@ -767,6 +767,7 @@ ys_cv_validate_union_one(clicon_handle h, char *restype; enum cv_type cvtype; cg_var *cvt=NULL; + yang_stmt *ysubt = NULL; if ((regexps = cvec_new(0)) == NULL){ clicon_err(OE_UNIX, errno, "cvec_new"); @@ -781,7 +782,7 @@ ys_cv_validate_union_one(clicon_handle h, goto done; restype = yrt?yang_argument_get(yrt):NULL; if (restype && strcmp(restype, "union") == 0){ /* recursive union */ - if ((retval = ys_cv_validate_union(h, ys, reason, yrt, type, val)) < 0) + if ((retval = ys_cv_validate_union(h, ys, reason, yrt, type, val, &ysubt)) < 0) goto done; } else { @@ -828,18 +829,23 @@ ys_cv_validate_union_one(clicon_handle h, } /*! Validate union + * @param[in] h Clixon handle + * @param[in] ys Yang statement (union) * @param[out] reason If given, and return value is 0, contains malloced string + * @param[in] val Value to match + * @param[out] ysubp Sub-type of ys that matches val * @retval -1 Error (fatal), with errno set to indicate error * @retval 0 Validation not OK, malloced reason is returned. Free reason with free() * @retval 1 Validation OK */ static int ys_cv_validate_union(clicon_handle h, - yang_stmt *ys, - char **reason, - yang_stmt *yrestype, - char *type, /* orig type */ - char *val) + yang_stmt *ys, + char **reason, + yang_stmt *yrestype, + char *type, /* orig type */ + char *val, + yang_stmt **ysubp) { int retval = 1; /* valid */ yang_stmt *yt = NULL; @@ -859,8 +865,13 @@ ys_cv_validate_union(clicon_handle h, reason1 = *reason; *reason = NULL; } - if (retval == 1) /* Enough that one type validates value */ + /* Enough that one type validates value, return that value + */ + if (retval == 1) { + if (ysubp) + *ysubp = yt; break; + } } done: if (retval == 0 && reason1){ @@ -877,6 +888,7 @@ ys_cv_validate_union(clicon_handle h, * @param[in] h Clicon handle * @param[in] cv A cligen variable to validate. This is a correctly parsed cv. * @param[in] ys A yang statement, must be leaf or leaf-list. + * @param[out] ysub Sub-type that matches val (in case of union, otherwise ys) * @param[out] reason If given, and if return value is 0, contains malloced * string describing reason why validation failed. * @retval -1 Error (fatal), with errno set to indicate error @@ -889,6 +901,7 @@ int ys_cv_validate(clicon_handle h, cg_var *cv, yang_stmt *ys, + yang_stmt **ysub, char **reason) { int retval = -1; @@ -948,7 +961,7 @@ ys_cv_validate(clicon_handle h, */ if ((val = cv_string_get(cv)) == NULL) val = ""; - if ((retval2 = ys_cv_validate_union(h, ys, reason, yrestype, origtype, val)) < 0) + if ((retval2 = ys_cv_validate_union(h, ys, reason, yrestype, origtype, val, ysub)) < 0) goto done; retval = retval2; /* invalid (0) with latest reason or valid 1 */ } @@ -969,6 +982,8 @@ ys_cv_validate(clicon_handle h, if ((retval = cv_validate1(h, cv, cvtype, options, cvv, regexps, yrestype, restype, reason)) < 0) goto done; + if (ysub) + *ysub = ys; } done: if (origtype) diff --git a/test/certs.sh b/test/certs.sh new file mode 100644 index 00000000..d1b06265 --- /dev/null +++ b/test/certs.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# Create server certs +# Assume: the following variables set: +# $dir, $certdir, $srvkey, $srvcert, $cakey, $cacert +# and that $certdir exists + +# 1. CA +cat< $dir/ca.cnf +[ ca ] +default_ca = CA_default + +[ CA_default ] +serial = ca-serial +crl = ca-crl.pem +database = ca-database.txt +name_opt = CA_default +cert_opt = CA_default +default_crl_days = 9999 +default_md = md5 + +[ req ] +default_bits = 2048 +days = 1 +distinguished_name = req_distinguished_name +attributes = req_attributes +prompt = no +output_password = password + +[ req_distinguished_name ] +C = SE +L = Stockholm +O = Clixon +OU = clixon +CN = ca +emailAddress = olof@hagsand.se + +[ req_attributes ] +challengePassword = test + +EOF + +# Generate CA cert +openssl req -x509 -days 1 -config $dir/ca.cnf -keyout $cakey -out $cacert + +cat< $dir/srv.cnf +[req] +prompt = no +distinguished_name = dn +req_extensions = ext +[dn] +CN = www.clicon.org # localhost +emailAddress = olof@hagsand.se +O = Clixon +L = Stockholm +C = SE +[ext] +subjectAltName = DNS:clicon.org +EOF + +# Generate server key +openssl genrsa -out $srvkey 2048 + +# Generate CSR (signing request) +openssl req -new -config $dir/srv.cnf -key $srvkey -out $certdir/srv_csr.pem + +# Sign server cert by CA +openssl x509 -req -extfile $dir/srv.cnf -days 1 -passin "pass:password" -in $certdir/srv_csr.pem -CA $cacert -CAkey $cakey -CAcreateserial -out $srvcert diff --git a/test/lib.sh b/test/lib.sh index e1ec4138..6569bb15 100755 --- a/test/lib.sh +++ b/test/lib.sh @@ -42,6 +42,7 @@ if [ -f ./config.sh ]; then fi # Sanity nginx running on systemd platforms +# ./lib.sh: line 45: systemctl: command not found if systemctl > /dev/null; then nginxactive=$(systemctl show nginx |grep ActiveState=active) if [ "${WITH_RESTCONF}" = "fcgi" ]; then @@ -273,8 +274,8 @@ wait_backend(){ # @see wait_restconf start_restconf(){ # Start in background - if [ $RCPROTO = https ]; then - EXTRA="-s" # server certs + if [ $RCPROTO = https -a "${WITH_RESTCONF}" = "evhtp" ]; then + EXTRA="-s" # server certs ONLY evhtp else EXTRA= fi diff --git a/test/test_restconf.sh b/test/test_restconf.sh index 904d7571..a1dfbce0 100755 --- a/test/test_restconf.sh +++ b/test/test_restconf.sh @@ -1,7 +1,18 @@ #!/usr/bin/env bash -# Restconf basic functionality -# also uri encoding using eth/0/0 -# Assume http server setup, such as nginx described in apps/restconf/README.md +# Restconf basic functionality also uri encoding using eth/0/0 +# Note there are many variants: (1)fcgi/evhtp, (2) http/https, (3) IPv4/IPv6, (4)local or backend-config +# (1) fcgi/evhtp +# This is compile-time --with-restconf=fcgi or evhtp, so either or +# - fcgi: Assume http server setup, such as nginx described in apps/restconf/README.md +# - evhtp: test both local config and get config from backend +# (2) http/https +# - fcgi: relies on nginx has https setup +# - evhtp: generate self-signed server certs +# (3) IPv4/IPv6 (only loopback 127.0.0.1 / ::1) +# - The tests runs through both +# (4) local/backend config. Evhtp only +# - The tests runs through both (if compiled with evhtp) +# See also test_restconf2.sh # Magic line must be first in script (see README.md) s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi @@ -29,24 +40,62 @@ cat < $cfg /usr/local/var/$APPNAME/$APPNAME.pidfile /usr/local/var/$APPNAME true +EOF + +if [ "${WITH_RESTCONF}" = "evhtp" ]; then + # Create server certs + certdir=$dir/certs + srvkey=$certdir/srv_key.pem + srvcert=$certdir/srv_cert.pem + cakey=$certdir/ca_key.pem # needed? + cacert=$certdir/ca_cert.pem + test -d $certdir || mkdir $certdir + . ./certs.sh + cat <> $cfg + $srvcert + $srvkey + $srvcert +EOF +fi + +cat <> $cfg EOF # This is a fixed 'state' implemented in routing_backend. It is assumed to be always there state='{"clixon-example:state":{"op":\["41","42","43"\]}' -if [ ${RCPROTO} = "https" ]; then - ssl=true - port=443 -else - ssl=false - port=80 -fi +# For backend config, create 4 sockets, all combinations IPv4/IPv6 + http/https +RESTCONFCONFIG=$(cat < + true + password + $srvcert + $srvkey + $cakey + default
0.0.0.0
80false
+ default
::
80false
+ default
0.0.0.0
443true
+ default
::
443true
+ +EOF +) +# Restconf test routine with arguments: +# 1. proto:http/https +# 2: addr: 127.0.0.1/::1 # IPv4 or IPv6 +# 3: config: local / backend config (evhtp only) testrun() { - USEBACKEND=$1 - + proto=$1 # http/https + addr=$2 # 127.0.0.1/::1 + config=$3 # local/backend + + RCPROTO=$proto # for start/wait of restconf + echo "proto:$proto" + echo "addr:$addr" + echo "config:$config" + new "test params: -f $cfg -- -s" if [ $BE -ne 0 ]; then new "kill old backend" @@ -63,10 +112,10 @@ testrun() new "wait backend" wait_backend - if $USEBACKEND; then + if [ $config = backend ] ; then # Create a backend config + # restconf backend config new "netconf edit config" - expecteof "$clixon_netconf -qf $cfg" 0 "default
0.0.0.0
$port$ssl
password
]]>]]>" - "^]]>]]>$" + expecteof "$clixon_netconf -qf $cfg" 0 "$RESTCONFCONFIG]]>]]>" "^]]>]]>$" new "netconf commit" expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^]]>]]>$" @@ -76,7 +125,7 @@ testrun() new "kill old restconf daemon" stop_restconf_pre - if $USEBACKEND; then + if [ $config = backend ] ; then # Add -b option new "start restconf daemon -b" start_restconf -f $cfg -b else @@ -86,23 +135,23 @@ testrun() fi new "wait restconf" wait_restconf - + new "restconf root discovery. RFC 8040 3.1 (xml+xrd)" - expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/.well-known/host-meta)" 0 'HTTP/1.1 200 OK' "" "" "" + expectpart "$(curl $CURLOPTS -X GET $proto://$addr/.well-known/host-meta)" 0 'HTTP/1.1 200 OK' "" "" "" new "restconf get restconf resource. RFC 8040 3.3 (json)" -expectpart "$(curl $CURLOPTS -X GET -H "Accept: application/yang-data+json" $RCPROTO://localhost/restconf)" 0 'HTTP/1.1 200 OK' '{"ietf-restconf:restconf":{"data":{},"operations":{},"yang-library-version":"2019-01-04"}}' +expectpart "$(curl $CURLOPTS -X GET -H "Accept: application/yang-data+json" $proto://$addr/restconf)" 0 'HTTP/1.1 200 OK' '{"ietf-restconf:restconf":{"data":{},"operations":{},"yang-library-version":"2019-01-04"}}' new "restconf get restconf resource. RFC 8040 3.3 (xml)" # Get XML instead of JSON? -expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPROTO://localhost/restconf)" 0 'HTTP/1.1 200 OK' '2019-01-04' +expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $proto://$addr/restconf)" 0 'HTTP/1.1 200 OK' '2019-01-04' # Should be alphabetically ordered new "restconf get restconf/operations. RFC8040 3.3.2 (json)" - expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/operations)" 0 'HTTP/1.1 200 OK' '{"operations":{"clixon-example:client-rpc":\[null\],"clixon-example:empty":\[null\],"clixon-example:optional":\[null\],"clixon-example:example":\[null\],"clixon-lib:debug":\[null\],"clixon-lib:ping":\[null\],"clixon-lib:stats":\[null\],"clixon-lib:restart-plugin":\[null\],"ietf-netconf:get-config":\[null\],"ietf-netconf:edit-config":\[null\],"ietf-netconf:copy-config":\[null\],"ietf-netconf:delete-config":\[null\],"ietf-netconf:lock":\[null\],"ietf-netconf:unlock":\[null\],"ietf-netconf:get":\[null\],"ietf-netconf:close-session":\[null\],"ietf-netconf:kill-session":\[null\],"ietf-netconf:commit":\[null\],"ietf-netconf:discard-changes":\[null\],"ietf-netconf:validate":\[null\]' + expectpart "$(curl $CURLOPTS -X GET $proto://$addr/restconf/operations)" 0 'HTTP/1.1 200 OK' '{"operations":{"clixon-example:client-rpc":\[null\],"clixon-example:empty":\[null\],"clixon-example:optional":\[null\],"clixon-example:example":\[null\],"clixon-lib:debug":\[null\],"clixon-lib:ping":\[null\],"clixon-lib:stats":\[null\],"clixon-lib:restart-plugin":\[null\],"ietf-netconf:get-config":\[null\],"ietf-netconf:edit-config":\[null\],"ietf-netconf:copy-config":\[null\],"ietf-netconf:delete-config":\[null\],"ietf-netconf:lock":\[null\],"ietf-netconf:unlock":\[null\],"ietf-netconf:get":\[null\],"ietf-netconf:close-session":\[null\],"ietf-netconf:kill-session":\[null\],"ietf-netconf:commit":\[null\],"ietf-netconf:discard-changes":\[null\],"ietf-netconf:validate":\[null\]' new "restconf get restconf/operations. RFC8040 3.3.2 (xml)" - ret=$(curl $CURLOPTS -X GET -H "Accept: application/yang-data+xml" $RCPROTO://localhost/restconf/operations) + ret=$(curl $CURLOPTS -X GET -H "Accept: application/yang-data+xml" $proto://$addr/restconf/operations) expect='' match=`echo $ret | grep --null -Eo "$expect"` if [ -z "$match" ]; then @@ -110,10 +159,10 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPR fi new "restconf get restconf/yang-library-version. RFC8040 3.3.3" - expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/yang-library-version)" 0 'HTTP/1.1 200 OK' '{"yang-library-version":"2019-01-04"}' + expectpart "$(curl $CURLOPTS -X GET $proto://$addr/restconf/yang-library-version)" 0 'HTTP/1.1 200 OK' '{"yang-library-version":"2019-01-04"}' new "restconf get restconf/yang-library-version. RFC8040 3.3.3 (xml)" - ret=$(curl $CURLOPTS -X GET -H "Accept: application/yang-data+xml" $RCPROTO://localhost/restconf/yang-library-version) + ret=$(curl $CURLOPTS -X GET -H "Accept: application/yang-data+xml" $proto://$addr/restconf/yang-library-version) expect="2019-01-04" match=`echo $ret | grep --null -Eo "$expect"` if [ -z "$match" ]; then @@ -121,48 +170,48 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPR fi new "restconf schema resource, RFC 8040 sec 3.7 according to RFC 7895 (explicit resource)" - expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/ietf-yang-library:modules-state/module=ietf-interfaces,2018-02-20)" 0 'HTTP/1.1 200 OK' '{"ietf-yang-library:module":\[{"name":"ietf-interfaces","revision":"2018-02-20","namespace":"urn:ietf:params:xml:ns:yang:ietf-interfaces","conformance-type":"implement"}\]}' + expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $proto://$addr/restconf/data/ietf-yang-library:modules-state/module=ietf-interfaces,2018-02-20)" 0 'HTTP/1.1 200 OK' '{"ietf-yang-library:module":\[{"name":"ietf-interfaces","revision":"2018-02-20","namespace":"urn:ietf:params:xml:ns:yang:ietf-interfaces","conformance-type":"implement"}\]}' new "restconf schema resource, mod-state top-level" - expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/ietf-yang-library:modules-state)" 0 'HTTP/1.1 200 OK' '{"ietf-yang-library:modules-state":{"module-set-id":"0","module":\[{"name":"clixon-example","revision":"2020-03-11","namespace":"urn:example:clixon","conformance-type":"implement"},{"name":"clixon-lib","revision":"2020-04-23","' + expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $proto://$addr/restconf/data/ietf-yang-library:modules-state)" 0 'HTTP/1.1 200 OK' '{"ietf-yang-library:modules-state":{"module-set-id":"0","module":\[{"name":"clixon-example","revision":"2020-03-11","namespace":"urn:example:clixon","conformance-type":"implement"},{"name":"clixon-lib","revision":"2020-04-23","' new "restconf options. RFC 8040 4.1" - expectpart "$(curl $CURLOPTS -X OPTIONS $RCPROTO://localhost/restconf/data)" 0 "HTTP/1.1 200 OK" "Allow: OPTIONS,HEAD,GET,POST,PUT,PATCH,DELETE" + expectpart "$(curl $CURLOPTS -X OPTIONS $proto://$addr/restconf/data)" 0 "HTTP/1.1 200 OK" "Allow: OPTIONS,HEAD,GET,POST,PUT,PATCH,DELETE" # -I means HEAD new "restconf HEAD. RFC 8040 4.2" - expectpart "$(curl $CURLOPTS -I -H "Accept: application/yang-data+json" $RCPROTO://localhost/restconf/data)" 0 "HTTP/1.1 200 OK" "Content-Type: application/yang-data+json" + expectpart "$(curl $CURLOPTS -I -H "Accept: application/yang-data+json" $proto://$addr/restconf/data)" 0 "HTTP/1.1 200 OK" "Content-Type: application/yang-data+json" new "restconf empty rpc JSON" - expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d {\"clixon-example:input\":null} $RCPROTO://localhost/restconf/operations/clixon-example:empty)" 0 "HTTP/1.1 204 No Content" + expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d {\"clixon-example:input\":null} $proto://$addr/restconf/operations/clixon-example:empty)" 0 "HTTP/1.1 204 No Content" new "restconf empty rpc XML" - expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+xml" -d '' $RCPROTO://localhost/restconf/operations/clixon-example:empty)" 0 "HTTP/1.1 204 No Content" + expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+xml" -d '' $proto://$addr/restconf/operations/clixon-example:empty)" 0 "HTTP/1.1 204 No Content" new "restconf empty rpc, default media type should fail" - expectpart "$(curl $CURLOPTS -X POST -d {\"clixon-example:input\":null} $RCPROTO://localhost/restconf/operations/clixon-example:empty)" 0 'HTTP/1.1 415 Unsupported Media Type' + expectpart "$(curl $CURLOPTS -X POST -d {\"clixon-example:input\":null} $proto://$addr/restconf/operations/clixon-example:empty)" 0 'HTTP/1.1 415 Unsupported Media Type' new "restconf empty rpc, default media type should fail (JSON)" - expectpart "$(curl $CURLOPTS -X POST -H "Accept: application/yang-data+json" -d {\"clixon-example:input\":null} $RCPROTO://localhost/restconf/operations/clixon-example:empty)" 0 'HTTP/1.1 415 Unsupported Media Type' + expectpart "$(curl $CURLOPTS -X POST -H "Accept: application/yang-data+json" -d {\"clixon-example:input\":null} $proto://$addr/restconf/operations/clixon-example:empty)" 0 'HTTP/1.1 415 Unsupported Media Type' new "restconf empty rpc with extra args (should fail)" - expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d {\"clixon-example:input\":{\"extra\":null}} $RCPROTO://localhost/restconf/operations/clixon-example:empty)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"extra"},"error-severity":"error","error-message":"Unrecognized parameter: extra in rpc: empty"}}}' + expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d {\"clixon-example:input\":{\"extra\":null}} $proto://$addr/restconf/operations/clixon-example:empty)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"extra"},"error-severity":"error","error-message":"Unrecognized parameter: extra in rpc: empty"}}}' # Irritiating to get debugs on the terminal #new "restconf debug rpc" - #expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d {\"clixon-lib:input\":{\"level\":0}} $RCPROTO://localhost/restconf/operations/clixon-lib:debug)" 0 "HTTP/1.1 204 No Content" + #expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d {\"clixon-lib:input\":{\"level\":0}} $proto://$addr/restconf/operations/clixon-lib:debug)" 0 "HTTP/1.1 204 No Content" new "restconf get empty config + state json" - expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/data/clixon-example:state)" 0 "HTTP/1.1 200 OK" '{"clixon-example:state":{"op":\["41","42","43"\]}}' + expectpart "$(curl $CURLOPTS -X GET $proto://$addr/restconf/data/clixon-example:state)" 0 "HTTP/1.1 200 OK" '{"clixon-example:state":{"op":\["41","42","43"\]}}' new "restconf get empty config + state json with wrong module name" - expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/data/badmodule:state)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"badmodule"},"error-severity":"error","error-message":"No such yang module prefix"}}}' + expectpart "$(curl $CURLOPTS -X GET $proto://$addr/restconf/data/badmodule:state)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"badmodule"},"error-severity":"error","error-message":"No such yang module prefix"}}}' #'HTTP/1.1 404 Not Found' #'{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"No such yang module: badmodule"}}}' new "restconf get empty config + state xml" - ret=$(curl $CURLOPTS -H "Accept: application/yang-data+xml" -X GET $RCPROTO://localhost/restconf/data/clixon-example:state) + ret=$(curl $CURLOPTS -H "Accept: application/yang-data+xml" -X GET $proto://$addr/restconf/data/clixon-example:state) expect='414243' match=`echo $ret | grep --null -Eo "$expect"` if [ -z "$match" ]; then @@ -170,11 +219,11 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPR fi new "restconf get data type json" - expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/data/clixon-example:state/op=42)" 0 '{"clixon-example:op":"42"}' + expectpart "$(curl $CURLOPTS -X GET $proto://$addr/restconf/data/clixon-example:state/op=42)" 0 '{"clixon-example:op":"42"}' new "restconf get state operation" # Cant get shell macros to work, inline matching from lib.sh - ret=$(curl $CURLOPTS -H "Accept: application/yang-data+xml" -X GET $RCPROTO://localhost/restconf/data/clixon-example:state/op=42) + ret=$(curl $CURLOPTS -H "Accept: application/yang-data+xml" -X GET $proto://$addr/restconf/data/clixon-example:state/op=42) expect='42' match=`echo $ret | grep --null -Eo "$expect"` if [ -z "$match" ]; then @@ -182,11 +231,11 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPR fi new "restconf get state operation type json" - expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/data/clixon-example:state/op=42)" 0 '{"clixon-example:op":"42"}' + expectpart "$(curl $CURLOPTS -X GET $proto://$addr/restconf/data/clixon-example:state/op=42)" 0 '{"clixon-example:op":"42"}' new "restconf get state operation type xml" # Cant get shell macros to work, inline matching from lib.sh - ret=$(curl $CURLOPTS -H "Accept: application/yang-data+xml" -X GET $RCPROTO://localhost/restconf/data/clixon-example:state/op=42) + ret=$(curl $CURLOPTS -H "Accept: application/yang-data+xml" -X GET $proto://$addr/restconf/data/clixon-example:state/op=42) expect='42' match=`echo $ret | grep --null -Eo "$expect"` if [ -z "$match" ]; then @@ -194,88 +243,88 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPR fi new "restconf GET datastore" - expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/data/clixon-example:state)" 0 "HTTP/1.1 200 OK" '{"clixon-example:state":{"op":\["41","42","43"\]}}' + expectpart "$(curl $CURLOPTS -X GET $proto://$addr/restconf/data/clixon-example:state)" 0 "HTTP/1.1 200 OK" '{"clixon-example:state":{"op":\["41","42","43"\]}}' # Exact match new "restconf Add subtree eth/0/0 to datastore using POST" - expectpart "$(curl $CURLOPTS -X POST -H "Accept: application/yang-data+json" -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}}' $RCPROTO://localhost/restconf/data)" 0 'HTTP/1.1 201 Created' "Location: $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces" + expectpart "$(curl $CURLOPTS -X POST -H "Accept: application/yang-data+json" -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}}' $proto://$addr/restconf/data)" 0 'HTTP/1.1 201 Created' "Location: $proto://$addr/restconf/data/ietf-interfaces:interfaces" new "restconf Re-add subtree eth/0/0 which should give error" - expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}}' $RCPROTO://localhost/restconf/data)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}}' + expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}}' $proto://$addr/restconf/data)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}}' new "restconf Check interfaces eth/0/0 added" - expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces)" 0 "HTTP/1.1 200 OK" '{"ietf-interfaces:interfaces":{"interface":\[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}\]}}' + expectpart "$(curl $CURLOPTS -X GET $proto://$addr/restconf/data/ietf-interfaces:interfaces)" 0 "HTTP/1.1 200 OK" '{"ietf-interfaces:interfaces":{"interface":\[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}\]}}' new "restconf delete interfaces" - expectpart "$(curl $CURLOPTS -X DELETE $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces)" 0 "HTTP/1.1 204 No Content" + expectpart "$(curl $CURLOPTS -X DELETE $proto://$addr/restconf/data/ietf-interfaces:interfaces)" 0 "HTTP/1.1 204 No Content" new "restconf Check empty config" - expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/data/clixon-example:state)" 0 "HTTP/1.1 200 OK" "$state" + expectpart "$(curl $CURLOPTS -X GET $proto://$addr/restconf/data/clixon-example:state)" 0 "HTTP/1.1 200 OK" "$state" new "restconf Add interfaces subtree eth/0/0 using POST" - expectpart "$(curl $CURLOPTS -X POST $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}')" 0 "HTTP/1.1 201 Created" + expectpart "$(curl $CURLOPTS -X POST $proto://$addr/restconf/data/ietf-interfaces:interfaces -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}')" 0 "HTTP/1.1 201 Created" new "restconf Check eth/0/0 added config" - expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces)" 0 'HTTP/1.1 200 OK' '{"ietf-interfaces:interfaces":{"interface":\[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}\]}}' + expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $proto://$addr/restconf/data/ietf-interfaces:interfaces)" 0 'HTTP/1.1 200 OK' '{"ietf-interfaces:interfaces":{"interface":\[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}\]}}' new "restconf Check eth/0/0 GET augmented state level 1" - expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 'HTTP/1.1 200 OK' '{"ietf-interfaces:interface":\[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}\]}' + expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $proto://$addr/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 'HTTP/1.1 200 OK' '{"ietf-interfaces:interface":\[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}\]}' new "restconf Check eth/0/0 GET augmented state level 2" - expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0/clixon-example:my-status)" 0 'HTTP/1.1 200 OK' '{"clixon-example:my-status":{"int":42,"str":"foo"}}' + expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $proto://$addr/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0/clixon-example:my-status)" 0 'HTTP/1.1 200 OK' '{"clixon-example:my-status":{"int":42,"str":"foo"}}' new "restconf Check eth/0/0 added state XXXXXXX" - expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $RCPROTO://localhost/restconf/data/clixon-example:state)" 0 'HTTP/1.1 200 OK' '{"clixon-example:state":{"op":\["41","42","43"\]}}' + expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+json' $proto://$addr/restconf/data/clixon-example:state)" 0 'HTTP/1.1 200 OK' '{"clixon-example:state":{"op":\["41","42","43"\]}}' new "restconf Re-post eth/0/0 which should generate error" - expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}} ' + expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' $proto://$addr/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}} ' new "Add leaf description using POST" - expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:description":"The-first-interface"}' $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "HTTP/1.1 201 Created" + expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:description":"The-first-interface"}' $proto://$addr/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "HTTP/1.1 201 Created" new "Add nothing using POST (expect fail)" - expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"The message-body MUST contain exactly one instance of the expected data resource"}}}' + expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" $proto://$addr/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"The message-body MUST contain exactly one instance of the expected data resource"}}}' new "restconf Check description added" - expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces)" 0 "HTTP/1.1 200 OK" '{"ietf-interfaces:interfaces":{"interface":\[{"name":"eth/0/0","description":"The-first-interface","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}\]}}' + expectpart "$(curl $CURLOPTS -X GET $proto://$addr/restconf/data/ietf-interfaces:interfaces)" 0 "HTTP/1.1 200 OK" '{"ietf-interfaces:interfaces":{"interface":\[{"name":"eth/0/0","description":"The-first-interface","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}\]}}' new "restconf delete eth/0/0" - expectpart "$(curl $CURLOPTS -X DELETE $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "HTTP/1.1 204 No Content" + expectpart "$(curl $CURLOPTS -X DELETE $proto://$addr/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "HTTP/1.1 204 No Content" new "Check deleted eth/0/0" - expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/data)" 0 "HTTP/1.1 200 OK" "$state" + expectpart "$(curl $CURLOPTS -X GET $proto://$addr/restconf/data)" 0 "HTTP/1.1 200 OK" "$state" new "restconf Re-Delete eth/0/0 using none should generate error" - expectpart "$(curl $CURLOPTS -X DELETE $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "HTTP/1.1 409 Conflict" '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-missing","error-severity":"error","error-message":"Data does not exist; cannot delete resource"}}}' + expectpart "$(curl $CURLOPTS -X DELETE $proto://$addr/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "HTTP/1.1 409 Conflict" '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-missing","error-severity":"error","error-message":"Data does not exist; cannot delete resource"}}}' new "restconf Add subtree eth/0/0 using PUT" - expectpart "$(curl $CURLOPTS -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "HTTP/1.1 201 Created" + expectpart "$(curl $CURLOPTS -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' $proto://$addr/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "HTTP/1.1 201 Created" new "restconf get subtree" - expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces)" 0 "HTTP/1.1 200 OK" '{"ietf-interfaces:interfaces":{"interface":\[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}\]}}' + expectpart "$(curl $CURLOPTS -X GET $proto://$addr/restconf/data/ietf-interfaces:interfaces)" 0 "HTTP/1.1 200 OK" '{"ietf-interfaces:interfaces":{"interface":\[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}\]}}' new "restconf rpc using POST json" - expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{"clixon-example:input":{"x":42}}' $RCPROTO://localhost/restconf/operations/clixon-example:example)" 0 "HTTP/1.1 200 OK" '{"clixon-example:output":{"x":"42","y":"42"}}' + expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{"clixon-example:input":{"x":42}}' $proto://$addr/restconf/operations/clixon-example:example)" 0 "HTTP/1.1 200 OK" '{"clixon-example:output":{"x":"42","y":"42"}}' if ! $YANG_UNKNOWN_ANYDATA ; then new "restconf rpc using POST json wrong" - expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{"clixon-example:input":{"wrongelement":"ipv4"}}' $RCPROTO://localhost/restconf/operations/clixon-example:example)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"wrongelement"},"error-severity":"error","error-message":"Failed to find YANG spec of XML node: wrongelement with parent: example in namespace: urn:example:clixon"}}}' + expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{"clixon-example:input":{"wrongelement":"ipv4"}}' $proto://$addr/restconf/operations/clixon-example:example)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"wrongelement"},"error-severity":"error","error-message":"Failed to find YANG spec of XML node: wrongelement with parent: example in namespace: urn:example:clixon"}}}' fi new "restconf rpc non-existing rpc without namespace" - expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{}' $RCPROTO://localhost/restconf/operations/kalle)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"kalle"},"error-severity":"error","error-message":"RPC not defined"}}' + expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{}' $proto://$addr/restconf/operations/kalle)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"kalle"},"error-severity":"error","error-message":"RPC not defined"}}' new "restconf rpc non-existing rpc" - expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{}' $RCPROTO://localhost/restconf/operations/clixon-example:kalle)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"kalle"},"error-severity":"error","error-message":"RPC not defined"}}' + expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{}' $proto://$addr/restconf/operations/clixon-example:kalle)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"kalle"},"error-severity":"error","error-message":"RPC not defined"}}' new "restconf rpc missing name" - expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{}' $RCPROTO://localhost/restconf/operations)" 0 'HTTP/1.1 412 Precondition Failed' '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"Operation name expected"}}}' + expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{}' $proto://$addr/restconf/operations)" 0 'HTTP/1.1 412 Precondition Failed' '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"Operation name expected"}}}' new "restconf rpc missing input" - expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{}' $RCPROTO://localhost/restconf/operations/clixon-example:example)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"restconf RPC does not have input statement"}}}' + expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{}' $proto://$addr/restconf/operations/clixon-example:example)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"restconf RPC does not have input statement"}}}' new "restconf rpc using POST xml" - ret=$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -H "Accept: application/yang-data+xml" -d '{"clixon-example:input":{"x":42}}' $RCPROTO://localhost/restconf/operations/clixon-example:example) + ret=$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -H "Accept: application/yang-data+xml" -d '{"clixon-example:input":{"x":42}}' $proto://$addr/restconf/operations/clixon-example:example) expect='4242' match=`echo $ret | grep --null -Eo "$expect"` if [ -z "$match" ]; then @@ -283,10 +332,10 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPR fi new "restconf rpc using wrong prefix" - expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{"wrong:input":{"routing-instance-name":"ipv4"}}' $RCPROTO://localhost/restconf/operations/wrong:example)" 0 "HTTP/1.1 412 Precondition Failed" '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"yang module not found"}}}' + expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{"wrong:input":{"routing-instance-name":"ipv4"}}' $proto://$addr/restconf/operations/wrong:example)" 0 "HTTP/1.1 412 Precondition Failed" '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"yang module not found"}}}' new "restconf local client rpc using POST xml" - ret=$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -H "Accept: application/yang-data+xml" -d '{"clixon-example:input":{"x":"example"}}' $RCPROTO://localhost/restconf/operations/clixon-example:client-rpc) + ret=$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -H "Accept: application/yang-data+xml" -d '{"clixon-example:input":{"x":"example"}}' $proto://$addr/restconf/operations/clixon-example:client-rpc) expect='example' match=`echo $ret | grep --null -Eo "$expect"` if [ -z "$match" ]; then @@ -294,10 +343,10 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPR fi new "restconf Add subtree without key (expected error)" - expectpart "$(curl $CURLOPTS -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces/interface)" 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 =interface, expected' + expectpart "$(curl $CURLOPTS -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' $proto://$addr/restconf/data/ietf-interfaces:interfaces/interface)" 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 =interface, expected' new "restconf Add subtree with too many keys (expected error)" - expectpart "$(curl $CURLOPTS -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' $RCPROTO://localhost/restconf/data/ietf-interfaces:interfaces/interface=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":"List key interface length mismatch"}}}' + expectpart "$(curl $CURLOPTS -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' $proto://$addr/restconf/data/ietf-interfaces:interfaces/interface=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":"List key interface length mismatch"}}}' if [ $RC -ne 0 ]; then new "Kill restconf daemon" @@ -316,10 +365,25 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPR fi } -new "Use local restconf config" -testrun false - -new "Get restconf config from backend" -testrun true +# Go thru all combinations of IPv4/IPv6, http/https, local/backend config +protos="http" +if [ "${WITH_RESTCONF}" = "evhtp" ]; then + # http only relevant for evhtp (for fcgi: need nginx config) + protos="$protos https" +fi +for proto in $protos; do + for addr in 127.0.0.1 "\[::1\]"; do + configs="local" + if [ "${WITH_RESTCONF}" = "evhtp" ]; then + # backend config retrieval only implemented for evhtp + configs="$configs backend" + fi + echo "configs:$configs" + for config in $configs; do + new "restconf test: proto:$proto addr:$addr config:$config" + testrun $proto $addr $config + done + done +done rm -rf $dir diff --git a/test/test_restconf2.sh b/test/test_restconf2.sh index ea7ccd5f..f8a563dc 100755 --- a/test/test_restconf2.sh +++ b/test/test_restconf2.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash # Restconf basic functionality # Assume http server setup, such as nginx described in apps/restconf/README.md +# Systematic tests of restconf operations GET/POST/PUT/DELETE +# See also test_restconf.sh # Magic line must be first in script (see README.md) s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi diff --git a/test/test_ssl_certs.sh b/test/test_ssl_certs.sh index e984b3ab..0e1b3451 100755 --- a/test/test_ssl_certs.sh +++ b/test/test_ssl_certs.sh @@ -23,10 +23,12 @@ fyang=$dir/example.yang cfg=$dir/conf.xml +# Local for test here certdir=$dir/certs srvkey=$certdir/srv_key.pem srvcert=$certdir/srv_cert.pem cakey=$certdir/ca_key.pem # needed? + cacert=$certdir/ca_cert.pem users="andy guest" # generate certs for some users in nacm.sh @@ -117,72 +119,13 @@ EOF ) if $genkeys; then -# Create certs -# 1. CA -cat< $dir/ca.cnf -[ ca ] -default_ca = CA_default -[ CA_default ] -serial = ca-serial -crl = ca-crl.pem -database = ca-database.txt -name_opt = CA_default -cert_opt = CA_default -default_crl_days = 9999 -default_md = md5 + # Server certs + . ./certs.sh -[ req ] -default_bits = 2048 -days = 1 -distinguished_name = req_distinguished_name -attributes = req_attributes -prompt = no -output_password = password - -[ req_distinguished_name ] -C = SE -L = Stockholm -O = Clixon -OU = clixon -CN = ca -emailAddress = olof@hagsand.se - -[ req_attributes ] -challengePassword = test - -EOF - -# Generate CA cert -openssl req -x509 -days 1 -config $dir/ca.cnf -keyout $cakey -out $cacert - -cat< $dir/srv.cnf -[req] -prompt = no -distinguished_name = dn -req_extensions = ext -[dn] -CN = www.clicon.org # localhost -emailAddress = olof@hagsand.se -O = Clixon -L = Stockholm -C = SE -[ext] -subjectAltName = DNS:clicon.org -EOF - -# Generate server key -openssl genrsa -out $srvkey 2048 - -# Generate CSR (signing request) -openssl req -new -config $dir/srv.cnf -key $srvkey -out $certdir/srv_csr.pem - -# Sign server cert by CA -openssl x509 -req -extfile $dir/srv.cnf -days 1 -passin "pass:password" -in $certdir/srv_csr.pem -CA $cacert -CAkey $cakey -CAcreateserial -out $srvcert - -# create client certs -for name in $users; do - cat< $dir/$name.cnf + # create client certs + for name in $users; do + cat< $dir/$name.cnf [req] prompt = no distinguished_name = dn @@ -193,15 +136,15 @@ O = Clixon L = Stockholm C = SE EOF - # Create client key - openssl genrsa -out "$certdir/$name.key" 2048 + # Create client key + openssl genrsa -out "$certdir/$name.key" 2048 - # Generate CSR (signing request) - openssl req -new -config $dir/$name.cnf -key $certdir/$name.key -out $certdir/$name.csr + # Generate CSR (signing request) + openssl req -new -config $dir/$name.cnf -key $certdir/$name.key -out $certdir/$name.csr - # Sign by CA - openssl x509 -req -extfile $dir/$name.cnf -days 1 -passin "pass:password" -in $certdir/$name.csr -CA $cacert -CAkey $cakey -CAcreateserial -out $certdir/$name.crt -done + # Sign by CA + openssl x509 -req -extfile $dir/$name.cnf -days 1 -passin "pass:password" -in $certdir/$name.csr -CA $cacert -CAkey $cakey -CAcreateserial -out $certdir/$name.crt + done # client key fi # genkeys @@ -226,16 +169,18 @@ testrun() cat < $dir/startup_db + $authtype + true + $srvcert + $srvkey + $cacert + default
0.0.0.0
$port $ssl
- $authtype - $srvcert - $srvkey - $cacert
$RULES
@@ -259,6 +204,9 @@ EOF start_backend -s startup -f $cfg fi + new "wait for backend" + wait_backend + if [ $RC -ne 0 ]; then new "kill old restconf daemon" stop_restconf_pre @@ -271,9 +219,6 @@ EOF fi fi - new "wait for backend" - wait_backend - new "wait for restconf" wait_restconf --key $certdir/andy.key --cert $certdir/andy.crt diff --git a/yang/clixon/clixon-restconf@2020-10-30.yang b/yang/clixon/clixon-restconf@2020-10-30.yang index f4285403..ecb4d6b9 100644 --- a/yang/clixon/clixon-restconf@2020-10-30.yang +++ b/yang/clixon/clixon-restconf@2020-10-30.yang @@ -57,10 +57,6 @@ module clixon-restconf { description "PAM password auth"; } - enum none { - description - "No authentication, no security."; - } } description "Enumeration of HTTP authorization types."; @@ -91,31 +87,22 @@ module clixon-restconf { presence "Enables RESTCONF"; description "HTTP daemon configuration."; - list socket { - key "namespace address port"; - leaf namespace { - type string; - description "indicates a namespace for instance. On platforms where namespaces are not suppported, always 'default'"; - } - leaf address { - type inet:ip-address; - description "IP address to bind to"; - } - leaf port { - type inet:port-number; - description "IP port to bind to"; - } - leaf ssl { - type boolean; - default true; - description "Enable for HTTPS otherwise HTTP protocol"; - } + leaf ssl-enable { + description + "Enable ssl server functionality. + Setting to false means the following are invalid: + - auth-type=client-certificate + - socket entries with ssl=true + Also, the following are not releveant: server-cert-path, server-key-path, + server-ca-cert-path"; + type boolean; + default false; } - leaf auth-type { + leaf-list auth-type { type http-auth-type; description "The authentication type. - Note client-certificate applies only if socket has ssl enabled"; + Note client-certificate applies only if ssl-enable is true and socket has ssl"; } leaf server-cert-path { type string; @@ -141,6 +128,26 @@ module clixon-restconf { default "/etc/ssl/certs/clixon-ca_crt.pem"; /* CLICON_SSL_CA_CERT */ } + list socket { + key "namespace address port"; + leaf namespace { + type string; + description "indicates a namespace for instance. On platforms where namespaces are not suppported, always 'default'"; + } + leaf address { + type inet:ip-address; + description "IP address to bind to"; + } + leaf port { + type inet:port-number; + description "IP port to bind to"; + } + leaf ssl { + type boolean; + default true; + description "Enable for HTTPS otherwise HTTP protocol"; + } + } } rpc restconf-control { input {