/* * ***** BEGIN LICENSE BLOCK ***** Copyright (C) 2009-2019 Olof Hagsand Copyright (C) 2020 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 ***** * libevhtp code */ #ifdef HAVE_CONFIG_H #include "clixon_config.h" /* generated by config & autoconf */ #endif /* The clixon evhtp code can be compiled with or without threading support * The choice is set at libevhtp compile time by cmake. Eg: * cmake -DEVHTP_DISABLE_EVTHR=ON # Disable threads. * Default in testing is disabled threads. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* chmod */ #include #include #include /* evhtp */ #include #include /* cligen */ #include /* clicon */ #include /* 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" /* Command line options to be passed to getopt(3) */ #define RESTCONF_OPTS "hD:f:E:l:p:d:y:a:u:ro:" /* See see listen(5) */ #define SOCKET_LISTEN_BACKLOG 16 /* clixon evhtp handle */ typedef struct { 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) { 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); } /*! Signall terminates process * XXX Try to get rid of code in signal handler -> so we can get rid of global variables */ static void restconf_sig_term(int arg) { static int i=0; 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){ // stream_child_freeall(_CLICON_HANDLE); restconf_terminate(_CLICON_HANDLE); } clicon_exit_set(); /* XXX should rather signal event_base_loop */ exit(-1); } static void restconf_sig_child(int arg) { int status; int pid; if ((pid = waitpid(-1, &status, 0)) != -1 && WIFEXITED(status)){ } } static char* evhtp_method2str(enum htp_method m) { switch (m){ case htp_method_GET: return "GET"; break; case htp_method_HEAD: return "HEAD"; break; case htp_method_POST: return "POST"; break; case htp_method_PUT: return "PUT"; break; case htp_method_DELETE: return "DELETE"; break; case htp_method_MKCOL: return "MKCOL"; break; case htp_method_COPY: return "COPY"; break; case htp_method_MOVE: return "MOVE"; break; case htp_method_OPTIONS: return "OPTIONS"; break; case htp_method_PROPFIND: return "PROPFIND"; break; case htp_method_PROPPATCH: return "PROPPATCH"; break; case htp_method_LOCK: return "LOCK"; break; case htp_method_UNLOCK: return "UNLOCK"; break; case htp_method_TRACE: return "TRACE"; break; case htp_method_CONNECT: return "CONNECT"; break; case htp_method_PATCH: return "PATCH"; break; default: return "UNKNOWN"; break; } } static int query_iterator(evhtp_header_t *hdr, void *arg) { cvec *qvec = (cvec *)arg; char *key; char *val; char *valu = NULL; /* unescaped value */ cg_var *cv; key = hdr->key; val = hdr->val; if (uri_percent_decode(val, &valu) < 0) return -1; if ((cv = cvec_add(qvec, CGV_STRING)) == NULL){ clicon_err(OE_UNIX, errno, "cvec_add"); return -1; } cv_name_set(cv, key); cv_string_set(cv, valu); if (valu) free(valu); return 0; } /*! Translate http header by capitalizing, prepend w HTTP_ and - -> _ * Example: Host -> HTTP_HOST */ static int convert_fcgi(evhtp_header_t *hdr, void *arg) { int retval = -1; clicon_handle h = (clicon_handle)arg; cbuf *cb = NULL; int i; char c; if ((cb = cbuf_new()) == NULL){ clicon_err(OE_UNIX, errno, "cbuf_new"); goto done; } /* convert key name */ cprintf(cb, "HTTP_"); for (i=0; ikey); i++){ c = hdr->key[i] & 0xff; if (islower(c)) cprintf(cb, "%c", toupper(c)); else if (c == '-') cprintf(cb, "_"); else cprintf(cb, "%c", c); } if (restconf_param_set(h, cbuf_get(cb), hdr->val) < 0) goto done; retval = 0; done: if (cb) cbuf_free(cb); return retval; } /*! Map from evhtp information to "fcgi" type parameters used in clixon code * * While all these params come via one call in fcgi, the information must be taken from * several different places in evhtp * @param[in] h Clicon handle * @param[in] req Evhtp request struct * @param[out] qvec Query parameters, ie the ?=&= stuff * @retval 1 OK continue * @retval 0 Fail, dont continue * @retval -1 Error * The following parameters are set: * QUERY_STRING * REQUEST_METHOD * REQUEST_URI * HTTPS * HTTP_HOST * HTTP_ACCEPT * HTTP_CONTENT_TYPE * @note there may be more used by an application plugin */ static int evhtp_params_set(clicon_handle h, evhtp_request_t *req, cvec *qvec) { int retval = -1; htp_method meth; evhtp_uri_t *uri; evhtp_path_t *path; evhtp_ssl_t *ssl = NULL; char *subject = NULL; cvec *cvv = NULL; char *cn; if ((uri = req->uri) == NULL){ clicon_err(OE_DAEMON, EFAULT, "No uri"); goto done; } if ((path = uri->path) == NULL){ clicon_err(OE_DAEMON, EFAULT, "No path"); goto done; } meth = evhtp_request_get_method(req); /* QUERY_STRING in fcgi but go direct to the info instead of putting it in a string? * This is different from all else: Ie one could have re-created a string here but * that would mean double parsing,... */ if (qvec && uri->query) if (evhtp_kvs_for_each(uri->query, query_iterator, qvec) < 0){ clicon_err(OE_CFG, errno, "evhtp_kvs_for_each"); goto done; } if (restconf_param_set(h, "REQUEST_METHOD", evhtp_method2str(meth)) < 0) goto done; if (restconf_param_set(h, "REQUEST_URI", path->full) < 0) goto done; clicon_debug(1, "%s proto:%d", __FUNCTION__, req->proto); if (req->proto != EVHTP_PROTO_10 && req->proto != EVHTP_PROTO_11){ if (restconf_badrequest(h, req) < 0) goto done; goto fail; } clicon_debug(1, "%s conn->ssl:%d", __FUNCTION__, req->conn->ssl?1:0); if ((ssl = req->conn->ssl) != NULL){ if (restconf_param_set(h, "HTTPS", "https") < 0) /* some string or NULL */ goto done; /* SSL subject fields, eg CN (Common Name) , can add more here? */ if ((subject = (char*)htp_sslutil_subject_tostr(req->conn->ssl)) != NULL){ if (str2cvec(subject, '/', '=', &cvv) < 0) goto done; if ((cn = cvec_find_str(cvv, "CN")) != NULL){ if (restconf_param_set(h, "SSL_CN", cn) < 0) goto done; } } } /* Translate all http headers by capitalizing, prepend w HTTP_ and - -> _ * Example: Host -> HTTP_HOST */ if (evhtp_headers_for_each(req->headers_in, convert_fcgi, h) < 0) goto done; retval = 1; done: if (subject) free(subject); if (cvv) cvec_free(cvv); return retval; fail: retval = 0; 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) { clicon_handle h = arg; 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; done: return; /* void */ } /*! /restconf callback * @see cx_genb */ static void cx_path_restconf(evhtp_request_t *req, void *arg) { clicon_handle h = arg; int ret; cvec *qvec = NULL; clicon_debug(1, "------------"); /* input debug */ if (clicon_debug_get()) evhtp_headers_for_each(req->headers_in, print_header, h); /* get accepted connection */ /* Query vector, ie the ?a=x&b=y stuff */ if ((qvec = cvec_new(0)) ==NULL){ clicon_err(OE_UNIX, errno, "cvec_new"); goto done; } /* set fcgi-like paramaters (ignore query vector) */ if ((ret = evhtp_params_set(h, req, qvec)) < 0) goto done; if (ret == 1){ /* call generic function */ if (api_root_restconf(h, req, qvec) < 0) goto done; } /* Clear (fcgi) paramaters from this request */ if (restconf_param_del_all(h) < 0) goto done; done: 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 */ 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; if (ssl_config == NULL || server_cert_path == NULL){ clicon_err(OE_CFG, EINVAL, "Input parameter is NULL"); 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); goto done; } retval = 0; done: 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 */ static int cx_get_ssl_client_ca_certs(clicon_handle h, const char *server_ca_cert_path, evhtp_ssl_cfg_t *ssl_config) { int retval = -1; struct stat f_stat; 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; fprintf(stderr, "%s %d\n", __FUNCTION__, pre_verify); 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()); 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; } /*! * * @param[out] addr Address as string, eg "0.0.0.0", "::" * @param[in] addrtype One of inet:ipv4-address or inet:ipv6-address * @param[out] ss Server socket (bound for accept) */ static int restconf_socket_init(clicon_handle h, const char *addr, const char *addrtype, uint16_t port, int *ss) { int retval = -1; int s = -1; struct sockaddr * sa; struct sockaddr_in6 sin6 = { 0 }; struct sockaddr_in sin = { 0 }; size_t sin_len; int on = 1; if (strcmp(addrtype, "inet:ipv6-address") == 0) { sin_len = sizeof(struct sockaddr_in6); sin6.sin6_port = htons(port); sin6.sin6_family = AF_INET6; evutil_inet_pton(AF_INET6, addr, &sin6.sin6_addr); sa = (struct sockaddr *)&sin6; } else if (strcmp(addrtype, "inet:ipv4-address") == 0) { sin_len = sizeof(struct sockaddr_in); sin.sin_family = AF_INET; sin.sin_port = htons(port); sin.sin_addr.s_addr = inet_addr(addr); sa = (struct sockaddr *)&sin; } else{ clicon_err(OE_XML, EINVAL, "Unexpected addrtype: %s", addrtype); return -1; } /* create inet socket */ if ((s = socket(sa->sa_family, SOCK_STREAM, 0)) < 0) { clicon_err(OE_UNIX, errno, "socket"); goto done; } evutil_make_socket_closeonexec(s); evutil_make_socket_nonblocking(s); if (setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, (void *)&on, sizeof(on)) == -1) { clicon_err(OE_UNIX, errno, "setsockopt SO_KEEPALIVE"); goto done; } if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (void *)&on, sizeof(on)) == -1) { clicon_err(OE_UNIX, errno, "setsockopt SO_REUSEADDR"); goto done; } /* only bind ipv6, otherwise it may bind to ipv4 as well which is strange but seems default */ if (sa->sa_family == AF_INET6 && setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) == -1) { clicon_err(OE_UNIX, errno, "setsockopt IPPROTO_IPV6"); goto done; } if (bind(s, sa, sin_len) == -1) { clicon_err(OE_UNIX, errno, "bind port %u", port); goto done; } if (listen(s, SOCKET_LISTEN_BACKLOG) < 0){ clicon_err(OE_UNIX, errno, "listen"); goto done; } if (ss) *ss = s; retval = 0; done: if (retval != 0 && s != -1) evutil_closesocket(s); return retval; // return evhtp_bind_sockaddr(htp, sa, sin_len, SOCKET_LISTEN_BACKLOG); } /*! Usage help routine * @param[in] argv0 command line * @param[in] h Clicon handle */ 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\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-d \t Specify restconf plugin directory dir (default: %s)\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