/* * ***** 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 ***** */ #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 #ifdef HAVE_LIBNGHTTP2 #include #endif /* cligen */ #include /* libclixon */ #include /* restconf */ #include "restconf_lib.h" /* generic shared with plugins */ #include "restconf_handle.h" #include "restconf_err.h" #include "restconf_native.h" /* Restconf-openssl mode specific headers*/ #ifdef HAVE_LIBNGHTTP2 #include #include "restconf_nghttp2.h" /* http/2 */ #endif #ifdef HAVE_HTTP1 #include "restconf_http1.h" #endif /* Forward */ static int restconf_idle_cb(int fd, void *arg); /*! Create restconf stream * * @param[in] rc Restconf connection handle * @see restconf_stream_free */ restconf_stream_data * restconf_stream_data_new(restconf_conn *rc, int32_t stream_id) { restconf_stream_data *sd; if ((sd = malloc(sizeof(restconf_stream_data))) == NULL){ clixon_err(OE_UNIX, errno, "malloc"); return NULL; } memset(sd, 0, sizeof(restconf_stream_data)); sd->sd_stream_id = stream_id; sd->sd_fd = -1; if ((sd->sd_inbuf = cbuf_new()) == NULL){ clixon_err(OE_UNIX, errno, "cbuf_new"); return NULL; } if ((sd->sd_indata = cbuf_new()) == NULL){ clixon_err(OE_UNIX, errno, "cbuf_new"); return NULL; } if ((sd->sd_outp_hdrs = cvec_new(0)) == NULL){ clixon_err(OE_UNIX, errno, "cvec_new"); return NULL; } if ((sd->sd_outp_buf = cbuf_new()) == NULL){ clixon_err(OE_UNIX, errno, "cbuf_new"); return NULL; } sd->sd_conn = rc; INSQ(sd, rc->rc_streams); return sd; } /*! Find restconf stream data * * @param[in] rc Restconf connection handle */ restconf_stream_data * restconf_stream_find(restconf_conn *rc, int32_t id) { restconf_stream_data *sd; if ((sd = rc->rc_streams) != NULL) { do { if (sd->sd_stream_id == id) return sd; sd = NEXTQ(restconf_stream_data *, sd); } while (sd && sd != rc->rc_streams); } return NULL; } /* * @param[in] sd Restconf data stream */ int restconf_stream_free(restconf_stream_data *sd) { if (sd->sd_fd != -1) { close(sd->sd_fd); } if (sd->sd_inbuf) cbuf_free(sd->sd_inbuf); if (sd->sd_indata) cbuf_free(sd->sd_indata); if (sd->sd_outp_hdrs) cvec_free(sd->sd_outp_hdrs); if (sd->sd_outp_buf) cbuf_free(sd->sd_outp_buf); if (sd->sd_body) cbuf_free(sd->sd_body); if (sd->sd_path) free(sd->sd_path); if (sd->sd_settings2) free(sd->sd_settings2); if (sd->sd_qvec) cvec_free(sd->sd_qvec); free(sd); return 0; } /*! Create restconf connection struct, per connect, ie transient * * @param[in] h Clixon handle * @param[in] s Connected socket * @param[in] rsock Backpointer to server struct * @see restconf_conn_free */ restconf_conn * restconf_conn_new(clixon_handle h, int s, restconf_socket *rsock) { restconf_conn *rc; if ((rc = (restconf_conn*)malloc(sizeof(restconf_conn))) == NULL){ clixon_err(OE_UNIX, errno, "malloc"); return NULL; } memset(rc, 0, sizeof(restconf_conn)); rc->rc_h = h; rc->rc_s = s; rc->rc_callhome = rsock->rs_callhome; rc->rc_socket = rsock; INSQ(rc, rsock->rs_conns); clixon_debug(CLIXON_DBG_CLIENT, "%s %p", __FUNCTION__, rc); return rc; } /*! Free clixon/cbuf resources related to a connection * * @param[in] rc restconf connection */ static int restconf_conn_free(restconf_conn *rc) { int retval = -1; restconf_stream_data *sd; restconf_socket *rsock; restconf_conn *rc1; clixon_debug(CLIXON_DBG_CLIENT, "%s", __FUNCTION__); if (rc == NULL){ clixon_err(OE_RESTCONF, EINVAL, "rc is NULL"); goto done; } #ifdef HAVE_LIBNGHTTP2 if (rc->rc_ngsession) nghttp2_session_del(rc->rc_ngsession); #endif /* Free all streams */ while ((sd = rc->rc_streams) != NULL) { DELQ(sd, rc->rc_streams, restconf_stream_data *); if (sd) restconf_stream_free(sd); } /* Free connect from server sock */ if ((rsock = rc->rc_socket) != NULL && (rc1 = rsock->rs_conns) != NULL){ do { if (rc == rc1){ DELQ(rc, rsock->rs_conns, restconf_conn *); break; } rc1 = NEXTQ(restconf_conn *, rc1); } while (rc1 && rc1 != rsock->rs_conns); } free(rc); retval = 0; done: return retval; } /*! Given SSL connection, get peer certificate one-line name * * @param[in] ssl SSL session * @param[out] oneline Cert name one-line * @retval 0 OK * @retval -1 Error */ int ssl_x509_name_oneline(SSL *ssl, char **oneline) { int retval = -1; char *p = NULL; X509 *cert = NULL; X509_NAME *name; if (ssl == NULL || oneline == NULL) { clixon_err(OE_RESTCONF, EINVAL, "ssl or cn is NULL"); goto done; } #if OPENSSL_VERSION_NUMBER < 0x30000000L if ((cert = SSL_get_peer_certificate(ssl)) == NULL) goto ok; #else if ((cert = SSL_get1_peer_certificate(ssl)) == NULL) goto ok; #endif if ((name = X509_get_subject_name(cert)) == NULL) goto ok; if ((p = X509_NAME_oneline(name, NULL, 0)) == NULL) goto ok; if ((*oneline = strdup(p)) == NULL){ clixon_err(OE_UNIX, errno, "strdup"); goto done; } ok: retval = 0; done: if (p) OPENSSL_free(p); if (cert) X509_free(cert); return retval; } /*! Check common connection sanity checks and terminate if found before request processing * * Tests of sanity of connection not really of an individual request, but is triggered by * the (first) request in http/1 and http/2 * These tests maybe could have done earlier, this is somewhat late since the session is * closed and that is always good to do as early as possible. * The following are current checks: * 1) Check if http/2 non-tls is disabled * 2) Check if ssl client certs ae valid * @param[in] h Clixon handle * @param[in] rc Restconf connection handle * @param[in] sd Http stream * @param[out] term Terminate session * @retval 0 OK * @retval -1 Error * @see restconf_accept_client where connection can be exited at an earlier stage */ int restconf_connection_sanity(clixon_handle h, restconf_conn *rc, restconf_stream_data *sd) { int retval = -1; cxobj *xerr = NULL; long code; cbuf *cberr = NULL; restconf_media media_out = YANG_DATA_JSON; char *media_str = NULL; char *oneline = NULL; /* 1) Check if http/2 non-tls is disabled */ if (rc->rc_ssl == NULL && rc->rc_proto == HTTP_2 && clicon_option_bool(h, "CLICON_RESTCONF_HTTP2_PLAIN") == 0){ if (netconf_invalid_value_xml(&xerr, "protocol", "Only HTTP/2 with TLS is enabled, plain http/2 is disabled") < 0) goto done; if ((media_str = restconf_param_get(h, "HTTP_ACCEPT")) == NULL){ media_out = YANG_DATA_JSON; } else if ((int)(media_out = restconf_media_str2int(media_str)) == -1){ if (strcmp(media_str, "*/*") == 0) /* catch-all */ media_out = YANG_DATA_JSON; } if (api_return_err0(h, sd, xerr, 1, media_out, 0) < 0) goto done; rc->rc_exit = 1; } /* 2) Check if ssl client cert is valid */ else if (rc->rc_ssl != NULL && (code = SSL_get_verify_result(rc->rc_ssl)) != 0){ /* Syslog cert failure */ if (ssl_x509_name_oneline(rc->rc_ssl, &oneline) < 0) goto done; if (oneline) clixon_log(h, LOG_NOTICE, "Cert error: %s: %s", oneline, X509_verify_cert_error_string(code)); else clixon_log(h, LOG_NOTICE, "Cert error: %s", X509_verify_cert_error_string(code)); /* Send return error message */ if ((cberr = cbuf_new()) == NULL){ clixon_err(OE_UNIX, errno, "cbuf_new"); goto done; } cprintf(cberr, "HTTP cert verification failed: %s[%ld]", X509_verify_cert_error_string(code), code); if (netconf_invalid_value_xml(&xerr, "protocol", cbuf_get(cberr)) < 0) goto done; if ((media_str = restconf_param_get(h, "HTTP_ACCEPT")) == NULL){ media_out = YANG_DATA_JSON; } else if ((int)(media_out = restconf_media_str2int(media_str)) == -1){ if (strcmp(media_str, "*/*") == 0) /* catch-all */ media_out = YANG_DATA_JSON; } if (api_return_err0(sd->sd_conn->rc_h, sd, xerr, 1, media_out, 0) < 0) goto done; rc->rc_exit = 1; } retval = 0; done: if (oneline) free(oneline); if (cberr) cbuf_free(cberr); if (xerr) xml_free(xerr); return retval; } /* Write buf to socket * see also this function in restcont_api_openssl.c * @param[in] h Clixon handle * @param[in] buf Buffer to write * @param[in] buflen Length of buffer * @param[in] rc Connection struct * @param[in] callfn For debug * @retval 1 OK * @retval 0 OK, but socket write returned error, caller should close rc * @retval -1 Error */ static int native_buf_write(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_CLIENT, "%s %s buflen:%zu buf:\n%s", __FUNCTION__, 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_CLIENT, "%s write EAGAIN", __FUNCTION__); 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_CLIENT, "%s write EAGAIN", __FUNCTION__); 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_CLIENT, "%s retval:%d", __FUNCTION__, retval); return retval; closed: retval = 0; goto done; } /*! Send early handcoded bad request reply before actual packet received, just after accept * * @param[in] h Clixon handle * @param[in] media * @param[in] body If given add message body using media * @param[in] rc Restconf connection, note may be closed in this * @retval 1 OK * @retval 0 OK, but socket write returned error, caller should close rc * @retval -1 Error * @see restconf_badrequest which can only be called in a request context */ static int native_send_badrequest(clixon_handle h, char *media, char *body, restconf_conn *rc) { int retval = -1; cbuf *cb = NULL; clixon_debug(CLIXON_DBG_CLIENT, "%s", __FUNCTION__); if ((cb = cbuf_new()) == NULL){ clixon_err(OE_UNIX, errno, "cbuf_new"); goto done; } cprintf(cb, "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n"); if (body){ cprintf(cb, "Content-Type: %s\r\n", media); cprintf(cb, "Content-Length: %zu\r\n", strlen(body)+2); /* for \r\n */ } else cprintf(cb, "Content-Length: 0\r\n"); cprintf(cb, "\r\n"); if (body) cprintf(cb, "%s\r\n", body); retval = native_buf_write(h, cbuf_get(cb), cbuf_len(cb), rc, __FUNCTION__); done: if (cb) cbuf_free(cb); return retval; } #ifdef HAVE_HTTP1 /*! Clear all input stream data if input is interrupted for some reason * * Only used by HTTP/1. * @param[in] h Clixon handle * @param[in] sd Http stream * @retval 0 OK * @retval -1 Error */ static int http1_native_clear_input(clixon_handle h, restconf_stream_data *sd) { int retval = -1; cbuf_reset(sd->sd_indata); if (sd->sd_qvec){ cvec_free(sd->sd_qvec); sd->sd_qvec = NULL; } if (restconf_param_del_all(h) < 0) goto done; retval = 0; done: return retval; } #endif /*! Read HTTP from SSL socket * * @param[in] rc Restconf connection handle * @param[in] buf Input buffer * @param[in] sz Size of input buffer * @param[out] np Bytes read * @param[out] again If set, read data again, do not continue processing * @retval 0 OK * @retval -1 Error */ static int read_ssl(restconf_conn *rc, char *buf, size_t sz, ssize_t *np, int *again) { int retval = -1; int sslerr; if ((*np = SSL_read(rc->rc_ssl, buf, sz)) <= 0){ sslerr = SSL_get_error(rc->rc_ssl, *np); clixon_debug(CLIXON_DBG_CLIENT, "%s SSL_read() n:%zd errno:%d sslerr:%d", __FUNCTION__, *np, errno, sslerr); switch (sslerr){ case SSL_ERROR_WANT_READ: /* 2 */ /* SSL_ERROR_WANT_READ is returned when the last operation was a read operation * from a nonblocking BIO. * That is, it can happen if restconf_socket_init() below is called * with SOCK_NONBLOCK */ clixon_debug(CLIXON_DBG_CLIENT, "%s SSL_read SSL_ERROR_WANT_READ", __FUNCTION__); usleep(1000); *again = 1; break; case SSL_ERROR_ZERO_RETURN: /* 6 */ *np = 0; /* should already be zero */ break; default: clixon_log(rc->rc_h, LOG_WARNING, "%s SSL_read(): %s sslerr:%d", __FUNCTION__, strerror(errno), sslerr); *np = 0; break; } /* switch */ } retval = 0; // done: clixon_debug(CLIXON_DBG_CLIENT, "%s %d", __FUNCTION__, retval); return retval; } /*! Read HTTP from regular socket * * @param[in] rc Restconf connection handle * @param[in] buf Input buffer * @param[in] sz Size of input buffer * @param[out] np Bytes read * @param[out] again If set, read data again, do not continue processing * @retval 1 OK * @retval 0 Socket closed, quit * @retval -1 Error * XXX: * readmore/continue * goto ok */ static int read_regular(restconf_conn *rc, char *buf, size_t sz, ssize_t *np, int *again) { int retval = -1; if ((*np = read(rc->rc_s, buf, sz)) < 0){ /* XXX atomicio ? */ switch(errno){ case ECONNRESET:/* Connection reset by peer */ clixon_debug(CLIXON_DBG_CLIENT, "%s %d Connection reset by peer", __FUNCTION__, rc->rc_s); if (restconf_close_ssl_socket(rc, __FUNCTION__, 0) < 0) goto done; retval = 0; /* Close socket and ssl */ goto done; break; case EAGAIN: clixon_debug(CLIXON_DBG_CLIENT, "%s read EAGAIN", __FUNCTION__); usleep(1000); *again = 1; break; default:; clixon_err(OE_XML, errno, "read"); goto done; break; } } retval = 1; done: return retval; } #ifdef HAVE_HTTP1 /*! Restconf HTTP/1 processing after chunk of bytes read * * @param[in] rc Restconf connection handle * @param[in] buf Input buffer * @param[in] n Length of data in input buffer * @param[out] readmore If set, read data again, do not continue processing * @retval 1 OK * @retval 0 Socket closed, quit * @retval -1 Error */ static int restconf_http1_process(restconf_conn *rc, char *buf, size_t n, int *readmore) { int retval = -1; restconf_stream_data *sd; clixon_handle h; int ret; int status; cbuf *cberr = NULL; h = rc->rc_h; if ((sd = restconf_stream_find(rc, 0)) == NULL){ clixon_err(OE_RESTCONF, EINVAL, "restconf stream not found"); goto done; } /* Two states for reading: * 1) Initial reading of headers, parse from start * 2) Headers are read / body started, dont parse, just append body */ /* Check whole message is read. * Only way this could happen is that body is read * 0: No Content-Length or 0 * header does not contain Content-Length or is 0 * (OR: message header not fully read SHOULDNT HAPPEN IF BODY READ) * 1: Content-Length found but body has fewer bytes, ie remaining bytes to read * 2: Content-Length found and matches body length. No more bytes to read */ if ((ret = http1_check_content_length(h, sd, &status)) < 0) goto done; if (status == 1){ /* Next read: keep header state and only append inbody */ if (cbuf_append_buf(sd->sd_indata, buf, n) < 0){ clixon_err(OE_UNIX, errno, "cbuf_append"); goto done; } } else { /* multi-buffer for multiple reads * This is different from sd_indata that it is before and includes headers */ if (cbuf_append_buf(sd->sd_inbuf, buf, n) < 0){ clixon_err(OE_UNIX, errno, "cbuf_append"); goto done; } if (clixon_http1_parse_string(h, rc, cbuf_get(sd->sd_inbuf)) < 0){ /* XXX This does not work for SSL */ if (rc->rc_ssl){ ret = SSL_pending(rc->rc_ssl); } else if ((ret = clixon_event_poll(rc->rc_s)) < 0) goto done; if (ret > 0){ if (http1_native_clear_input(h, sd) < 0) goto done; (*readmore)++; goto ok; } /* Return error. Honestly, the sender could just e slow, it should really be a * timeout here. */ if ((cberr = cbuf_new()) == NULL){ clixon_err(OE_UNIX, errno, "cbuf_new"); goto done; } cprintf(cberr, "protocolmalformed-message%s", clixon_err_reason()); if ((ret = native_send_badrequest(h, "application/yang-data+xml", cbuf_get(cberr), rc)) < 0) goto done; if (http1_native_clear_input(h, sd) < 0) goto done; if (restconf_close_ssl_socket(rc, __FUNCTION__, 0) < 0) goto done; rc = NULL; goto closed; } /* Check for Continue and if so reply with 100 Continue * ret == 1: send reply */ if ((ret = http1_check_expect(h, rc, sd)) < 0) goto done; if (ret == 1){ if ((ret = native_buf_write(h, cbuf_get(sd->sd_outp_buf), cbuf_len(sd->sd_outp_buf), rc, __FUNCTION__)) < 0) goto done; cvec_reset(sd->sd_outp_hdrs); cbuf_reset(sd->sd_outp_buf); if (ret == 0){ if (restconf_close_ssl_socket(rc, __FUNCTION__, 0) < 0) goto done; rc = NULL; goto closed; } } } /* Check whole message is read. * Only way this could happen is that body is read * 0: No Content-Length or 0 * header does not contain Content-Length or is 0 * (OR: message header not fully read SHOULDNT HAPPEN IF BODY READ OK in parse above) * 1: Content-Length found but body has fewer bytes, ie remaining bytes to read * 2: Content-Length found and matches body length. No more bytes to read */ if ((ret = http1_check_content_length(h, sd, &status)) < 0) goto done; if (status == 1){ (*readmore)++; goto ok; } /* nginx compatible, set HTTPS parameter if SSL */ if (rc->rc_ssl) if (restconf_param_set(h, "HTTPS", "https") < 0) goto done; /* main restconf processing */ if (restconf_http1_path_root(h, rc) < 0) goto done; if ((ret = native_buf_write(h, cbuf_get(sd->sd_outp_buf), cbuf_len(sd->sd_outp_buf), rc, __FUNCTION__)) < 0) goto done; cvec_reset(sd->sd_outp_hdrs); /* Can be done in native_send_reply */ cbuf_reset(sd->sd_outp_buf); cbuf_reset(sd->sd_inbuf); cbuf_reset(sd->sd_indata); if (sd->sd_body) cbuf_reset(sd->sd_body); if (sd->sd_qvec){ cvec_free(sd->sd_qvec); sd->sd_qvec = NULL; } if (ret == 0 || rc->rc_exit){ /* Server-initiated exit */ if (restconf_close_ssl_socket(rc, __FUNCTION__, 0) < 0) goto done; goto closed; } ok: retval = 1; done: if (cberr) cbuf_free(cberr); return retval; closed: retval = 0; goto done; } #endif #ifdef HAVE_LIBNGHTTP2 #ifdef HAVE_HTTP1 static int restconf_http2_upgrade(restconf_conn *rc) { int retval = -1; restconf_stream_data *sd; if ((sd = restconf_stream_find(rc, 0)) == NULL){ clixon_err(OE_RESTCONF, EINVAL, "restconf stream not found"); goto done; } if (sd->sd_upgrade2){ nghttp2_error ngerr; /* Switch to http/2 according to RFC 7540 Sec 3.2 and RFC 7230 Sec 6.7 */ rc->rc_proto = HTTP_2; if (http2_session_init(rc) < 0){ restconf_close_ssl_socket(rc, __FUNCTION__, 0); goto done; } /* The HTTP/1.1 request that is sent prior to upgrade is assigned a * stream identifier of 1 (see Section 5.1.1) with default priority */ sd->sd_stream_id = 1; /* The first HTTP/2 frame sent by the server MUST be a server connection * preface (Section 3.5) consisting of a SETTINGS frame (Section 6.5). */ if ((ngerr = nghttp2_session_upgrade2(rc->rc_ngsession, sd->sd_settings2, sd->sd_settings2?strlen((const char*)sd->sd_settings2):0, 0, /* XXX: 1 if HEAD */ NULL)) < 0){ clixon_err(OE_NGHTTP2, ngerr, "nghttp2_session_upgrade2"); goto done; } if (http2_send_server_connection(rc) < 0){ restconf_close_ssl_socket(rc, __FUNCTION__, 0); goto done; } /* Use params from original http/1 session to http/2 stream */ if (http2_exec(rc, sd, rc->rc_ngsession, 1) < 0) goto done; /* * Very special case for http/1->http/2 upgrade and restconf "restart" * That is, the restconf daemon is restarted under the hood, and the session * is closed in mid-step: it needs a couple of extra rounds to complete the http/2 * settings before it completes. * Maybe a more precise way would be to encode that semantics using recieved http/2 * frames instead of just postponing nrof events? */ if (clixon_exit_get() == 1){ clixon_exit_set(3); } } retval = 0; done: return retval; } #endif /* HAVE_LIBHTTP1 */ /*! Restconf HTTP/2 processing after chunk of bytes read * * @param[in] rc Restconf connection * @param[in] buf Input buffer * @param[in] n Size of input buffer * @param[in] n Length of data in input buffer * @param[out] readmore If set, read data again, do not continue processing * @retval 1 OK * @retval 0 Socket closed, quit * @retval -1 Error */ static int restconf_http2_process(restconf_conn *rc, char *buf, size_t n, int *readmore) { int retval = -1; int ret; nghttp2_error ngerr; clixon_debug(CLIXON_DBG_CLIENT, "%s", __FUNCTION__); if (rc->rc_exit){ /* Server-initiated exit for http/2 */ if ((ngerr = nghttp2_session_terminate_session(rc->rc_ngsession, 0)) < 0){ clixon_err(OE_NGHTTP2, ngerr, "nghttp2_session_terminate_session %d", ngerr); goto done; // XXX not here in original? } } else { if ((ret = http2_recv(rc, (unsigned char *)buf, n)) < 0) goto done; if (ret == 0){ if (restconf_close_ssl_socket(rc, __FUNCTION__, 0) < 0) goto done; retval = 0; goto done; } /* Check if read more data frames */ ret = 0; if (rc->rc_ssl) ret = SSL_pending(rc->rc_ssl); else if ((ret = clixon_event_poll(rc->rc_s)) < 0) goto done; if (ret > 0) (*readmore)++; } retval = 1; done: clixon_debug(CLIXON_DBG_CLIENT, "%s %d", __FUNCTION__, retval); return retval; } #endif /* HAVE_LIBNGHTTP2 */ /*! Get restconf native handle * * @param[in] h Clixon handle * @retval rn Restconf native handle */ restconf_native_handle * restconf_native_handle_get(clixon_handle h) { clicon_hash_t *cdat = clicon_data(h); size_t len; void *p; if ((p = clicon_hash_value(cdat, "restconf-native-handle", &len)) != NULL) return *(restconf_native_handle **)p; return NULL; } /*---------------------------- Connect ---------------------------------*/ /*! New data connection after accept, receive and reply on data socket * * @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 * @note read buffer is limited. More data can be read in two ways: returns a buffer * with 100 Continue, in which case that is replied and the function returns and the client sends * more data. * OR returns 0 with no reply, then this is assumed to mean read more data from the socket. */ int restconf_connection(int s, void *arg) { int retval = -1; restconf_conn *rc = NULL; ssize_t n; char buf[1024]; /* Alter BUFSIZ (8K) from stdio.h 8K. 256 fails some tests */ int readmore = 1; int ret; clixon_debug(CLIXON_DBG_CLIENT, "%s %d", __FUNCTION__, s); if ((rc = (restconf_conn*)arg) == NULL){ clixon_err(OE_RESTCONF, EINVAL, "arg is NULL"); goto done; } if (s != rc->rc_s){ clixon_err(OE_RESTCONF, EINVAL, "s != rc->rc_s"); goto done; } gettimeofday(&rc->rc_t, NULL); /* activity timer */ while (readmore) { clixon_debug(CLIXON_DBG_CLIENT, "%s readmore", __FUNCTION__); readmore = 0; /* Example: curl -Ssik -u wilma:bar -X GET https://localhost/restconf/data/example:x */ if (rc->rc_ssl){ if (read_ssl(rc, buf, sizeof(buf), &n, &readmore) < 0) goto done; } else{ /* Not SSL */ if ((ret = read_regular(rc, buf, sizeof(buf), &n, &readmore)) < 0) goto done; if (ret == 0) goto ok; /* abort here */ } clixon_debug(CLIXON_DBG_CLIENT, "%s read:%zd", __FUNCTION__, n); if (readmore) continue; if (n == 0){ clixon_debug(CLIXON_DBG_CLIENT, "%s n=0 closing socket", __FUNCTION__); if (restconf_close_ssl_socket(rc, __FUNCTION__, 0) < 0) goto done; rc = NULL; goto ok; } switch (rc->rc_proto){ #ifdef HAVE_HTTP1 case HTTP_10: case HTTP_11: if ((ret = restconf_http1_process(rc, buf, n, &readmore)) < 0) goto done; if (ret == 0){ goto ok; } gettimeofday(&rc->rc_t, NULL); /* activity timer */ if (readmore) continue; #ifdef HAVE_LIBNGHTTP2 if (restconf_http2_upgrade(rc) < 0) goto done; #endif /* HAVE_LIBNGHTTP2 */ break; #endif /* HAVE_HTTP1 */ #ifdef HAVE_LIBNGHTTP2 case HTTP_2: if ((ret = restconf_http2_process(rc, buf, n, &readmore)) < 0) goto done; gettimeofday(&rc->rc_t, NULL); /* activity timer */ if (ret == 0) goto ok; break; #endif /* HAVE_LIBNGHTTP2 */ default: break; } /* switch rc_proto */ } /* while readmore */ ok: retval = 0; done: clixon_debug(CLIXON_DBG_CLIENT, "%s retval %d", __FUNCTION__, retval); return retval; } /* restconf_connection */ /*----------------------------- Close socket ------------------------------*/ /*! Close Restconf native connection socket and unregister callback * * For callhome also start reconnect timer * @param[in] rc rstconf connection * @retval 0 OK * @retval -1 Error */ static int restconf_connection_close1(restconf_conn *rc) { int retval = -1; restconf_socket *rsock; if (rc == NULL){ clixon_err(OE_RESTCONF, EINVAL, "rc is NULL"); goto done; } rsock = rc->rc_socket; clixon_debug(CLIXON_DBG_CLIENT, "%s \"%s\"", __FUNCTION__, rsock->rs_description); if (close(rc->rc_s) < 0){ clixon_err(OE_UNIX, errno, "close"); goto done; } clixon_event_unreg_fd(rc->rc_s, restconf_connection); /* re-set timer */ if (rc->rc_callhome){ if (rsock->rs_periodic) restconf_idle_timer_unreg(rc); if (restconf_callhome_timer(rsock, 1) < 0) goto done; } retval = 0; done: clixon_debug(CLIXON_DBG_CLIENT, "%s %d", __FUNCTION__, retval); return retval; } /*! Utility function to close restconf server ssl socket. * * There are many variants to closing, one could probably make this more generic * and always use this function, but it is difficult. * @param[in] rc restconf connection * @param[in] callfn For debug * @param[in] dontshutdown If != 0, do not shutdown * @retval 0 OK * @retval -1 Error */ int restconf_close_ssl_socket(restconf_conn *rc, const char *callfn, int dontshutdown) { int retval = -1; int ret; int sslerr; int er; clixon_debug(CLIXON_DBG_CLIENT, "%s %s", __FUNCTION__, callfn); if (rc->rc_ssl != NULL){ if (!dontshutdown && (ret = SSL_shutdown(rc->rc_ssl)) < 0){ er = errno; sslerr = SSL_get_error(rc->rc_ssl, ret); clixon_debug(CLIXON_DBG_CLIENT, "%s errno:%s(%d) sslerr:%d", __FUNCTION__, strerror(er), er, sslerr); if (sslerr == SSL_ERROR_SSL || /* 1 */ sslerr == SSL_ERROR_ZERO_RETURN){ /* 6 */ } else if (sslerr == SSL_ERROR_SYSCALL){ /* 5 */ /* Some non-recoverable, fatal I/O error occurred. The OpenSSL error queue may contain more information on the error. For socket I/O on Unix systems, consult errno for details. If this error occurs then no further I/O operations should be performed on the connection and SSL_shutdown() must not be called.*/ /* Ignore eg EBADF/ECONNRESET/EPIPE */ } else{ /* To avoid close again in restconf_native_terminate */ rc->rc_s = -1; clixon_err(OE_SSL, sslerr, "SSL_shutdown, %s err:%d %d", callfn, sslerr, er); goto done; } } SSL_free(rc->rc_ssl); rc->rc_ssl = NULL; } if (restconf_connection_close1(rc) < 0) goto done; if (restconf_conn_free(rc) < 0) goto done; retval = 0; done: clixon_debug(CLIXON_DBG_CLIENT, "%s retval:%d", __FUNCTION__, retval); return retval; } /*------------------------------ Accept--------------------------------*/ /*! Check ALPN result * * @param[in] h Clixon handle * @param[out] proto * @retval 1 OK with proto set * @retval 0 Fail, ALPN null or not recognized * @retval -1 Error */ static int ssl_alpn_check(clixon_handle h, const unsigned char *alpn, unsigned int alpnlen, restconf_conn *rc, restconf_http_proto *proto) { int retval = -1; cbuf *cberr = NULL; clixon_debug(CLIXON_DBG_CLIENT, "%s", __FUNCTION__); /* Alternatively, call restconf_str2proto but alpn is not a proper string */ if (alpn && alpnlen == 8 && memcmp("http/1.1", alpn, 8) == 0){ *proto = HTTP_11; } #ifdef HAVE_LIBNGHTTP2 else if (alpn && alpnlen == 2 && memcmp("h2", alpn, 2) == 0){ *proto = HTTP_2; } #endif else { if ((cberr = cbuf_new()) == NULL){ clixon_err(OE_UNIX, errno, "cbuf_new"); goto done; } if (alpn != NULL){ cprintf(cberr, "protocolmalformed-messageALPN: protocol not recognized: %s", alpn); clixon_log(h, LOG_INFO, "%s Warning: %s", __FUNCTION__, cbuf_get(cberr)); if (native_send_badrequest(h, "application/yang-data+xml", cbuf_get(cberr), rc) < 0) goto done; if (restconf_close_ssl_socket(rc, __FUNCTION__, 0) < 0) goto done; goto fail; } else{ #if defined(HAVE_HTTP1) #if defined(HAVE_LIBNGHTTP2) char *pstr; /* Both http/1 and http/2 */ int p = -1; pstr = clicon_option_str(h, "CLICON_NOALPN_DEFAULT"); if (pstr) p = restconf_str2proto(pstr); if (pstr == NULL || p == -1){ clixon_log(h, LOG_INFO, "%s Warning: ALPN: No protocol selected, or no ALPN?", __FUNCTION__); if (restconf_close_ssl_socket(rc, __FUNCTION__, 0) < 0) goto done; goto fail; } *proto = p; #else *proto = HTTP_11; /* Only http/1 */ #endif #else *proto = HTTP_2; /* Only http/1 */ #endif } } retval = 1; done: clixon_debug(CLIXON_DBG_CLIENT, "%s retval:%d", __FUNCTION__, retval); if (cberr) cbuf_free(cberr); return retval; fail: retval = 0; /* ALPN not OK */ goto done; } /* ssl_alpn_check */ /*! Accept new socket client. Note SSL not ip, this applies also to callhome * * @param[in] h Clixon handle * @param[in] s Socket (unix or ip) * @param[in] rsock Socket struct * @param[out] rcp Restconf connection, if present and retval=1 * @retval 1 OK, connection is up, rcp set * @retval 0 OK, but connection closed * @retval -1 Error * @see openssl_init_socket where this callback is registered */ int restconf_ssl_accept_client(clixon_handle h, int s, restconf_socket *rsock, restconf_conn **rcp) { int retval = -1; restconf_native_handle *rn = NULL; restconf_conn *rc = NULL; char *name = NULL; int ret; int e; int er; int readmore; const unsigned char *alpn = NULL; unsigned int alpnlen = 0; restconf_http_proto proto = HTTP_11; /* Non-SSL negotiation NYI */ clixon_debug(CLIXON_DBG_CLIENT, "%s", __FUNCTION__); #ifdef HAVE_LIBNGHTTP2 #ifndef HAVE_HTTP1 proto = HTTP_2; /* If nghttp2 only let default be 2.0 */ #endif #endif if ((rn = restconf_native_handle_get(h)) == NULL){ clixon_err(OE_XML, EFAULT, "No openssl handle"); goto done; } /* * Register callbacks for actual data socket */ if ((rc = restconf_conn_new(h, s, rsock)) == NULL) goto done; clixon_debug(CLIXON_DBG_CLIENT, "%s s:%d", __FUNCTION__, rc->rc_s); if (rsock->rs_ssl){ if ((rc->rc_ssl = SSL_new(rn->rn_ctx)) == NULL){ clixon_err(OE_SSL, 0, "SSL_new"); goto done; } clixon_debug(CLIXON_DBG_CLIENT, "%s SSL_new(%p)", __FUNCTION__, rc->rc_ssl); /* CCL_CTX_set_verify already set, need not call SSL_set_verify again for this server */ /* X509_CHECK_FLAG_NO_WILDCARDS disables wildcard expansion */ SSL_set_hostflags(rc->rc_ssl, X509_CHECK_FLAG_NO_WILDCARDS); #if 0 /* XXX This code is kept for the time being just for reference, it does not belong here. * If you want to restrict client certs to a specific set. * Otherwise this is done in restcon ca-auth callback and ultimately NACM * SSL_set1_host() sets the expected DNS hostname to name C = SE L = Stockholm O = Clixon OU = clixon CN = ca <--- emailAddress = olof@hagsand.se */ if (SSL_set1_host(rc->rc_ssl, "andy") != 1) { /* for peer cert */ clixon_err(OE_SSL, 0, "SSL_set1_host"); goto done; } if (SSL_add1_host(rc->rc_ssl, "olof") != 1) { /* for peer cert */ clixon_err(OE_SSL, 0, "SSL_set1_host"); goto done; } #endif if (SSL_set_fd(rc->rc_ssl, rc->rc_s) != 1){ clixon_err(OE_SSL, 0, "SSL_set_fd"); goto done; } readmore = 1; while (readmore){ readmore = 0; /* 1: OK, -1 fatal, 0: TLS/SSL handshake was not successful * Both error cases: Call SSL_get_error() with the return value ret */ if ((ret = SSL_accept(rc->rc_ssl)) != 1) { clixon_debug(CLIXON_DBG_CLIENT, "%s SSL_accept() ret:%d errno:%d", __FUNCTION__, ret, er=errno); e = SSL_get_error(rc->rc_ssl, ret); switch (e){ case SSL_ERROR_SSL: /* 1 */ clixon_debug(CLIXON_DBG_CLIENT, "%s SSL_ERROR_SSL (non-ssl message on ssl socket)", __FUNCTION__); #ifdef HTTP_ON_HTTPS_REPLY SSL_free(rc->rc_ssl); rc->rc_ssl = NULL; if (native_send_badrequest(h, "application/yang-data+xml", "protocolmalformed-messageThe plain HTTP request was sent to HTTPS port", rc) < 0) goto done; #endif if (restconf_close_ssl_socket(rc, __FUNCTION__, 1) < 0) goto done; goto closed; break; case SSL_ERROR_SYSCALL: /* 5 */ /* Some non-recoverable, fatal I/O error occurred. The OpenSSL error queue may contain more information on the error. For socket I/O on Unix systems, consult errno for details. If this error occurs then no further I/O operations should be performed on the connection and SSL_shutdown() must not be called.*/ clixon_debug(CLIXON_DBG_CLIENT, "%s SSL_accept() SSL_ERROR_SYSCALL %d", __FUNCTION__, er); if (restconf_close_ssl_socket(rc, __FUNCTION__, 1) < 0) goto done; rc = NULL; goto closed; break; case SSL_ERROR_WANT_READ: /* 2 */ case SSL_ERROR_WANT_WRITE: /* 3 */ /* SSL_ERROR_WANT_READ is returned when the last operation was a read operation * from a nonblocking BIO. * That is, it can happen if restconf_socket_init() below is called * with SOCK_NONBLOCK */ clixon_debug(CLIXON_DBG_CLIENT, "%s write SSL_ERROR_WANT_READ", __FUNCTION__); usleep(10000); readmore = 1; break; case SSL_ERROR_NONE: /* 0 */ case SSL_ERROR_ZERO_RETURN: /* 6 */ case SSL_ERROR_WANT_CONNECT: /* 7 */ case SSL_ERROR_WANT_ACCEPT: /* 8 */ case SSL_ERROR_WANT_X509_LOOKUP: /* 4 */ case SSL_ERROR_WANT_ASYNC: /* 8 */ case SSL_ERROR_WANT_ASYNC_JOB: /* 10 */ #ifdef SSL_ERROR_WANT_CLIENT_HELLO_CB case SSL_ERROR_WANT_CLIENT_HELLO_CB: /* 11 */ #endif default: clixon_err(OE_SSL, 0, "SSL_accept:%d", e); goto done; break; } } /* SSL_accept */ } /* while(readmore) */ /* Sets data and len to point to the client's requested protocol for this connection. */ #ifndef OPENSSL_NO_NEXTPROTONEG SSL_get0_next_proto_negotiated(rc->rc_ssl, &alpn, &alpnlen); #endif /* !OPENSSL_NO_NEXTPROTONEG */ if (alpn == NULL){ /* Returns a pointer to the selected protocol in data with length len. */ SSL_get0_alpn_selected(rc->rc_ssl, &alpn, &alpnlen); } if ((ret = ssl_alpn_check(h, alpn, alpnlen, rc, &proto)) < 0) goto done; if (ret == 0){ goto closed; } clixon_debug(CLIXON_DBG_CLIENT, "%s proto:%s", __FUNCTION__, restconf_proto2str(proto)); #if 0 /* Seems too early to fail here, instead let authentication callback deal with this */ /* For client-cert authentication, check if any certs are present, * if not, send bad request * Alt: set SSL_CTX_set_verify(ctx, SSL_VERIFY_FAIL_IF_NO_PEER_CERT) * but then SSL_accept fails. */ if (restconf_auth_type_get(h) == CLIXON_AUTH_CLIENT_CERTIFICATE){ X509 *peercert; #if OPENSSL_VERSION_NUMBER < 0x30000000L if ((peercert = SSL_get_peer_certificate(rc->rc_ssl)) != NULL){ X509_free(peercert); } #else if ((peercert = SSL_get1_peer_certificate(rc->rc_ssl)) != NULL){ X509_free(peercert); } #endif else { /* Get certificates (if available) */ if (proto != HTTP_2 && native_send_badrequest(h, rc->rc_s, rc->rc_ssl, "application/yang-data+xml", "protocolmalformed-messagePeer certificate required", rc->rc_socket, rc) < 0) goto done; if (restconf_close_ssl_socket(rc, __FUNCTION__, 0) < 0) goto done; goto closed; } } #endif /* Get the actual peer, XXX this maybe could be done in ca-auth client-cert code ? * Note this _only_ works if SSL_set1_host() was set previously,... */ if ((ret = SSL_get_verify_result(rc->rc_ssl)) == X509_V_OK) { /* for peer cert */ const char *peername = SSL_get0_peername(rc->rc_ssl); if (peername != NULL) { /* Name checks were in scope and matched the peername */ clixon_debug(CLIXON_DBG_CLIENT, "%s peername:%s", __FUNCTION__, peername); } } #if 0 else{ clixon_log(h, LOG_NOTICE, "Cert error: %s", X509_verify_cert_error_string(ret)); /* Maybe should return already here, but to get proper return message need to * continue to http/1 or http/2 handling * @see restconf_connection_sanity */ } #endif #if 0 /* debug */ if (clixon_debug_get()) restconf_listcerts(rc->rc_ssl); #endif } /* if ssl */ rc->rc_proto = proto; switch (rc->rc_proto){ #ifdef HAVE_HTTP1 case HTTP_10: case HTTP_11: /* Create a default stream for http/1 */ if (restconf_stream_data_new(rc, 0) == NULL) goto done; break; #endif /* HAVE_HTTP1 */ #ifdef HAVE_LIBNGHTTP2 case HTTP_2:{ if (http2_session_init(rc) < 0){ restconf_close_ssl_socket(rc, __FUNCTION__, 0); clixon_err_reset(); goto closed; } if (http2_send_server_connection(rc) < 0){ restconf_close_ssl_socket(rc, __FUNCTION__, 0); clixon_err_reset(); goto closed; } break; } #endif /* HAVE_LIBNGHTTP2 */ default: break; } /* switch proto */ gettimeofday(&rc->rc_t, NULL); /* activity timer */ if (clixon_event_reg_fd(rc->rc_s, restconf_connection, (void*)rc, "restconf client socket") < 0) goto done; if (rcp) *rcp = rc; retval = 1; /* OK, up */ done: clixon_debug(CLIXON_DBG_CLIENT, "%s retval %d", __FUNCTION__, retval); if (name) free(name); return retval; closed: retval = 0; /* OK, closed */ goto done; } /* restconf_ssl_accept_client */ static int restconf_idle_timer_set(struct timeval t, void *arg, char *descr) { int retval = -1; cbuf *cb = NULL; if ((cb = cbuf_new()) == NULL){ clixon_err(OE_UNIX, errno, "cbuf_new"); goto done; } cprintf(cb, "restconf idle timer %s", descr); if (clixon_event_reg_timeout(t, restconf_idle_cb, arg, cbuf_get(cb)) < 0) goto done; retval = 0; done: if (cb) cbuf_free(cb); return retval; } /*! Idle timeout timer callback * * @param[in] rc Restconf connection, more specifically: callhome connection * @retval 0 OK * @retval -1 Error * * t0 tp t1 tn * |---------|-----------|--------------------| * <---idle-timeout----> <---idle-timeout----> * <---td----> * t0: rc connection creation * tp: last packet activity * t1: timeout (when this cb is called first time) * td: duration from last activity * tn: next timeout, set to t1+idle-timeout. * XXX one could set tn = tp+idle-timeout but may lead to small intervals */ static int restconf_idle_cb(int fd, void *arg) { int retval = -1; restconf_socket *rsock; restconf_conn *rc; struct timeval now = {0,}; struct timeval td; struct timeval tn; struct timeval to = {0,}; if ((rc = (restconf_conn *)arg) == NULL){ clixon_err(OE_YANG, EINVAL, "rc is NULL"); goto done; } if ((rsock = rc->rc_socket) == NULL){ clixon_err(OE_YANG, EINVAL, "rsock is NULL"); goto done; } clixon_debug(CLIXON_DBG_CLIENT, "%s \"%s\"", __FUNCTION__, rsock->rs_description); if (rc->rc_callhome && rsock->rs_periodic && rc->rc_s > 0 && rsock->rs_idle_timeout){ gettimeofday(&now, NULL); timersub(&now, &rc->rc_t, &td); /* Last packet timestamp */ if (td.tv_sec >= rsock->rs_idle_timeout){ if (restconf_close_ssl_socket(rc, __FUNCTION__, 0) < 0) goto done; } else{ to.tv_sec = rsock->rs_idle_timeout; timeradd(&now, &to, &tn); clixon_debug(CLIXON_DBG_CLIENT, "%s now:%lu timeout:%lu.%lu", __FUNCTION__, now.tv_sec, tn.tv_sec, tn.tv_usec); if (restconf_idle_timer_set(tn, rc, rsock->rs_description) < 0) goto done; } } retval = 0; done: return retval; } int restconf_idle_timer_unreg(restconf_conn *rc) { return clixon_event_unreg_timeout(restconf_idle_cb, rc); } /*! Set callhome periodic idle-timeout * * 1) If callhome and periodic, set timer for t0+idle-timeout(ti) * 2) Timestamp any data passing on the socket(td) * 3) At timeout (ti) check if ti = td+idle-timeout (for first timeout same as t0=td), * if so close socket * 4) Else, reset timer to ti = td + idle-timeout. Goto (2) * socket. * XXX: now just timeout dont keep track of data (td) * @see restconf_idle_timer_unreg */ int restconf_idle_timer(restconf_conn *rc) { int retval = -1; struct timeval now; struct timeval t; struct timeval to = {0, 0}; restconf_socket *rsock; if (rc == NULL || !rc->rc_callhome){ clixon_err(OE_RESTCONF, EINVAL, "rc is NULL or not callhome"); goto done; } rsock = rc->rc_socket; if (rsock == NULL || !rsock->rs_periodic || rsock->rs_idle_timeout==0){ clixon_err(OE_YANG, EINVAL, "rsock is NULL or not periodic"); goto done; } clixon_debug(CLIXON_DBG_CLIENT, "%s \"%s\" register", __FUNCTION__, rsock->rs_description); gettimeofday(&now, NULL); to.tv_sec = rsock->rs_idle_timeout; timeradd(&now, &to, &t); if (restconf_idle_timer_set(t, rc, rsock->rs_description) < 0) goto done; retval = 0; done: return retval; } /*! Callhome timer callback * * @param[in] fd No-op * @param[in] arg restconf_socket * Can be called directly, but typically call wrapper restconf_callhome_timer instead * the reason is that this function may continue with SSL accept handling whereas the wrapper always * returns directly. */ static int restconf_callhome_cb(int fd, void *arg) { int retval = -1; clixon_handle h; restconf_socket *rsock = NULL; struct sockaddr_in6 sin6 = {0,}; // because its larger than sin and sa struct sockaddr *sa = (struct sockaddr *)&sin6; size_t sa_len; int s; restconf_conn *rc = NULL; int ret; rsock = (restconf_socket *)arg; if (rsock == NULL || !rsock->rs_callhome){ clixon_err(OE_YANG, EINVAL, "rsock is NULL"); goto done; } clixon_debug(CLIXON_DBG_CLIENT, "%s \"%s\"", __FUNCTION__, rsock->rs_description); h = rsock->rs_h; /* Already computed in restconf_socket_init, could be saved in rsock? */ if (clixon_inet2sin(rsock->rs_addrtype, rsock->rs_addrstr, rsock->rs_port, sa, &sa_len) < 0) goto done; if ((s = socket(sa->sa_family, SOCK_STREAM, 0)) < 0) { clixon_err(OE_UNIX, errno, "socket"); goto done; } if (connect(s, sa, sa_len) < 0){ clixon_debug(CLIXON_DBG_CLIENT, "%s connect %hu fail:%d %s", __FUNCTION__, rsock->rs_port, errno, strerror(errno)); close(s); rsock->rs_attempts++; /* Fail: Initiate new timer */ if (restconf_callhome_timer(rsock, 0) < 0) goto done; } else { clixon_debug(CLIXON_DBG_CLIENT, "%s connect %hu OK", __FUNCTION__, rsock->rs_port); rsock->rs_attempts = 0; if ((ret = restconf_ssl_accept_client(h, s, rsock, &rc)) < 0) goto done; /* ret == 0 means already closed */ if (ret == 1 && rsock->rs_periodic && rsock->rs_idle_timeout){ if (restconf_idle_timer(rc) < 0) goto done; } } retval = 0; done: return retval; } int restconf_callhome_timer_unreg(restconf_socket *rsock) { return clixon_event_unreg_timeout(restconf_callhome_cb, rsock); } /*! Set callhome timer, which tries to connect to callhome client * * Implement callhome re-connect strategies in ietf-restconf-server.yang * NYI: start-with, anchor-time * @param[in] rsock restconf_socket * @param[in] new if periodic: 1: Force a new period * @retval 0 OK * @retval -1 Error * @see restconf_callhome_timer_unreg */ int restconf_callhome_timer(restconf_socket *rsock, int status) { int retval = -1; struct timeval now; struct timeval t; struct timeval t1 = {0, 0}; cbuf *cb = NULL; if (rsock == NULL || !rsock->rs_callhome){ clixon_err(OE_YANG, EINVAL, "rsock is NULL or not callhome"); goto done; } clixon_debug(CLIXON_DBG_CLIENT, "%s \"%s\"", __FUNCTION__, rsock->rs_description); if (!rsock->rs_callhome) goto ok; /* shouldnt happen */ gettimeofday(&now, NULL); if (rsock->rs_periodic){ if ((status == 1) || rsock->rs_attempts >= rsock->rs_max_attempts){ rsock->rs_period_nr++; rsock->rs_attempts = 0; t1.tv_sec = rsock->rs_start + rsock->rs_period_nr*rsock->rs_period; while (t1.tv_sec < now.tv_sec) t1.tv_sec += rsock->rs_period; t = t1; } else { t1.tv_sec = 1; timeradd(&now, &t1, &t); } } else{ /* persistent: try again: attempts? */ t1.tv_sec = 1; timeradd(&now, &t1, &t); } if ((cb = cbuf_new()) == NULL){ clixon_err(OE_UNIX, errno, "cbuf_new"); goto done; } cprintf(cb, "restconf callhome timer %s", rsock->rs_description); if (rsock->rs_description) clixon_debug(CLIXON_DBG_CLIENT, "%s registering \"%s\": +%lu", __FUNCTION__, rsock->rs_description, t.tv_sec-now.tv_sec); else clixon_debug(CLIXON_DBG_CLIENT, "%s: %lu", __FUNCTION__, t.tv_sec); /* Should be only place restconf_callhome_cb is registered */ if (clixon_event_reg_timeout(t, restconf_callhome_cb, rsock, cbuf_get(cb)) < 0) goto done; ok: retval = 0; done: if (cb) cbuf_free(cb); return retval; } /*! Extract socket info from backend config * * @param[in] h Clixon handle * @param[in] xs socket config * @param[in] nsc Namespace context * @param[out] rsock restconf socket data, filled in with many fields * @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 TCP Port * @retval 0 OK * @retval -1 Error */ int restconf_socket_extract(clixon_handle h, cxobj *xs, cvec *nsc, restconf_socket *rsock, char **namespace, char **address, char **addrtype, uint16_t *port) { 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){ clixon_err(OE_XML, EINVAL, "Mandatory namespace not given"); goto done; } *namespace = xml_body(x); if ((x = xpath_first(xs, nsc, "description")) != NULL){ if ((rsock->rs_description = strdup(xml_body(x))) == NULL){ clixon_err(OE_UNIX, errno, "strdup"); goto done; } } if ((x = xpath_first(xs, nsc, "address")) == NULL){ clixon_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){ clixon_err(OE_UNIX, errno, "cv_dup"); goto done; } if ((ret = cv_parse1(body, cv, &reason)) < 0){ clixon_err(OE_XML, errno, "cv_parse1"); goto done; } if (ret == 0){ clixon_err(OE_XML, EFAULT, "%s", reason); goto done; } if ((ret = ys_cv_validate(h, cv, y, &ysub, &reason)) < 0) goto done; if (ret == 0){ clixon_err(OE_XML, EFAULT, "Validation os address: %s", reason); goto done; } if (ysub == NULL){ clixon_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){ clixon_err(OE_XML, errno, "parse_uint16"); goto done; } if (ret == 0){ clixon_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) rsock->rs_ssl = 0; else if (strcmp(str, "true") == 0) rsock->rs_ssl = 1; else { clixon_err(OE_XML, EINVAL, "Unrecognized value of ssl: %s", str); goto done; } } if (xpath_first(xs, nsc, "call-home") != NULL){ rsock->rs_callhome = 1; if (xpath_first(xs, nsc, "call-home/connection-type/persistent") != NULL){ rsock->rs_periodic = 0; } else if (xpath_first(xs, nsc, "call-home/connection-type/periodic") != NULL){ rsock->rs_periodic = 1; if ((x = xpath_first(xs, nsc, "call-home/connection-type/periodic/period")) != NULL && (str = xml_body(x)) != NULL){ if ((ret = parse_uint32(str, &rsock->rs_period, &reason)) < 0){ clixon_err(OE_XML, errno, "parse_uint16"); goto done; } if (ret == 0){ clixon_err(OE_XML, EINVAL, "Unrecognized value of period: %s", str); goto done; } } if ((x = xpath_first(xs, nsc, "call-home/connection-type/periodic/idle-timeout")) != NULL && (str = xml_body(x)) != NULL){ if ((ret = parse_uint16(str, &rsock->rs_idle_timeout, &reason)) < 0){ clixon_err(OE_XML, errno, "parse_uint16"); goto done; } if (ret == 0){ clixon_err(OE_XML, EINVAL, "Unrecognized value of idle-timeout: %s", str); goto done; } } } if ((x = xpath_first(xs, nsc, "call-home/reconnect-strategy/max-attempts")) != NULL && (str = xml_body(x)) != NULL){ if ((ret = parse_uint8(str, &rsock->rs_max_attempts, &reason)) < 0){ clixon_err(OE_XML, errno, "parse_uint8"); goto done; } if (ret == 0){ clixon_err(OE_XML, EINVAL, "Unrecognized value of max-attempts: %s", str); goto done; } } } retval = 0; done: if (cv) cv_free(cv); if (reason) free(reason); return retval; }