diff --git a/CHANGELOG.md b/CHANGELOG.md index ddb1c506..7b0bd5f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,8 +31,13 @@ ## 5.1.0 Expected: April +### New features + + + ### API changes on existing protocol/config features +* Restconf "evhtp" mode MUST use libevhtp from https://github.com/clixon/clixon-libevhtp.git instead from criticalstack * NETCONF Hello message semantics has been made stricter according to RFC 6241 Sec 8.1, for example: * A client MUST send a element. * Each peer MUST send at least the base NETCONF capability, "urn:ietf:params:netconf:base:1.1" (or 1.0 for RFC 4741) @@ -59,6 +64,12 @@ Developers may need to change their code ### Minor features +* Updated "evhtp" restconf mode + * No reliance on libevent or libevhtp, but on libssl >= 1.1 directly + * Moved out event handling to clixon event handling + * Moved out all ssl calls to clixon + * New code MUST use libevhtp from https://github.com/clixon/clixon-libevhtp.git + * This does NOT work: libevhtp from https://github.com/criticalstack/libevhtp.git * Application specialized error handling for specific error categories * See: https://clixon-docs.readthedocs.io/en/latest/misc.html#specialized-error-handling * Added several fields to process-control status operation: active, description, command, status, starttime, pid diff --git a/LICENSE.md b/LICENSE.md index 835873ff..b0c13253 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -9,6 +9,9 @@ You choose. Except as otherwise noted, - see lib/src/clixon_sha1.c +- see apps/restconf/libevhtp_parser.[ch] + BSD 3-clause: Copyright (c) 2010-2018, Mark Ellzey, Nathan French, Marcus Sundberg + The two licenses are included below. diff --git a/apps/restconf/Makefile.in b/apps/restconf/Makefile.in index 5f5f7f84..6358e3a7 100644 --- a/apps/restconf/Makefile.in +++ b/apps/restconf/Makefile.in @@ -77,7 +77,6 @@ endif LIBDEPS = $(top_srcdir)/lib/src/$(CLIXON_LIB) LIBS = -L$(top_srcdir)/lib/src $(top_srcdir)/lib/src/$(CLIXON_LIB) @LIBS@ -#LIBS += -lpthread -levent -levent_openssl -lssl -lcrypto ifeq ($(LINKAGE),dynamic) CPPFLAGS = @CPPFLAGS@ -fPIC diff --git a/apps/restconf/restconf_api_evhtp.c b/apps/restconf/restconf_api_evhtp.c index 4c782baf..e36322b1 100644 --- a/apps/restconf/restconf_api_evhtp.c +++ b/apps/restconf/restconf_api_evhtp.c @@ -32,29 +32,25 @@ ***** END LICENSE BLOCK ***** - * Concrete functions for libevhtp of the + * Concrete functions for openssl of the * Virtual clixon restconf API functions. * @see restconf_api.h for virtual API */ -#include -#include +#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 +#include +#include +#include /* evhtp */ #include -#include /* cligen */ #include @@ -65,15 +61,6 @@ #include "restconf_lib.h" #include "restconf_api.h" /* Virtual api */ -/* evhtp_safe_free is a macro that may not be present in a libevhtp release - */ -#ifndef evhtp_safe_free -#define evhtp_safe_free(_var, _freefn) do { \ - _freefn((_var)); \ - (_var) = NULL; \ -} while (0) -#endif - /*! Add HTTP header field name and value to reply, evhtp specific * @param[in] req Evhtp http request handle * @param[in] name HTTP header field name @@ -85,7 +72,6 @@ restconf_reply_header(void *req0, const char *name, const char *vfmt, ...) - { evhtp_request_t *req = (evhtp_request_t *)req0; int retval = -1; @@ -139,8 +125,9 @@ restconf_reply_send(void *req0, { evhtp_request_t *req = (evhtp_request_t *)req0; int retval = -1; - struct evbuffer *eb = NULL; const char *reason_phrase; + evhtp_connection_t *conn; + struct evbuffer *eb = NULL; req->status = code; if ((reason_phrase = restconf_code2reason(code)) == NULL) @@ -149,17 +136,10 @@ restconf_reply_send(void *req0, if (restconf_reply_header(req, "Status", "%d %s", code, reason_phrase) < 0) goto done; #endif -#if 0 /* Optional? */ - { - evhtp_connection_t *conn; - - if ((conn = evhtp_request_get_connection(req)) == NULL){ - clicon_err(OE_DAEMON, EFAULT, "evhtp_request_get_connection"); - goto done; - } - htp_sslutil_add_xheaders(req->headers_out, conn->ssl, HTP_SSLUTILS_XHDR_ALL); + if ((conn = evhtp_request_get_connection(req)) == NULL){ + clicon_err(OE_DAEMON, EFAULT, "evhtp_request_get_connection"); + goto done; } -#endif /* If body, add a content-length header */ if (cb != NULL && cbuf_len(cb)){ @@ -167,14 +147,13 @@ restconf_reply_send(void *req0, if (restconf_reply_header(req, "Content-Length", "%d", cbuf_len(cb)) < 0) goto done; } - /* create evbuffer* : bufferevent_write_buffer/ drain, - ie send everything , except body */ - evhtp_send_reply_start(req, req->status); + evhtp_send_reply(req, req->status); + /* Write a body if cbuf is nonzero */ if (cb != NULL && cbuf_len(cb)){ /* Suboptimal, copy from cbuf to evbuffer */ if ((eb = evbuffer_new()) == NULL){ - clicon_err(OE_CFG, errno, "evbuffer_new"); + clicon_err(OE_RESTCONF, errno, "evbuffer_new"); goto done; } if (evbuffer_add(eb, cbuf_get(cb), cbuf_len(cb)) < 0){ @@ -217,3 +196,4 @@ restconf_get_indata(void *req0) return cb; } + diff --git a/apps/restconf/restconf_api_fcgi.c b/apps/restconf/restconf_api_fcgi.c index 88981bf8..c101e1d0 100644 --- a/apps/restconf/restconf_api_fcgi.c +++ b/apps/restconf/restconf_api_fcgi.c @@ -37,6 +37,10 @@ * @see restconf_api.h for virtual API */ +#ifdef HAVE_CONFIG_H +#include "clixon_config.h" /* generated by config & autoconf */ +#endif + #include #include #include diff --git a/apps/restconf/restconf_err.c b/apps/restconf/restconf_err.c index 591c0974..9f32d020 100644 --- a/apps/restconf/restconf_err.c +++ b/apps/restconf/restconf_err.c @@ -36,6 +36,10 @@ * @see RFC 7231 Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content */ +#ifdef HAVE_CONFIG_H +#include "clixon_config.h" /* generated by config & autoconf */ +#endif + #include #include #include diff --git a/apps/restconf/restconf_lib.c b/apps/restconf/restconf_lib.c index 8881d5f7..def64fe2 100644 --- a/apps/restconf/restconf_lib.c +++ b/apps/restconf/restconf_lib.c @@ -38,6 +38,10 @@ * altogether? */ +#ifdef HAVE_CONFIG_H +#include "clixon_config.h" /* generated by config & autoconf */ +#endif + #include #include #include @@ -52,6 +56,7 @@ #include #include #include +#include /* cligen */ #include @@ -256,8 +261,8 @@ restconf_terminate(clicon_handle h) xpath_optimize_exit(); restconf_handle_exit(h); clixon_err_exit(); - clicon_log_exit(); clicon_debug(1, "%s done", __FUNCTION__); + clicon_log_exit(); /* Must be after last clicon_debug */ return 0; } @@ -585,7 +590,7 @@ restconf_authentication_cb(clicon_handle h, goto done; } -/*! Basic config init +/*! 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 Error @@ -636,3 +641,171 @@ restconf_config_init(clicon_handle h, retval = 0; goto done; } + +/*! Create and bind restconf socket + * + * @param[in] netns0 Network namespace, special value "default" is same as NULL + * @param[in] addr 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) + */ +int +restconf_socket_init(const char *netns0, + const char *addr, + const char *addrtype, + uint16_t port, + int backlog, + int flags, + int *ss) +{ + int retval = -1; + struct sockaddr * sa; + struct sockaddr_in6 sin6 = { 0 }; + struct sockaddr_in sin = { 0 }; + size_t sin_len; + const char *netns; + + clicon_debug(1, "%s %s %s %s %hu", __FUNCTION__, netns0, addrtype, addr, port); + /* netns default -> NULL */ + if (netns0 != NULL && strcmp(netns0, "default")==0) + netns = NULL; + else + netns = netns0; + if (strcmp(addrtype, "inet:ipv6-address") == 0) { + sin_len = sizeof(struct sockaddr_in6); + sin6.sin6_port = htons(port); + sin6.sin6_family = AF_INET6; + + 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; + } + if (clixon_netns_socket(netns, sa, sin_len, backlog, flags, ss) < 0) + goto done; + clicon_debug(1, "%s ss=%d", __FUNCTION__, *ss); + retval = 0; + done: + clicon_debug(1, "%s %d", __FUNCTION__, retval); + return retval; +} + +/*! 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 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 + */ +int +restconf_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; + 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"); + goto done; + } + *namespace = xml_body(x); + if ((x = xpath_first(xs, nsc, "address")) == NULL){ + clicon_err(OE_XML, EINVAL, "Mandatory address not given"); + goto done; + } + /* 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; + /* This is YANG type name of ip-address: + * typedef ip-address { + * type union { + * type inet:ipv4-address; <--- + * type inet:ipv6-address; <--- + * } + */ + *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){ + clicon_err(OE_XML, errno, "parse_uint16"); + goto done; + } + if (ret == 0){ + clicon_err(OE_XML, EINVAL, "Unrecognized value of port: %s", str); + goto done; + } + } + if ((x = xpath_first(xs, nsc, "ssl")) != NULL && + (str = xml_body(x)) != NULL){ + /* XXX use parse_bool but it is legacy static */ + if (strcmp(str, "false") == 0) + *ssl = 0; + else if (strcmp(str, "true") == 0) + *ssl = 1; + else { + clicon_err(OE_XML, EINVAL, "Unrecognized value of ssl: %s", str); + goto done; + } + } + retval = 0; + done: + if (cv) + cv_free(cv); + if (reason) + free(reason); + return retval; +} diff --git a/apps/restconf/restconf_lib.h b/apps/restconf/restconf_lib.h index 3c8354a1..a1e5187a 100644 --- a/apps/restconf/restconf_lib.h +++ b/apps/restconf/restconf_lib.h @@ -84,6 +84,8 @@ char *restconf_uripath(clicon_handle h); int restconf_drop_privileges(clicon_handle h, char *user); int restconf_authentication_cb(clicon_handle h, void *req, int pretty, restconf_media media_out); int restconf_config_init(clicon_handle h, cxobj *xrestconf); +int restconf_socket_init(const char *netns0, const char *addr, const char *addrtype, uint16_t port, int backlog, int flags, int *ss); +int restconf_socket_extract(clicon_handle h, cxobj *xs, cvec *nsc, char **namespace, char **address, char **addrtype, uint16_t *port, uint16_t *ssl); #endif /* _RESTCONF_LIB_H_ */ diff --git a/apps/restconf/restconf_main_evhtp.c b/apps/restconf/restconf_main_evhtp.c index 0de3ab77..74904271 100644 --- a/apps/restconf/restconf_main_evhtp.c +++ b/apps/restconf/restconf_main_evhtp.c @@ -32,7 +32,89 @@ ***** END LICENSE BLOCK ***** - * libevhtp code + * libssl 1.1 API + * Data structures: + * 1 1 + * +--------------------+ restconf_handle_get +--------------------+ + * | rh restconf_handle | <--------------------- | h clicon_handle | + * +--------------------+ +--------------------+ + * common SSL config \ ^ + * \ | n + * \ rh_sockets +--------------------+ + * +-----------> | rs restconf_socket | + * +--------------------+ + * n per-socket SSL config + * +--------------------+ + * | rr restconf_request| per-packet + * +--------------------+ + * Note restconf_request may need to be extended eg backpointer to rs? + * + * +--------------------+ +--------------------+ + * | evhtp_connection | -----> | evhtp_t (core) | + * +--------------------+ +--------------------+ + * |request ^ + * v | conn + * +--------------------+ +--------------------+ + * | evhtp_request | --> uri | evhtp_uri | + * +--------------------+ +--------------------+ + * | (created by parser) | | | + * v v v v + * headers/buffers/method/... authority path query + * + * Buffer handling: + * c + * | + * +--------------------+ + * | bufferevent | + * +--------------------+ + * | | + * input output + * + * Three special cases and expected (typical replies): + * + * Note (1) http to https port: + * olof@alarik> curl -Ssik -X GET http://www.hagsand.se:443 + * HTTP/1.1 400 Bad Request + * Server: nginx + * Date: Tue, 30 Mar 2021 06:46:34 GMT + * Content-Type: text/html + * Content-Length: 248 + * Connection: close + * + * + * 400 The plain HTTP request was sent to HTTPS port + * + *

