From 1336a4ccfbb7ff249a2deb5e613b426295c387f5 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Sun, 19 May 2024 17:44:00 +0200 Subject: [PATCH] RESTCONF notification for native mode --- CHANGELOG.md | 3 + apps/restconf/Makefile.in | 2 - apps/restconf/README.md | 25 ++- apps/restconf/restconf_api.c | 67 ------ apps/restconf/restconf_http1.c | 4 - apps/restconf/restconf_native.c | 9 +- apps/restconf/restconf_native.h | 5 +- apps/restconf/restconf_nghttp2.c | 6 - apps/restconf/restconf_stream.c | 122 ++++++++++- apps/restconf/restconf_stream.h | 7 +- apps/restconf/restconf_stream_fcgi.c | 261 ++++++++--------------- apps/restconf/restconf_stream_native.c | 275 +++---------------------- include/clixon_custom.h | 4 - test/test_restconf_notifications.sh | 25 +-- 14 files changed, 282 insertions(+), 533 deletions(-) delete mode 100644 apps/restconf/restconf_api.c diff --git a/CHANGELOG.md b/CHANGELOG.md index 088c5fc3..6181e5e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ Expected: June 2024 ### Features +* RESTCONF notification for native mode + * Previously only for FCGI + * Limitations, ie only HTTP/1, regular subscription + stop-time * Optimization of yang schema mount: share yang-specs if equal * Changed datastore modstate to be last in file, as prior to 7.0 * New: Event priority. Backend socket has higher prio diff --git a/apps/restconf/Makefile.in b/apps/restconf/Makefile.in index c5992829..728ca18b 100644 --- a/apps/restconf/Makefile.in +++ b/apps/restconf/Makefile.in @@ -88,7 +88,6 @@ LEX = @LEX@ CPPFLAGS = @CPPFLAGS@ - ifeq ($(LINKAGE),dynamic) CPPFLAGS += -fPIC endif @@ -100,7 +99,6 @@ APPL = clixon_restconf # Common source - not accessible from plugin - independent of restconf package (fcgi|native) APPSRC = -APPSRC += restconf_api.c # maybe empty APPSRC += restconf_err.c APPSRC += restconf_methods.c APPSRC += restconf_methods_post.c diff --git a/apps/restconf/README.md b/apps/restconf/README.md index 17d662e4..d59832a5 100644 --- a/apps/restconf/README.md +++ b/apps/restconf/README.md @@ -4,17 +4,25 @@ * [Nginx](#nginx) * [Streams](#streams) * [Nchan Streams](#nchan) - * [Debugging](#debugging) + * [Debugging](#debugging) There are two installation instructions: for native and nginx. ## Native +Native with http1 and http2 is the main variant, with most regression testing. + Configure clixon with native restconf: ``` ./configure --with-restconf=native ``` +You can disable http1 and http2: +``` + --disable-http1 Disable native http/1.1 (ie http/2 only) + --disable-nghttp2 Disable native http/2 using libnghttp2 (ie http/1 only) +``` + Ensure www-data is member of the CLICON_SOCK_GROUP (default clicon). If not, add it: ``` sudo usermod -a -G clicon www-data @@ -164,6 +172,8 @@ See (stream tests)[../test/test_streams.sh] for more examples. ## Nchan +This is not supported + As an alternative streams implementation, Nginx/Nchan can be used. Nginx uses pub/sub channels and can be configured in a variety of ways. The following uses a simple variant with one generic subscription @@ -247,3 +257,16 @@ You can set debug level of the backend via restconf: ``` curl -is -X POST -H "Content-Type: application/yang-data+json" -d '{"clixon-lib:input":{"level":1}}' http://localhost/restconf/operations/clixon-lib:debug ``` + +## Code structure + +Due to the native and fcgi variants, and also native http1/http2, the +source file structure is complex. + +There are the following blocks of files: + +* COMMON: Common code, such as HTTP methods processing, error and common lib functions +* FCGI: Top-level main, stream and low-level lib (as defined by restconf_api.h) +* NATIVE-COMMON: Top-level main, stream and low-level lib +* NATIVE-HTTP1: Native for HTTP/1 only +* NATIVE-HTTP2: Native for Libnghttp2 only diff --git a/apps/restconf/restconf_api.c b/apps/restconf/restconf_api.c deleted file mode 100644 index a44b20b6..00000000 --- a/apps/restconf/restconf_api.c +++ /dev/null @@ -1,67 +0,0 @@ -/* - * - ***** BEGIN LICENSE BLOCK ***** - - Copyright (C) 2009-2019 Olof Hagsand - Copyright (C) 2020-2022 Olof Hagsand and Rubicon Communications, LLC(Netgate) - - This file is part of CLIXON. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - Alternatively, the contents of this file may be used under the terms of - the GNU General Public License Version 3 or later (the "GPL"), - in which case the provisions of the GPL are applicable instead - of those above. If you wish to allow use of your version of this file only - under the terms of the GPL, and not to allow others to - use your version of this file under the terms of Apache License version 2, - indicate your decision by deleting the provisions above and replace them with - the notice and other provisions required by the GPL. If you do not delete - the provisions above, a recipient may use your version of this file under - the terms of any one of the Apache License version 2 or the GPL. - - ***** END LICENSE BLOCK ***** - * Generic restconf API functions - */ - - -#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 /* chmod */ - -/* cligen */ -#include - -/* clixon */ -#include - -/* restconf */ -#include "restconf_lib.h" -#include "restconf_api.h" - -/* empty */ diff --git a/apps/restconf/restconf_http1.c b/apps/restconf/restconf_http1.c index b043a4cd..e4d72be7 100644 --- a/apps/restconf/restconf_http1.c +++ b/apps/restconf/restconf_http1.c @@ -67,9 +67,7 @@ #include "clixon_http1_parse.h" #include "restconf_http1.h" #include "clixon_http_data.h" -#ifdef RESTCONF_NATIVE_STREAM #include "restconf_stream.h" -#endif /* Size of xml read buffer */ #define BUFLEN 1024 @@ -453,13 +451,11 @@ restconf_http1_path_root(clixon_handle h, if (api_http_data(h, sd, sd->sd_qvec) < 0) goto done; } -#ifdef RESTCONF_NATIVE_STREAM else if (api_path_is_stream(h)){ restconf_socket *rs = rc->rc_socket; if (api_stream(h, sd, sd->sd_qvec, rs->rs_stream_timeout, NULL) < 0) goto done; } -#endif else sd->sd_code = 404; /* catch all without body/media */ fail: diff --git a/apps/restconf/restconf_native.c b/apps/restconf/restconf_native.c index feaaf801..c07e9449 100644 --- a/apps/restconf/restconf_native.c +++ b/apps/restconf/restconf_native.c @@ -76,9 +76,7 @@ #ifdef HAVE_HTTP1 #include "restconf_http1.h" #endif -#ifdef RESTCONF_NATIVE_STREAM #include "restconf_stream.h" -#endif /* Forward */ static int restconf_idle_cb(int fd, void *arg); @@ -384,7 +382,8 @@ restconf_connection_sanity(clixon_handle h, } /* Write buf to socket - * see also this function in restcont_api_openssl.c + * + * see also this function in restconf_api_openssl.c * @param[in] h Clixon handle * @param[in] buf Buffer to write * @param[in] buflen Length of buffer @@ -394,7 +393,7 @@ restconf_connection_sanity(clixon_handle h, * @retval 0 OK, but socket write returned error, caller should close rc * @retval -1 Error */ -static int +int native_buf_write(clixon_handle h, char *buf, size_t buflen, @@ -1081,11 +1080,9 @@ restconf_connection_close1(restconf_conn *rc) if (restconf_callhome_timer(rsock, 1) < 0) goto done; } -#ifdef RESTCONF_NATIVE_STREAM if (rc->rc_event_stream){ stream_close(rc->rc_h, rc); } -#endif retval = 0; done: clixon_debug(CLIXON_DBG_RESTCONF, "retval:%d", retval); diff --git a/apps/restconf/restconf_native.h b/apps/restconf/restconf_native.h index 3c917be5..1386da5c 100644 --- a/apps/restconf/restconf_native.h +++ b/apps/restconf/restconf_native.h @@ -121,9 +121,7 @@ typedef struct restconf_conn { restconf_socket *rc_socket; /* Backpointer to restconf_socket needed for callhome */ struct timeval rc_t; /* Timestamp of last read/write activity, used by callhome idle-timeout algorithm */ -#ifdef RESTCONF_NATIVE_STREAM int rc_event_stream; /* Event notification stream socket (maybe in sd?) */ -#endif } restconf_conn; /* Restconf per socket handle @@ -182,6 +180,7 @@ int ssl_x509_name_oneline(SSL *ssl, char **oneline); int restconf_close_ssl_socket(restconf_conn *rc, const char *callfn, int sslerr0); int restconf_connection_sanity(clixon_handle h, restconf_conn *rc, restconf_stream_data *sd); +int native_buf_write(clixon_handle h, char *buf, size_t buflen, restconf_conn *rc, const char *callfn); restconf_native_handle *restconf_native_handle_get(clixon_handle h); int restconf_connection(int s, void *arg); int restconf_ssl_accept_client(clixon_handle h, int s, restconf_socket *rsock, restconf_conn **rcp); @@ -190,6 +189,8 @@ int restconf_callhome_timer(restconf_socket *rsock, int status); int restconf_socket_extract(clixon_handle h, cxobj *xs, cvec *nsc, restconf_socket *rsock, char **namespace, char **address, char **addrtype, uint16_t *port); + + #endif /* _RESTCONF_NATIVE_H_ */ #ifdef __cplusplus diff --git a/apps/restconf/restconf_nghttp2.c b/apps/restconf/restconf_nghttp2.c index 790cd0e0..b0ee3211 100644 --- a/apps/restconf/restconf_nghttp2.c +++ b/apps/restconf/restconf_nghttp2.c @@ -85,9 +85,7 @@ #include "restconf_err.h" #include "restconf_root.h" #include "restconf_native.h" /* Restconf-openssl mode specific headers*/ -#ifdef RESTCONF_NATIVE_STREAM #include "restconf_stream.h" -#endif #ifdef HAVE_LIBNGHTTP2 /* Ends at end-of-file */ #include "restconf_nghttp2.h" /* Restconf-openssl mode specific headers*/ #include "clixon_http_data.h" @@ -341,13 +339,11 @@ restconf_nghttp2_path(restconf_stream_data *sd) if (api_http_data(h, sd, sd->sd_qvec) < 0) goto done; } -#ifdef RESTCONF_NATIVE_STREAM else if (api_path_is_stream(h)){ restconf_socket *rs = rc->rc_socket; if (api_stream(h, sd, sd->sd_qvec, rs->rs_stream_timeout, NULL) < 0) goto done; } -#endif else if (api_root_restconf(h, sd, sd->sd_qvec) < 0) /* error handling */ goto done; } @@ -497,9 +493,7 @@ http2_exec(restconf_conn *rc, if (strcmp(sd->sd_path, RESTCONF_WELL_KNOWN) == 0 || api_path_is_restconf(rc->rc_h) || api_path_is_data(rc->rc_h) -#ifdef RESTCONF_NATIVE_STREAM || api_path_is_stream(rc->rc_h) -#endif ) { clixon_debug(CLIXON_DBG_RESTCONF, "path found"); if (restconf_nghttp2_path(sd) < 0) diff --git a/apps/restconf/restconf_stream.c b/apps/restconf/restconf_stream.c index e850352f..255e3397 100644 --- a/apps/restconf/restconf_stream.c +++ b/apps/restconf/restconf_stream.c @@ -179,6 +179,8 @@ restconf_subscription(clixon_handle h, goto ok; } /* Setting up stream */ + if (restconf_reply_header(req, "Server", "clixon") < 0) + goto done; if (restconf_reply_header(req, "Server", "clixon") < 0) goto done; if (restconf_reply_header(req, "Content-Type", "text/event-stream") < 0) @@ -187,10 +189,9 @@ restconf_subscription(clixon_handle h, goto done; if (restconf_reply_header(req, "Connection", "keep-alive") < 0) goto done; -#ifndef RESTCONF_NATIVE_STREAM + /* Must be there for FCGI caching */ if (restconf_reply_header(req, "X-Accel-Buffering", "no") < 0) goto done; -#endif if (restconf_reply_send(req, 201, NULL, 0) < 0) goto done; *sp = s; @@ -204,3 +205,120 @@ restconf_subscription(clixon_handle h, cbuf_free(cb); return retval; } + +/*! Process a stream request + * + * @param[in] h Clixon handle + * @param[in] req Generic Www handle (can be part of clixon handle) + * @param[in] qvec Query parameters, ie the ?=&= stuff + * @param[in] timeout Stream timeout + * @param[out] finish Set to zero, if request should not be finnished by upper layer + * @retval 0 OK + * @retval -1 Error + */ +int +api_stream(clixon_handle h, + void *req, + cvec *qvec, + int timeout, + int *finish) +{ + int retval = -1; + char *path = NULL; + char **pvec = NULL; + int pn; + cvec *pcvec = NULL; /* for rest api */ + cxobj *xerr = NULL; + char *streampath; + int pretty; + int besock = -1; + restconf_media media_reply = YANG_DATA_XML; + char *media_str = NULL; + char *stream_name; + int ret; + + clixon_debug(CLIXON_DBG_STREAM, ""); + if (req == NULL){ + clixon_err(OE_RESTCONF, EINVAL, "req is NULL"); + goto done; + } + streampath = clicon_option_str(h, "CLICON_STREAM_PATH"); + if ((path = restconf_uripath(h)) == NULL) + goto done; + pretty = restconf_pretty_get(h); + if ((pvec = clicon_strsep(path, "/", &pn)) == NULL) + goto done; + /* Sanity check of path. Should be /stream/ */ + if (pn != 3){ + if (netconf_invalid_value_xml(&xerr, "protocol", "Invalid path, /stream/ expected") < 0) + goto done; + if (api_return_err0(h, req, xerr, pretty, media_reply, 0) < 0) + goto done; + goto ok; + } + /* Get media for output (proactive negotiation) RFC7231 by using + */ + media_str = restconf_param_get(h, "HTTP_ACCEPT"); + if (media_str == NULL){ + if (restconf_not_acceptable(h, req, pretty, media_reply) < 0) + goto done; + goto ok; + } + /* Accept only text_event-stream or */ + if (strcmp(media_str, "*/*") != 0 && + strcmp(media_str, "text/event-stream") != 0){ + if (restconf_not_acceptable(h, req, pretty, media_reply) < 0) + goto done; + goto ok; + } + if (strlen(pvec[0]) != 0){ + if (netconf_invalid_value_xml(&xerr, "protocol", "Invalid path, /stream/ expected") < 0) + goto done; + if (api_return_err0(h, req, xerr, pretty, media_reply, 0) < 0) + goto done; + goto ok; + } + if (strcmp(pvec[1], streampath)){ + if (netconf_invalid_value_xml(&xerr, "protocol", "Invalid path, /stream/ expected") < 0) + goto done; + if (api_return_err0(h, req, xerr, pretty, media_reply, 0) < 0) + goto done; + goto ok; + } + if ((stream_name = pvec[2]) == NULL){ + if (netconf_invalid_value_xml(&xerr, "protocol", "Invalid path, /stream/ expected") < 0) + goto done; + if (api_return_err0(h, req, xerr, pretty, media_reply, 0) < 0) + goto done; + goto ok; + } + clixon_debug(CLIXON_DBG_STREAM, "stream-name: %s", stream_name); + if (uri_str2cvec(path, '/', '=', 1, &pcvec) < 0) /* rest url eg /album=ricky/foo */ + goto done; + /* If present, check credentials. See "plugin_credentials" in plugin + * See RFC 8040 section 2.5 + */ + if ((ret = restconf_authentication_cb(h, req, pretty, media_reply)) < 0) + goto done; + if (ret == 0) + goto ok; + if (restconf_subscription(h, req, stream_name, qvec, pretty, media_reply, &besock) < 0) + goto done; + if (besock != -1){ + if (stream_sockets_setup(h, req, timeout, besock, finish) < 0) + goto done; + } + ok: + retval = 0; + done: + clixon_debug(CLIXON_DBG_STREAM, "retval:%d", retval); + if (xerr) + xml_free(xerr); + if (pvec) + free(pvec); + if (pcvec) + cvec_free(pcvec); + if (path) + free(path); + return retval; +} diff --git a/apps/restconf/restconf_stream.h b/apps/restconf/restconf_stream.h index 9614565f..6707bbef 100644 --- a/apps/restconf/restconf_stream.h +++ b/apps/restconf/restconf_stream.h @@ -42,9 +42,10 @@ */ int api_path_is_stream(clixon_handle h); int restconf_subscription(clixon_handle h, void *req, char *name, cvec *qvec, int pretty, restconf_media media_out, int *sp); -int stream_child_free(clixon_handle h, int pid); -int stream_child_freeall(clixon_handle h); -int stream_close(clixon_handle h, void *req); int api_stream(clixon_handle h, void *req, cvec *qvec, int timeout, int *finish); +int stream_sockets_setup(clixon_handle h, void *req, int timeout, int besock, int *finish); +int stream_close(clixon_handle h, void *req); // only native +int stream_child_free(clixon_handle h, int pid); // only fcgi +int stream_child_freeall(clixon_handle h); // only fcgi #endif /* _RESTCONF_STREAM_H_ */ diff --git a/apps/restconf/restconf_stream_fcgi.c b/apps/restconf/restconf_stream_fcgi.c index 6f6d6ccc..2322d3f8 100644 --- a/apps/restconf/restconf_stream_fcgi.c +++ b/apps/restconf/restconf_stream_fcgi.c @@ -104,8 +104,9 @@ */ /* Enable for forking stream subscription loop. * Disable to get single threading but blocking on streams + * XXX: Integrate with top-level events */ -#define STREAM_FORK +#undef STREAM_FORK /* Keep track of children - when they exit - their FCGX handle needs to be * freed with FCGX_Free(&rbk, 0); @@ -120,6 +121,8 @@ struct stream_child{ */ static struct stream_child *STREAM_CHILD = NULL; +static int backend_eof = 0; + /*! Find restconf child using PID and cleanup FCGI Request data * * For forked, called on SIGCHILD @@ -174,11 +177,11 @@ stream_child_freeall(clixon_handle h) * @see netconf_notification_cb */ static int -restconf_stream_cb(int s, - void *arg) +stream_fcgi_backend_cb(int s, + void *arg) { int retval = -1; - FCGX_Request *r = (FCGX_Request *)arg; + FCGX_Request *req = (FCGX_Request *)arg; int eof; cxobj *xtop = NULL; /* top xml */ cxobj *xn; /* notification xml */ @@ -193,14 +196,13 @@ restconf_stream_cb(int s, clixon_debug(CLIXON_DBG_STREAM, "%s", cbuf_get(cbmsg)); // Also MSG /* handle close from remote end: this will exit the client */ if (eof){ - clixon_debug(CLIXON_DBG_STREAM, "eof"); - clixon_err(OE_PROTO, ESHUTDOWN, "Socket unexpected close"); - errno = ESHUTDOWN; - FCGX_FPrintF(r->out, "SHUTDOWN\r\n"); - FCGX_FPrintF(r->out, "\r\n"); - FCGX_FFlush(r->out); - clixon_exit_set(1); - goto done; + clixon_debug(CLIXON_DBG_STREAM, "eof, terminate stream"); + backend_eof = 1; + clixon_exit_set(1); // local timeout + FCGX_FPrintF(req->out, "SHUTDOWN\r\n"); + FCGX_FPrintF(req->out, "\r\n"); + FCGX_FFlush(req->out); + goto ok; } if ((ret = clixon_xml_parse_string(cbuf_get(cbmsg), YB_NONE, NULL, &xtop, NULL)) < 0) goto done; @@ -229,9 +231,9 @@ restconf_stream_cb(int s, #endif if (clixon_xml2cbuf(cb, xn, 0, pretty, NULL, -1, 0) < 0) goto done; - FCGX_FPrintF(r->out, "data: %s\r\n", cbuf_get(cb)); - FCGX_FPrintF(r->out, "\r\n"); - FCGX_FFlush(r->out); + FCGX_FPrintF(req->out, "data: %s\r\n", cbuf_get(cb)); + FCGX_FPrintF(req->out, "\r\n"); + FCGX_FFlush(req->out); ok: retval = 0; done: @@ -251,8 +253,8 @@ restconf_stream_cb(int s, * @param[in] req Generic Www handle (can be part of clixon handle) */ static int -stream_checkuplink(int s, - void *arg) +stream_fcgi_uplink_cb(int s, + void *arg) { FCGX_Request *r = (FCGX_Request *)arg; @@ -289,191 +291,106 @@ fcgi_stream_timeout(int s, /*! Timeout of notification stream, limit lifetime, for debug */ static int -fcgi_stream_timeout2(int s, - void *arg) +stream_timeout_end(int s, + void *arg) { clixon_debug(CLIXON_DBG_STREAM, "Terminate stream"); clixon_exit_set(1); // XXX This is local eventloop see below, not global return 0; } -/*! Process a stream request +/*! FCGI specific code for setting up stream sockets * * @param[in] h Clixon handle * @param[in] req Generic Www handle (can be part of clixon handle) - * @param[in] qvec Query parameters, ie the ?=&= stuff * @param[in] timeout Stream timeout + * @param[in] besock Socket to backend * @param[out] finish Set to zero, if request should not be finnished by upper layer * @retval 0 OK * @retval -1 Error + * Consider moving timeout and backend sock to generic code */ int -api_stream(clixon_handle h, - void *req, - cvec *qvec, - int timeout, - int *finish) +stream_sockets_setup(clixon_handle h, + void *req, + int timeout, + int besock, + int *finish) { int retval = -1; FCGX_Request *rfcgi = (FCGX_Request *)req; /* XXX */ - char *path = NULL; - char *method; - char **pvec = NULL; - int pn; - cvec *pcvec = NULL; /* for rest api */ - cbuf *cb = NULL; - char *indata; - int pretty; - restconf_media media_out = YANG_DATA_XML; /* XXX default */ - cbuf *cbret = NULL; - int s = -1; - int ret; - cxobj *xerr = NULL; - char *streampath; #ifdef STREAM_FORK int pid; struct stream_child *sc; + + if ((pid = fork()) == 0){ /* child */ +#if 0 // Leaks + if (pvec) + free(pvec); + if (qvec) + cvec_free(qvec); + if (pcvec) + cvec_free(pcvec); #endif - - clixon_debug(CLIXON_DBG_STREAM, ""); - streampath = clicon_option_str(h, "CLICON_STREAM_PATH"); - if ((path = restconf_uripath(h)) == NULL) - goto done; - pretty = restconf_pretty_get(h); - if ((pvec = clicon_strsep(path, "/", &pn)) == NULL) - goto done; - /* Sanity check of path. Should be /stream/ */ - if (pn != 3){ - if (netconf_invalid_value_xml(&xerr, "protocol", "Invalid path, /stream/ expected") < 0) - goto done; - if (api_return_err0(h, req, xerr, pretty, media_out, 0) < 0) - goto done; - goto ok; - } - if (strlen(pvec[0]) != 0){ - if (netconf_invalid_value_xml(&xerr, "protocol", "Invalid path, /stream/ expected") < 0) - goto done; - if (api_return_err0(h, req, xerr, pretty, media_out, 0) < 0) - goto done; - goto ok; - } - if (strcmp(pvec[1], streampath)){ - if (netconf_invalid_value_xml(&xerr, "protocol", "Invalid path, /stream/ expected") < 0) - goto done; - if (api_return_err0(h, req, xerr, pretty, media_out, 0) < 0) - goto done; - goto ok; - } - if ((method = pvec[2]) == NULL){ - if (netconf_invalid_value_xml(&xerr, "protocol", "Invalid path, /stream/ expected") < 0) - goto done; - if (api_return_err0(h, req, xerr, pretty, media_out, 0) < 0) - goto done; - goto ok; - } - clixon_debug(CLIXON_DBG_STREAM, "method=%s", method); - - if (uri_str2cvec(path, '/', '=', 1, &pcvec) < 0) /* rest url eg /album=ricky/foo */ - goto done; - /* data */ - if ((cb = restconf_get_indata(req)) == NULL) - goto done; - indata = cbuf_get(cb); - clixon_debug(CLIXON_DBG_STREAM, "DATA=%s", indata); - - /* If present, check credentials. See "plugin_credentials" in plugin - * See RFC 8040 section 2.5 - */ - if ((ret = restconf_authentication_cb(h, req, pretty, media_out)) < 0) - goto done; - if (ret == 0) - goto ok; - if (restconf_subscription(h, req, method, qvec, pretty, media_out, &s) < 0) - goto done; - if (s != -1){ -#ifdef STREAM_FORK - if ((pid = fork()) == 0){ /* child */ - if (pvec) - free(pvec); - if (qvec) - cvec_free(qvec); - if (pcvec) - cvec_free(pcvec); - if (cb) - cbuf_free(cb); - if (cbret) - cbuf_free(cbret); #endif /* STREAM_FORK */ - /* Listen to backend socket */ - if (clixon_event_reg_fd(s, - restconf_stream_cb, - req, - "stream socket") < 0) + backend_eof = 0; + /* Listen to backend socket */ + if (clixon_event_reg_fd(besock, + stream_fcgi_backend_cb, + req, + "stream socket") < 0) + goto done; + if (clixon_event_reg_fd(rfcgi->listen_sock, + stream_fcgi_uplink_cb, + req, + "stream socket") < 0) + goto done; + /* Timeout of notification stream, close after limited lifetime, for debug */ + if (timeout){ + struct timeval t; + gettimeofday(&t, NULL); + t.tv_sec += timeout; + clixon_event_reg_timeout(t, stream_timeout_end, req, "Stream timeout"); + } + /* Poll upstream errors */ + fcgi_stream_timeout(0, req); + /* Start loop */ + clixon_event_loop(h); + clixon_debug(CLIXON_DBG_STREAM, "after loop"); + if (backend_eof == 0) + if (clicon_rpc_close_session(h) < 0) goto done; - if (clixon_event_reg_fd(rfcgi->listen_sock, - stream_checkuplink, - req, - "stream socket") < 0) - goto done; - /* Timeout of notification stream, close after limited lifetime, for debug */ - if (timeout){ - struct timeval t; - gettimeofday(&t, NULL); - t.tv_sec += timeout; - clixon_event_reg_timeout(t, fcgi_stream_timeout2, req, "Stream timeout"); - } - /* Poll upstream errors */ - fcgi_stream_timeout(0, req); - /* Start loop */ - clixon_event_loop(h); - clixon_debug(CLIXON_DBG_STREAM, "after loop"); - clicon_rpc_close_session(h); - clixon_event_unreg_fd(s, restconf_stream_cb); - close(s); - clixon_event_unreg_fd(rfcgi->listen_sock, - restconf_stream_cb); - clixon_event_unreg_timeout(fcgi_stream_timeout, (void*)req); - clixon_event_unreg_timeout(fcgi_stream_timeout2, (void*)req); - clixon_exit_set(0); /* reset */ + clixon_event_unreg_fd(besock, stream_fcgi_backend_cb); + close(besock); + clixon_event_unreg_fd(rfcgi->listen_sock, stream_fcgi_uplink_cb); + clixon_event_unreg_timeout(fcgi_stream_timeout, (void*)req); + clixon_event_unreg_timeout(stream_timeout_end, (void*)req); + clixon_exit_set(0); /* reset */ #ifdef STREAM_FORK #if 0 /* Seems to be a global resource, but there is till some timing error here */ - FCGX_Finish_r(rfcgi); - FCGX_Free(rfcgi, 0); + FCGX_Finish_r(rfcgi); + FCGX_Free(rfcgi, 0); #endif - restconf_terminate(h); - exit(0); - } - /* parent */ - /* Create stream_child struct and store pid and FCGI data, when child - * killed, call FCGX_Free - */ - if ((sc = malloc(sizeof(struct stream_child))) == NULL){ - clixon_err(OE_XML, errno, "malloc"); - goto done; - } - memset(sc, 0, sizeof(struct stream_child)); - sc->sc_pid = pid; - sc->sc_r = *rfcgi; /* XXX by value */ - - ADDQ(sc, STREAM_CHILD); - *finish = 0; /* If spawn child, we should not finish this stream */ -#endif /* STREAM_FORK */ + restconf_terminate(h); + exit(0); } - ok: + /* parent */ + /* Create stream_child struct and store pid and FCGI data, when child + * killed, call FCGX_Free + */ + if ((sc = malloc(sizeof(struct stream_child))) == NULL){ + clixon_err(OE_XML, errno, "malloc"); + goto done; + } + memset(sc, 0, sizeof(struct stream_child)); + sc->sc_pid = pid; + sc->sc_r = *rfcgi; /* XXX by value */ + + ADDQ(sc, STREAM_CHILD); + *finish = 0; /* If spawn child, we should not finish this stream */ +#endif /* STREAM_FORK */ retval = 0; done: clixon_debug(CLIXON_DBG_STREAM, "retval:%d", retval); - if (xerr) - xml_free(xerr); - if (pvec) - free(pvec); - if (pcvec) - cvec_free(pcvec); - if (cb) - cbuf_free(cb); - if (cbret) - cbuf_free(cbret); - if (path) - free(path); return retval; } diff --git a/apps/restconf/restconf_stream_native.c b/apps/restconf/restconf_stream_native.c index c3645ed5..3f739f7b 100644 --- a/apps/restconf/restconf_stream_native.c +++ b/apps/restconf/restconf_stream_native.c @@ -86,101 +86,6 @@ #include "restconf_native.h" /* Restconf-openssl mode specific headers*/ #include "restconf_stream.h" -// XXX: copy from restconf_native.c -static int -native_buf_write_xxx(clixon_handle h, - char *buf, - size_t buflen, - restconf_conn *rc, - const char *callfn) -{ - int retval = -1; - ssize_t len; - ssize_t totlen = 0; - int er; - SSL *ssl; - - if (rc == NULL){ - clixon_err(OE_RESTCONF, EINVAL, "rc is NULL"); - goto done; - } - ssl = rc->rc_ssl; - /* Two problems with debugging buffers that this fixes: - * 1. they are not "strings" in the sense they are not NULL-terminated - * 2. they are often very long - */ - if (clixon_debug_get()) { - char *dbgstr = NULL; - size_t sz; - sz = buflen>256?256:buflen; /* Truncate to 256 */ - if ((dbgstr = malloc(sz+1)) == NULL){ - clixon_err(OE_UNIX, errno, "malloc"); - goto done; - } - memcpy(dbgstr, buf, sz); - dbgstr[sz] = '\0'; - clixon_debug(CLIXON_DBG_RESTCONF, "%s buflen:%zu buf:\n%s", callfn, buflen, dbgstr); - free(dbgstr); - } - while (totlen < buflen){ - if (ssl){ - if ((len = SSL_write(ssl, buf+totlen, buflen-totlen)) <= 0){ - er = errno; - switch (SSL_get_error(ssl, len)){ - case SSL_ERROR_SYSCALL: /* 5 */ - if (er == ECONNRESET || /* Connection reset by peer */ - er == EPIPE) { /* Reading end of socket is closed */ - goto closed; /* Close socket and ssl */ - } - else if (er == EAGAIN){ - clixon_debug(CLIXON_DBG_RESTCONF, "write EAGAIN"); - usleep(10000); - continue; - } - else{ - clixon_err(OE_RESTCONF, er, "SSL_write %d", er); - goto done; - } - break; - default: - clixon_err(OE_SSL, 0, "SSL_write"); - goto done; - break; - } - goto done; - } - } - else{ - if ((len = write(rc->rc_s, buf+totlen, buflen-totlen)) < 0){ - switch (errno){ - case EAGAIN: /* Operation would block */ - clixon_debug(CLIXON_DBG_RESTCONF, "write EAGAIN"); - usleep(10000); - continue; - break; - // case EBADF: // XXX if this happens there is some larger error - case ECONNRESET: /* Connection reset by peer */ - case EPIPE: /* Broken pipe */ - goto closed; /* Close socket and ssl */ - break; - default: - clixon_err(OE_UNIX, errno, "write %d", errno); - goto done; - break; - } - } - } - totlen += len; - } /* while */ - retval = 1; - done: - clixon_debug(CLIXON_DBG_RESTCONF, "retval:%d", retval); - return retval; - closed: - retval = 0; - goto done; -} - /*! Callback when stream notifications arrive from backend * * @param[in] s Socket @@ -190,8 +95,8 @@ native_buf_write_xxx(clixon_handle h, * @see netconf_notification_cb */ static int -restconf_native_stream_cb(int s, - void *arg) +stream_native_backend_cb(int s, + void *arg) { int retval = -1; restconf_stream_data *sd = (restconf_stream_data *)arg; @@ -258,7 +163,7 @@ restconf_native_stream_cb(int s, cprintf(cb, "\r\n"); cprintf(cb, "\r\n"); #endif - if ((ret = native_buf_write_xxx(h, cbuf_get(cb), cbuf_len(cb), rc, "native stream")) < 0) + if ((ret = native_buf_write(h, cbuf_get(cb), cbuf_len(cb), rc, "native stream")) < 0) goto done; ok: retval = 0; @@ -278,8 +183,8 @@ restconf_native_stream_cb(int s, /*! Timeout of notification stream, limit lifetime, for debug */ static int -native_stream_timeout(int s, - void *arg) +stream_timeout_end(int s, + void *arg) { restconf_conn *rc = (restconf_conn *)arg; @@ -299,170 +204,52 @@ stream_close(clixon_handle h, restconf_conn *rc = (restconf_conn *)req; clicon_rpc_close_session(h); - clixon_event_unreg_fd(rc->rc_event_stream, restconf_native_stream_cb); - clixon_event_unreg_timeout(native_stream_timeout, req); + clixon_event_unreg_fd(rc->rc_event_stream, stream_native_backend_cb); + clixon_event_unreg_timeout(stream_timeout_end, req); close(rc->rc_event_stream); rc->rc_event_stream = 0; return 0; } -/*! Process a stream request, native variant +/*! Native specific code for setting up stream sockets * * @param[in] h Clixon handle * @param[in] req Generic Www handle (can be part of clixon handle) - * @param[in] qvec Query parameters, ie the ?=&= stuff * @param[in] timeout Stream timeout - * @param[out] finish Not used in native? + * @param[in] besock Socket to backend + * @param[out] finish Set to zero, if request should not be finnished by upper layer * @retval 0 OK * @retval -1 Error - * @see api_stream fcgi implementation - * @note According to RFC8040 Sec 6 accept-stream is text/event-stream, but stream data - * is XML according to RFC5277. But what is error return? assume XML here + * Consider moving timeout and backend sock to generic code */ -static int -api_native_stream(clixon_handle h, - void *req, - cvec *qvec, - int timeout, - int *finish) +int +stream_sockets_setup(clixon_handle h, + void *req, + int timeout, + int besock, + int *finish) { - int retval = -1; + int retval = -1; restconf_stream_data *sd = (restconf_stream_data *)req; - restconf_conn *rc; - char *path = NULL; - char *request_method = NULL; /* GET,.. */ - char *streampath; - int pretty; - char **pvec = NULL; - int pn; - cvec *pcvec = NULL; /* for rest api */ - cxobj *xerr = NULL; - char *media_str = NULL; - char *stream_name; - restconf_media media_reply = YANG_DATA_XML; - int ret; - int backend_socket = -1; + restconf_conn *rc; - clixon_debug(CLIXON_DBG_STREAM, ""); - if (req == NULL){ - clixon_err(OE_RESTCONF, EINVAL, "req is NULL"); + /* Listen to backend socket */ + if (clixon_event_reg_fd(besock, + stream_native_backend_cb, + req, + "stream socket") < 0) goto done; - } rc = sd->sd_conn; - streampath = clicon_option_str(h, "CLICON_STREAM_PATH"); - if ((path = restconf_uripath(h)) == NULL) - goto done; - clixon_debug(CLIXON_DBG_STREAM, "path:%s", path); - request_method = restconf_param_get(h, "REQUEST_METHOD"); - clixon_debug(CLIXON_DBG_STREAM, "method:%s", request_method); - pretty = restconf_pretty_get(h); - clixon_debug(CLIXON_DBG_STREAM, "pretty:%d", pretty); - /* Get media for output (proactive negotiation) RFC7231 by using - * Accept:. This is for methods that have output, such as GET, - * operation POST, etc - * If accept is * default is yang-json - */ - media_str = restconf_param_get(h, "HTTP_ACCEPT"); - clixon_debug(CLIXON_DBG_STREAM, "accept(media):%s", media_str); - if (media_str == NULL){ - if (restconf_not_acceptable(h, sd, pretty, media_reply) < 0) - goto done; - goto ok; + rc->rc_event_stream = besock; + /* Timeout of notification stream, close after limited lifetime, for debug */ + if (timeout){ + struct timeval t; + gettimeofday(&t, NULL); + t.tv_sec += timeout; + clixon_event_reg_timeout(t, stream_timeout_end, rc, "Stream timeout"); } - /* Accept only text_event-stream or */ - if (strcmp(media_str, "*/*") != 0 && - strcmp(media_str, "text/event-stream") != 0){ - if (restconf_not_acceptable(h, req, pretty, media_reply) < 0) - goto done; - goto ok; - } - if ((pvec = clicon_strsep(path, "/", &pn)) == NULL) - goto done; - if (strlen(pvec[0]) != 0){ - if (netconf_invalid_value_xml(&xerr, "protocol", "Invalid path, /stream/ expected") < 0) - goto done; - if (api_return_err0(h, req, xerr, pretty, media_reply, 0) < 0) - goto done; - goto ok; - } - else if (strcmp(pvec[1], streampath)){ - if (netconf_invalid_value_xml(&xerr, "protocol", "Invalid path, /stream/ expected") < 0) - goto done; - if (api_return_err0(h, req, xerr, pretty, media_reply, 0) < 0) - goto done; - goto ok; - } - else if ((stream_name = pvec[2]) == NULL || - strlen(stream_name) == 0){ - if (netconf_invalid_value_xml(&xerr, "protocol", "Invalid path, /stream/ expected") < 0) - goto done; - if (api_return_err0(h, req, xerr, pretty, media_reply, 0) < 0) - goto done; - goto ok; - } - clixon_debug(CLIXON_DBG_STREAM, "stream-name: %s", stream_name); - if (uri_str2cvec(path, '/', '=', 1, &pcvec) < 0) /* rest url eg /album=ricky/foo */ - goto done; - /* If present, check credentials. See "plugin_credentials" in plugin - * See RFC 8040 section 2.5 - */ - if ((ret = restconf_authentication_cb(h, req, pretty, media_reply)) < 0) - goto done; - if (ret == 0) - goto ok; - clixon_debug(CLIXON_DBG_STREAM, "passed auth"); - if (restconf_subscription(h, req, stream_name, qvec, pretty, media_reply, &backend_socket) < 0) - goto done; - if (backend_socket != -1){ - // XXX Could add forking here eventurally - /* Listen to backend socket */ - if (clixon_event_reg_fd(backend_socket, - restconf_native_stream_cb, - sd, - "stream socket") < 0) - goto done; - rc->rc_event_stream = backend_socket; - /* Timeout of notification stream, close after limited lifetime, for debug */ - if (timeout){ - struct timeval t; - gettimeofday(&t, NULL); - t.tv_sec += timeout; - clixon_event_reg_timeout(t, native_stream_timeout, rc, "Stream timeout"); - } - } - ok: retval = 0; done: clixon_debug(CLIXON_DBG_STREAM, "retval:%d", retval); - if (xerr) - xml_free(xerr); - if (path) - free(path); - if (pvec) - free(pvec); - if (pcvec) - cvec_free(pcvec); return retval; } - -/*! Process a stream request, native variant - * - * @param[in] h Clixon handle - * @param[in] req Generic Www handle (can be part of clixon handle) - * @param[in] qvec Query parameters, ie the ?=&= stuff - * @param[out] finish Not used in native? - * @retval 0 OK - * @retval -1 Error - * @see api_stream fcgi implementation - * @note According to RFC8040 Sec 6 accept-stream is text/event-stream, but stream data - * is XML according to RFC5277. But what is error return? assume XML here - */ -int -api_stream(clixon_handle h, - void *req, - cvec *qvec, - int timeout, - int *finish) -{ - return api_native_stream(h, req, qvec, timeout, finish); -} diff --git a/include/clixon_custom.h b/include/clixon_custom.h index 7aab4927..6f58f861 100644 --- a/include/clixon_custom.h +++ b/include/clixon_custom.h @@ -212,10 +212,6 @@ */ #undef USE_SHA256 -/*! Restconf native stream support - */ -#define RESTCONF_NATIVE_STREAM - /*! Temporary comparison of xyanglibs, should be removed asap */ #define YANG_SCHEMA_CMP_KLUDGE diff --git a/test/test_restconf_notifications.sh b/test/test_restconf_notifications.sh index fe6a8ef7..96d3307b 100755 --- a/test/test_restconf_notifications.sh +++ b/test/test_restconf_notifications.sh @@ -133,7 +133,6 @@ function runtest() new "2a) start $extra timeout:${TIMEOUT}s - expect ${LBOUND}-${UBOUND} notifications" ret=$(curl $CURLOPTS $extra -X GET -H "Accept: text/event-stream" -H "Cache-Control: no-cache" -H "Connection: keep-alive" $RCPROTO://localhost/streams/EXAMPLE) - match=$(echo "$ret" | grep -Eo "$expect") if [ -z "$match" ]; then err "$expect" "$ret" @@ -149,7 +148,6 @@ function runtest() new "2b) start $extra timeout:${TIMEOUT} stop after 5s - expect ${LB}-${UB} notifications" ret=$(curl $CURLOPTS $extra -X GET -H "Accept: text/event-stream" -H "Cache-Control: no-cache" -H "Connection: keep-alive" $RCPROTO://localhost/streams/EXAMPLE?stop-time=${time1}) - match=$(echo "$ret" | grep -Eo "$expect") if [ -z "$match" ]; then err "$expect" "$ret" @@ -181,6 +179,9 @@ if [ $BE -ne 0 ]; then if [ $? -ne 0 ]; then err fi + sudo pkill -f clixon_backend # to be sure +fi +if [ $BE -ne 0 ]; then new "start backend -s init -f $cfg -- -n ${PERIOD}" # create example notification stream with periodic timeout ${PERIOD} seconds start_backend -s init -f $cfg -- -n ${PERIOD} @@ -192,7 +193,8 @@ wait_backend if [ $RC -ne 0 ]; then new "kill old restconf daemon" stop_restconf_pre - + + sleep 1 new "start restconf daemon -f $cfg -t ${TIMEOUT}" start_restconf -f $cfg -t ${TIMEOUT} fi @@ -233,23 +235,6 @@ if [ "${WITH_RESTCONF}" = "native" ]; then fi if false; then # NYI -test-pause - -# 2b) start subscription 8s - stoptime after 5s - expect 1-2 notifications -new "2b) start subscriptions 8s - stoptime after 5s - expect 1-2 notifications" -ret=$($clixon_util_stream -u $RCPROTO://localhost/streams/EXAMPLE -t 8 -e +10) -expect="data: ${DATE}T[0-9:.]*ZfaultEthernet0major" -match=$(echo "$ret" | grep -Eo "$expect") -if [ -z "$match" ]; then - err "$expect" "$ret" -fi -nr=$(echo "$ret" | grep -c "data:") -if [ $nr -lt 1 -o $nr -gt 2 ]; then - err 1 "$nr" -fi - -test-pause - # 2c new "2c) start sub 8s - replay from start -8s - expect 3-4 notifications" ret=$($clixon_util_stream -u $RCPROTO://localhost/streams/EXAMPLE -t 10 -s -8)