400 Bad Request

+ *
The plain HTTP request was sent to HTTPS port
+ *
nginx
+ * + * + * + * Note (2) https to http port: + * olof@alarik> curl -Ssik -X GET https://www.hagsand.se:80 + * curl: (35) error:1408F10B:SSL routines:ssl3_get_record:wrong version number + * + * Note (3) client-cert is NULL: + * curl -i -m 40 -k -H 'Content-Type: application/yang-data+json' https://10.255.10.1/restconf/data/netgate-system:system/auth + * HTTP/2 400 + * [1mserver[0m: nginx/1.16.1 + * [1mdate[0m: Sun, 28 Feb 2021 17:34:08 GMT + * [1mcontent-type[0m: text/html + * [1mcontent-length[0m: 237 + * + * 400 No required SSL certificate was sent + * + *

400 Bad Request

+ *
No required SSL certificate was sent
+ *
nginx/1.16.1
+ * + * + * + * Example links regarding certificate verification + * https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_verify.html + * http://fm4dd.com/openssl/certverify.htm + * https://wiki.openssl.org/index.php/Simple_TLS_Server + * https://wiki.openssl.org/index.php/Hostname_validation */ #ifdef HAVE_CONFIG_H @@ -45,28 +127,21 @@ * Default in testing is disabled threads. */ -#include -#include +#include #include -#include -#include +#include #include -#include -#include -#include -#include #include -#include -#include +#include #include -#include /* chmod */ +#include #include -#include #include -/* evhtp */ -#include -#include +#include +#include +#include +#include /* cligen */ #include @@ -74,90 +149,118 @@ /* clicon */ #include -/* restconf */ +/* evhtp */ +#include /* evbuffer */ +#include +#include /* XXX inline this / use SSL directly */ +/* restconf */ #include "restconf_lib.h" /* generic shared with plugins */ #include "restconf_handle.h" #include "restconf_api.h" /* generic not shared with plugins */ #include "restconf_err.h" #include "restconf_root.h" +#include "restconf_openssl.h" /* Restconf-openssl mode specific headers*/ /* Command line options to be passed to getopt(3) */ #define RESTCONF_OPTS "hD:f:E:l:p:y:a:u:ro:" /* See see listen(5) */ -#define SOCKET_LISTEN_BACKLOG 16 +#define SOCKET_LISTEN_BACKLOG 8 -/* Clixon evhtp handle - * Global data about evhtp lib, - * See evhtp_request_t *req for per-message state data +/* Cert verify depth */ +#define VERIFY_DEPTH 1 + +/* Forward */ +static int restconf_connection(int s, void* arg); + +/*! Get restconf openssl global handle + * @param[in] h Clicon handle + * @retval rh Openssl global handle */ -typedef struct { - clicon_handle eh_h; - 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; - -/* Need this global to pass to signal handler - * XXX Try to get rid of code in signal handler - */ -static cx_evhtp_handle *_EVHTP_HANDLE = NULL; - -/* Need global variable to for signal handler XXX */ -static clicon_handle _CLICON_HANDLE = NULL; - -static void -evhtp_terminate(cx_evhtp_handle *eh) +static restconf_handle * +restconf_handle_get(clicon_handle h) { - evhtp_ssl_cfg_t *sc; - int i; - - if (eh == NULL) - return; - 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); - if ((sc = eh->eh_ssl_config) != NULL){ - if (sc->cafile) - free(sc->cafile); - if (sc->pemfile) - free(sc->pemfile); - if (sc->privfile) - free(sc->privfile); - free(sc); - } - free(eh); + clicon_hash_t *cdat = clicon_data(h); + size_t len; + void *p; + + if ((p = clicon_hash_value(cdat, "restconf_openssl_handle", &len)) != NULL) + return *(restconf_handle **)p; + return NULL; } -/*! Signall terminates process - * XXX Try to get rid of code in signal handler -> so we can get rid of global variables +/*! Set restconf openssl global handle + * @param[in] h Clicon handle + * @param[in] rh Openssl handle (malloced pointer) + * @see clicon_config_yang_set for the configuration yang */ -static void -restconf_sig_term(int arg) +static int +openspec_handle_set(clicon_handle h, + restconf_handle *rh) { - static int i=0; + clicon_hash_t *cdat = clicon_data(h); - if (i++ == 0) - clicon_log(LOG_NOTICE, "%s: %s: pid: %u Signal %d", - __PROGRAM__, __FUNCTION__, getpid(), arg); - else - exit(-1); - if (_EVHTP_HANDLE) /* global */ - evhtp_terminate(_EVHTP_HANDLE); - if (_CLICON_HANDLE){ /* could be replaced by eh->eh_h */ - // stream_child_freeall(_CLICON_HANDLE); - restconf_terminate(_CLICON_HANDLE); + /* It is the pointer to ys that should be copied by hash, + so we send a ptr to the ptr to indicate what to copy. + */ + if (clicon_hash_add(cdat, "restconf_openssl_handle", &rh, sizeof(rh)) == NULL) + return -1; + return 0; +} + +/* Write evbuf to socket + * see also this function in restcont_api_openssl.c + */ +static ssize_t +evbuf_write(struct evbuffer *ev, + int s, + SSL *ssl) +{ + ssize_t retval = -1; + size_t buflen; + unsigned char *buf; + int ret; + + if ((buflen = evbuffer_get_length(ev)) > 0){ + buf = evbuffer_pullup(ev, -1); + if (ssl){ + if ((ret = SSL_write(ssl, buf, buflen)) <= 0){ + clicon_err(OE_SSL, 0, "SSL_write"); + goto done; + } + } + else{ + if (write(s, buf, buflen) < 0){ + clicon_err(OE_UNIX, errno, "write"); + goto done; + } + } } - clicon_exit_set(); /* XXX should rather signal event_base_loop */ - exit(-1); + retval = 0; + done: + return retval; +} + +/* util function to append log string + */ +static int +print_cb(const char *str, size_t len, void *cb) +{ + return cbuf_append_str((cbuf*)cb, (char*)str); /* Assume string */ +} + +/* Clixon error category log callback + * @param[in] handle Application-specific handle + * @param[out] cb Read log/error string into this buffer + */ +static int +openssl_cat_log_cb(void *handle, + cbuf *cb) +{ + clicon_debug(1, "%s", __FUNCTION__); + ERR_print_errors_cb(print_cb, cb); + return 0; } static char* @@ -218,6 +321,14 @@ evhtp_method2str(enum htp_method m) } } +static int +print_header(evhtp_header_t *header, + void *arg) +{ + clicon_debug(1, "%s %s %s", __FUNCTION__, header->key, header->val); + return 0; +} + static int query_iterator(evhtp_header_t *hdr, void *arg) @@ -314,7 +425,6 @@ evhtp_params_set(clicon_handle h, cvec *cvv = NULL; char *cn; - if ((uri = req->uri) == NULL){ clicon_err(OE_DAEMON, EFAULT, "No uri"); goto done; @@ -377,123 +487,37 @@ evhtp_params_set(clicon_handle h, goto done; } -static int -print_header(evhtp_header_t *header, - void *arg) -{ - // clicon_handle h = (clicon_handle)arg; - - clicon_debug(1, "%s %s %s", - __FUNCTION__, header->key, header->val); - return 0; -} - -static evhtp_res -cx_pre_accept(evhtp_connection_t *conn, - void *arg) -{ - // clicon_handle h = (clicon_handle)arg; - - clicon_debug(1, "%s", __FUNCTION__); - return EVHTP_RES_OK; -} - -static evhtp_res -cx_post_accept(evhtp_connection_t *conn, - void *arg) -{ - // clicon_handle h = (clicon_handle)arg; - - clicon_debug(1, "%s", __FUNCTION__); - return EVHTP_RES_OK; -} - -/*! Generic callback called if no other callbacks are matched - */ -static void -cx_gencb(evhtp_request_t *req, - void *arg) -{ - evhtp_connection_t *conn; - // clicon_handle h = arg; - - clicon_debug(1, "%s", __FUNCTION__); - if (req == NULL){ - errno = EINVAL; - return; - } - if ((conn = evhtp_request_get_connection(req)) == NULL) - goto done; - htp_sslutil_add_xheaders( - req->headers_out, - conn->ssl, - HTP_SSLUTILS_XHDR_ALL); - evhtp_send_reply(req, EVHTP_RES_NOTFOUND); - done: - return; /* void */ -} - -/*! /.well-known callback - * @see cx_genb - */ -static void -cx_path_wellknown(evhtp_request_t *req, - void *arg) -{ - int retval = -1; - cx_evhtp_handle *eh = (cx_evhtp_handle*)arg; - clicon_handle h = eh->eh_h; - int ret; - - clicon_debug(1, "------------"); - /* input debug */ - if (clicon_debug_get()) - evhtp_headers_for_each(req->headers_in, print_header, h); - /* get accepted connection */ - - /* set fcgi-like paramaters (ignore query vector) */ - if ((ret = evhtp_params_set(h, req, NULL)) < 0) - goto done; - if (ret == 1){ - /* call generic function */ - if (api_well_known(h, req) < 0) - goto done; - } - /* Clear (fcgi) paramaters from this request */ - if (restconf_param_del_all(h) < 0) - goto done; - retval = 0; - done: - /* Catch all on fatal error. This does not terminate the process but closes request stream */ - if (retval < 0) - evhtp_send_reply(req, EVHTP_RES_ERROR); - return; /* void */ -} - /*! Callback for each incoming http request for path / * * This are all messages except /.well-known, Registered with evhtp_set_cb * - * @param[in] req evhtp request structure defining the incoming message + * @param[in] req evhtp http request structure defining the incoming message * @param[in] arg cx_evhtp handle clixon specific fields * @retval void * Discussion: problematic if fatal error -1 is returneod from clixon routines * without actually terminating. Consider: * 1) sending some error? and/or * 2) terminating the process? - * @see cx_genb */ static void -cx_path_restconf(evhtp_request_t *req, - void *arg) +restconf_path_root(evhtp_request_t *req, + void *arg) { - int retval = -1; - cx_evhtp_handle *eh = (cx_evhtp_handle*)arg; - clicon_handle h = eh->eh_h; - int ret; - cvec *qvec = NULL; + int retval = -1; + clicon_handle h; + evhtp_connection_t *conn; + int ret; + cvec *qvec = NULL; clicon_debug(1, "------------"); + if ((h = (clicon_handle)arg) == NULL){ + clicon_err(OE_RESTCONF, EINVAL, "arg is NULL"); + goto done; + } + if ((conn = req->conn) == NULL){ + clicon_err(OE_RESTCONF, EINVAL, "req->conn is NULL"); + goto done; + } /* input debug */ if (clicon_debug_get()) evhtp_headers_for_each(req->headers_in, print_header, h); @@ -519,56 +543,79 @@ cx_path_restconf(evhtp_request_t *req, retval = 0; done: /* Catch all on fatal error. This does not terminate the process but closes request stream */ - if (retval < 0) + if (retval < 0){ evhtp_send_reply(req, EVHTP_RES_ERROR); + } if (qvec) cvec_free(qvec); return; /* void */ } -/*! Get Server cert ssl info - * @param[in] h Clicon handle - * @param[in] server_cert_path Path to server ssl cert file - * @param[in] server_key_path Path to server ssl key file - * @param[in,out] ssl_config evhtp ssl config struct - * @retval 0 OK - * @retval -1 Error +/*! /.well-known callback + * + * @param[in] req evhtp http request structure defining the incoming message + * @param[in] arg cx_evhtp handle clixon specific fields + * @retval void + */ +static void +restconf_path_wellknown(evhtp_request_t *req, + void *arg) +{ + int retval = -1; + clicon_handle h; + evhtp_connection_t *conn; + int ret; + + clicon_debug(1, "------------"); + if ((h = (clicon_handle)arg) == NULL){ + clicon_err(OE_RESTCONF, EINVAL, "arg is NULL"); + goto done; + } + if ((conn = req->conn) == NULL){ + clicon_err(OE_RESTCONF, EINVAL, "req->conn is NULL"); + goto done; + } + /* input debug */ + if (clicon_debug_get()) + evhtp_headers_for_each(req->headers_in, print_header, h); + /* get accepted connection */ + + /* set fcgi-like paramaters (ignore query vector) */ + if ((ret = evhtp_params_set(h, req, NULL)) < 0) + goto done; + if (ret == 1){ + /* call generic function */ + if (api_well_known(h, req) < 0) + goto done; + } + /* Clear (fcgi) paramaters from this request */ + if (restconf_param_del_all(h) < 0) + goto done; + retval = 0; + done: + /* Catch all on fatal error. This does not terminate the process but closes request stream */ + if (retval < 0){ + evhtp_send_reply(req, EVHTP_RES_ERROR); + } + return; /* void */ +} + +/* + * see restconf_config ->cv_evhtp_init(x2) -> cx_evhtp_socket -> + * evhtp_ssl_init:4757 */ static int -cx_get_ssl_server_certs(clicon_handle h, - const char *server_cert_path, - const char *server_key_path, - evhtp_ssl_cfg_t *ssl_config) -{ - int retval = -1; - struct stat f_stat; +init_openssl(void) +{ + int retval = -1; - if (ssl_config == NULL){ - clicon_err(OE_CFG, EINVAL, "ssl_config is NULL"); - goto done; - } - if (server_cert_path == NULL){ - clicon_err(OE_CFG, EINVAL, "server_cert_path is not set but is required when ssl is enabled"); - goto done; - } - if (server_key_path == NULL){ - clicon_err(OE_CFG, EINVAL, "server_key_path is not set but is required when ssl is enabled"); - goto done; - } - if ((ssl_config->pemfile = strdup(server_cert_path)) == NULL){ - clicon_err(OE_CFG, errno, "strdup"); - goto done; - } - if (stat(ssl_config->pemfile, &f_stat) != 0) { - clicon_err(OE_FATAL, errno, "Cannot load SSL cert '%s'", ssl_config->pemfile); - goto done; - } - if ((ssl_config->privfile = strdup(server_key_path)) == NULL){ - clicon_err(OE_CFG, errno, "strdup"); - goto done; - } - if (stat(ssl_config->privfile, &f_stat) != 0) { - clicon_err(OE_FATAL, errno, "Cannot load SSL key '%s'", ssl_config->privfile); + /* In Openssl 1.1 lib inits itself (?) + * eg SSL_load_error_strings(); + */ + /* This isn't strictly necessary... OpenSSL performs RAND_poll + * automatically on first use of random number generator. */ + if (RAND_poll() != 1) { + clicon_err(OE_SSL, errno, "Random generator has not been seeded with enough data"); goto done; } retval = 0; @@ -576,403 +623,695 @@ cx_get_ssl_server_certs(clicon_handle h, return retval; } -/*! Get client ssl cert info - * @param[in] h Clicon handle - * @param[in] server_ca_cert_path Path to server ssl CA file for client certs - * @param[in,out] ssl_config evhtp ssl config struct - * @retval 0 OK - * @retval -1 Error +/*! + * The verify_callback function is used to control the behaviour when the SSL_VERIFY_PEER flag + * is set. It must be supplied by the application and receives two arguments: preverify_ok + * indicates, whether the verification of the certificate in question was passed + * (preverify_ok=1) or not (preverify_ok=0). x509_ctx is a pointer to the complete context + * used for the certificate chain verification. */ static int -cx_get_ssl_client_ca_certs(clicon_handle h, - const char *server_ca_cert_path, - evhtp_ssl_cfg_t *ssl_config) +restconf_verify_certs(int preverify_ok, + evhtp_x509_store_ctx_t *store) { - int retval = -1; - struct stat f_stat; - - if (ssl_config == NULL || server_ca_cert_path == NULL){ - clicon_err(OE_CFG, EINVAL, "Input parameter is NULL"); - goto done; - } - if ((ssl_config->cafile = strdup(server_ca_cert_path)) == NULL){ - clicon_err(OE_CFG, errno, "strdup"); - goto done; - } - if (stat(ssl_config->cafile, &f_stat) != 0) { - clicon_err(OE_FATAL, errno, "Cannot load SSL key '%s'", ssl_config->privfile); - goto done; - } - retval = 0; - done: - return retval; -} - -static int -cx_verify_certs(int pre_verify, - evhtp_x509_store_ctx_t *store) -{ -#if 0 //def NOTYET - char buf[256]; - X509 * err_cert; - int err; - int depth; - SSL * ssl; - evhtp_connection_t * connection; - evhtp_ssl_cfg_t * ssl_cfg; + char buf[256]; + X509 *err_cert; + int err; + int depth; + // SSL *ssl; + // clicon_handle h; - fprintf(stderr, "%s %d\n", __FUNCTION__, pre_verify); - + clicon_debug(1, "%s %d", __FUNCTION__, preverify_ok); err_cert = X509_STORE_CTX_get_current_cert(store); err = X509_STORE_CTX_get_error(store); depth = X509_STORE_CTX_get_error_depth(store); - ssl = X509_STORE_CTX_get_ex_data(store, SSL_get_ex_data_X509_STORE_CTX_idx()); + // ssl = X509_STORE_CTX_get_ex_data(store, SSL_get_ex_data_X509_STORE_CTX_idx()); + clicon_debug(1, "%s preverify_ok:%d err:%d depth:%d", __FUNCTION__, preverify_ok, err, depth); X509_NAME_oneline(X509_get_subject_name(err_cert), buf, 256); - - connection = SSL_get_app_data(ssl); - ssl_cfg = connection->htp->ssl_cfg; -#endif - return pre_verify; + switch (err){ + case X509_V_ERR_HOSTNAME_MISMATCH: + clicon_debug(1, "%s X509_V_ERR_HOSTNAME_MISMATCH", __FUNCTION__); + break; + } + /* Catch a too long certificate chain. should be +1 in SSL_CTX_set_verify_depth() */ + if (depth > 1) { + preverify_ok = 0; + err = X509_V_ERR_CERT_CHAIN_TOO_LONG; + X509_STORE_CTX_set_error(store, err); + } + if (depth == VERIFY_DEPTH){ + /* Verify the CA name */ + + } + // h = SSL_get_app_data(ssl); + return preverify_ok; } -/*! Create and bind restconf socket - * - * @param[in] netns0 Network namespace, special value "default" is same as NULL - * @param[in] addr 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[out] ss Server socket (bound for accept) +static int session_id_context = 1; + +/* + * see restconf_config ->cv_evhtp_init(x2) -> cx_evhtp_socket -> + * evhtp_ssl_init:4794 + */ +static SSL_CTX * +restconf_ssl_context_create(clicon_handle h) +{ + const SSL_METHOD *method; + SSL_CTX *ctx = NULL; + + method = TLS_server_method(); + + if ((ctx = SSL_CTX_new(method)) == NULL) { + clicon_err(OE_SSL, 0, "SSL_CTX_new"); + goto done; + } + /* Options + * As of OpenSSL 1.0.2g the SSL_OP_NO_SSLv2 option is set by default. * + * It is recommended that applications should set SSL_OP_NO_SSLv3. + * The question is which TLS to disable + * SSL_OP_NO_TLSv1, SSL_OP_NO_TLSv1_1, SSL_OP_NO_TLSv1_2 + */ + SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1); + + SSL_CTX_set_options(ctx, SSL_MODE_RELEASE_BUFFERS | SSL_OP_NO_COMPRESSION); + // SSL_CTX_set_timeout(ctx, cfg->ssl_ctx_timeout); /* default 300s */ + done: + return ctx; +} + +/* + * see restconf_config ->cv_evhtp_init(x2) -> cx_evhtp_socket -> + * evhtp_ssl_init: 4886 + * @param[in] ctx SSL context + * @param[in] server_cert_path Server cert + * @param[in] server_key_path Server private key + * @param[in] server_ca_cert_path CA cert Only if auth-type = client cert + * @see restconf_ssl_context_create */ static int -restconf_socket_init(const char *netns0, - const char *addr, - const char *addrtype, - uint16_t port, - int *ss) +restconf_ssl_context_configure(clixon_handle h, + SSL_CTX *ctx, + const char *server_cert_path, + const char *server_key_path, + const char *server_ca_cert_path) +{ + int retval = -1; + + SSL_CTX_set_ecdh_auto(ctx, 1); + + /* Specifies the locations where CA certificates are located. The certificates available via + * CAfile and CApath are trusted. + * retval= 0: The operation failed because CAfile and CApath are NULL or the + * processing at one of the locations specified failed. Check the error stack to + * find out the reason. + */ + if (server_ca_cert_path){ + if (SSL_CTX_load_verify_locations(ctx, server_ca_cert_path, NULL) != 1){ + clicon_err(OE_SSL, 0, "SSL_CTX_load_verify_locations(%s)", server_ca_cert_path); + goto done; + } + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER /*| SSL_VERIFY_FAIL_IF_NO_PEER_CERT */, + restconf_verify_certs); + SSL_CTX_set_verify_depth(ctx, VERIFY_DEPTH+1); + } + + + X509_STORE_set_flags(SSL_CTX_get_cert_store(ctx), 0); + + SSL_CTX_set_session_id_context(ctx, (void *)&session_id_context, sizeof(session_id_context)); + SSL_CTX_set_app_data(ctx, h); + SSL_CTX_set_session_cache_mode(ctx, 0); + + /* Set the key and cert */ + if (SSL_CTX_use_certificate_chain_file(ctx, server_cert_path) != 1) { + ERR_print_errors_fp(stderr); + goto done; + } + + if (SSL_CTX_use_PrivateKey_file(ctx, + server_key_path, + SSL_FILETYPE_PEM) <= 0 ) { + ERR_print_errors_fp(stderr); + goto done; + } + + retval = 0; + done: + return retval; +} + +static int +close_openssl_socket(int s, + SSL *ssl) +{ + int retval = -1; + int ret; + + if (ssl){ + // SSL_set_shutdown(ssl, SSL_RECEIVED_SHUTDOWN); + /* SSL_shutdown() must not be called if a previous fatal error has occurred on a connection + * i.e. if SSL_get_error() has returned SSL_ERROR_SYSCALL or SSL_ERROR_SSL. + */ + if ((ret = SSL_shutdown(ssl)) < 0){ + int e = SSL_get_error(ssl, ret); + clicon_err(OE_SSL, 0, "SSL_shutdown, err:%d", e); + goto done; + } + SSL_free(ssl); + } + close(s); + clixon_event_unreg_fd(s, restconf_connection); + retval = 0; + done: + return retval; +} + +/*! New data connection after accept, receive and reply on data sockte + * + * @param[in] s Socket where message arrived. read from this. + * @param[in] arg Client entry (from). + * @retval 0 OK + * @retval -1 Error Terminates backend and is never called). Instead errors are + * propagated back to client. + * @see restconf_accept_client where this callback is registered + */ +static int +restconf_connection(int s, + void* arg) { int retval = -1; - struct sockaddr * sa; - struct sockaddr_in6 sin6 = { 0 }; - struct sockaddr_in sin = { 0 }; - size_t sin_len; - const char *netns; + evhtp_connection_t *conn = NULL; + size_t n; + char buf[1024]; + SSL *ssl; + clicon_handle h; - clicon_debug(1, "%s %s %s %s", __FUNCTION__, netns0, addrtype, addr); - /* netns default -> NULL */ - if (netns0 != NULL && strcmp(netns0, "default")==0) - netns = NULL; - else - netns = netns0; - if (strcmp(addrtype, "inet:ipv6-address") == 0) { - sin_len = sizeof(struct sockaddr_in6); - sin6.sin6_port = htons(port); - sin6.sin6_family = AF_INET6; - - inet_pton(AF_INET6, addr, &sin6.sin6_addr); - sa = (struct sockaddr *)&sin6; + clicon_debug(1, "%s", __FUNCTION__); + if ((conn = (evhtp_connection_t*)arg) == NULL){ + clicon_err(OE_RESTCONF, EINVAL, "arg is NULL"); + goto done; } - 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; + h = (clicon_handle*)conn->htp->arg; + /* Example: curl -Ssik -u wilma:bar -X GET https://localhost/restconf/data/example:x */ + if (conn->ssl){ + /* Non-ssl gets n == 0 here! + curl -Ssik --key /var/tmp/./test_restconf_ssl_certs.sh/certs/limited.key --cert /var/tmp/./test_restconf_ssl_certs.sh/certs/limited.crt -X GET https://localhost/restconf/data/example:x +*/ + if ((n = SSL_read(conn->ssl, buf, sizeof(buf))) < 0){ + clicon_err(OE_XML, errno, "SSL_read"); + goto done; + } } else{ - clicon_err(OE_XML, EINVAL, "Unexpected addrtype: %s", addrtype); - return -1; + if ((n = read(conn->sock, buf, sizeof(buf))) < 0){ /* XXX atomicio ? */ + clicon_err(OE_XML, errno, "SSL_read"); + goto done; + } } - if (clixon_netns_socket(netns, sa, sin_len, SOCKET_LISTEN_BACKLOG, ss) < 0) - goto done; + if (n == 0){ + ssl = conn->ssl; + conn->ssl = NULL; + evhtp_connection_free(conn); /* evhtp */ + if (close_openssl_socket(s, ssl) < 0) + goto done; + goto ok; + } + /* parse incoming packet + * signature: + */ + if (connection_parse_nobev(buf, n, conn) < 0){ + evhtp_request_t *er; + er = evhtp_request_new(NULL, NULL); + er->conn = conn; + conn->request = er; + htparser_set_major(conn->parser, '\1'); + htparser_set_minor(conn->parser, '\1'); + if (restconf_badrequest(h, er) < 0) + goto done; + if (conn->bev != NULL) + if (evbuf_write(bufferevent_get_output(conn->bev), s, NULL) < 0) + goto done; + evhtp_connection_free(conn); + if (close_openssl_socket(s, NULL) < 0) + goto done; + goto ok; + } + if (conn->bev != NULL){ + /* This is for 100 Continue, typically bev is set but output is empty + * if sent by fini callback + */ + if (evbuf_write(bufferevent_get_output(conn->bev), conn->sock, conn->ssl) < 0) + goto done; + } + ok: retval = 0; done: - clicon_debug(1, "%s %d", __FUNCTION__, retval); + clicon_debug(1, "%s retval %d", __FUNCTION__, retval); return retval; } -/*! Usage help routine - * @param[in] argv0 command line - * @param[in] h Clicon handle - */ -static void -usage(clicon_handle h, - char *argv0) - -{ - fprintf(stderr, "usage:%s [options]\n" - "where options are\n" - "\t-h \t\t Help\n" - "\t-D \t Debug level, overrides any config debug setting\n" - "\t-f \t Configuration file (mandatory)\n" - "\t-E \t Extra configuration file directory\n" - "\t-l > \t Log on (s)yslog, (f)ile (syslog is default)\n" - "\t-p \t Yang directory path (see CLICON_YANG_DIR)\n" - "\t-y \t Load yang spec file (override yang main module)\n" - "\t-a UNIX|IPv4|IPv6 Internal backend socket family\n" - "\t-u \t Internal socket domain path or IP addr (see -a)\n" - "\t-r \t\t Do not drop privileges if run as root\n" - "\t-o