w-level general upgrades and as a complement to module-specific upgrade. * Called on startup after initial XML parsing, but before module-specific upgrades * Enabled by definign the `.ca_datastore_upgrade` * [General-purpose upgrade documentation](https://clixon-docs.readthedocs.io/en/latest/backend.html#ge neral-purpose) * JSON parse error messages change from ` on line x: syntax error,..` to `json_parse: line x: syntax err or` * Unknown-element error message is more descriptive, eg from `namespace is: urn:example:clixon` to: `Fai led to find YANG spec of XML node: x with parent: xp in namespace urn:example:clixon`. * C-API parse and validation API more capable * `xml_spec_populate` family of functions extended with three-value return values * -1: error, 0: parse OK, 1: parse and YANG binding OK. * `xml_parse` and `json_parse` API changes * Three value returns: -1: error, 0: parse OK, 1: parse and YANG binding OK. * Extended `xml_parse_file2` and `xml_parse_string2` extended API functions with all options available. * New concept called `yang_bind` that defines how XML symbols are bound to YANG after parsing * Existing API same except `xml_parse_file` `endtag` argument moved to `xml_parse_file2` * C-API: Added instrumentation: `xml_size` and `xml_stats_get`. * Fixed: Enabling modstate (CLICON_XMLDB_MODSTATE), changing a revision on a yang, and restarting made the backend daemon exit at start (thanks Matt) * Also: ensure to load `ietf-yang-library.yang ` if CLICON_XMLDB_MODSTATE is set
555 lines
15 KiB
C
555 lines
15 KiB
C
/*
|
|
*
|
|
***** BEGIN LICENSE BLOCK *****
|
|
|
|
Copyright (C) 2009-2016 Olof Hagsand and Benny Holmgren
|
|
Copyright (C) 2017-2019 Olof Hagsand
|
|
Copyright (C) 2020 Olof Hagsand and Rubicon Communications, LLC
|
|
|
|
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 *****
|
|
|
|
* XML support functions.
|
|
* @see https://www.w3.org/TR/2008/REC-xml-20081126
|
|
* https://www.w3.org/TR/2009/REC-xml-names-20091208
|
|
* The function can do yang validation, process xml and json, etc.
|
|
* On success, nothing is printed and exitcode 0
|
|
* On failure, an error is printed on stderr and exitcode != 0
|
|
* Failure error prints are different, it would be nice to make them more
|
|
* uniform. (see clicon_rpc_generate_error)
|
|
*/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "clixon_config.h" /* generated by config & autoconf */
|
|
#endif
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
#include <syslog.h>
|
|
#include <netdb.h> /* gethostbyname */
|
|
#include <arpa/inet.h> /* inet_pton */
|
|
#include <netinet/tcp.h> /* TCP_NODELAY */
|
|
|
|
|
|
#include <openssl/ssl.h>
|
|
#include <nghttp2/nghttp2.h>
|
|
|
|
/* cligen */
|
|
#include <cligen/cligen.h>
|
|
|
|
/* clixon */
|
|
#include "clixon/clixon.h"
|
|
|
|
#define UTIL_GRPC_OPTS "hD:H:"
|
|
|
|
#define ARRLEN(x) (sizeof(x) / sizeof(x[0]))
|
|
|
|
/* User data handle to nghttp2 lib */
|
|
typedef struct {
|
|
int sd_s;
|
|
SSL *sd_ssl;
|
|
nghttp2_session *sd_session;
|
|
int32_t sd_stream_id;
|
|
} session_data;
|
|
|
|
#define MAKE_NV(NAME, VALUE, VALUELEN) \
|
|
{ \
|
|
(uint8_t *)NAME, (uint8_t *)VALUE, sizeof(NAME) - 1, VALUELEN, \
|
|
NGHTTP2_NV_FLAG_NONE \
|
|
}
|
|
|
|
#define MAKE_NV2(NAME, VALUE) \
|
|
{ \
|
|
(uint8_t *)NAME, (uint8_t *)VALUE, sizeof(NAME) - 1, sizeof(VALUE) - 1, \
|
|
NGHTTP2_NV_FLAG_NONE \
|
|
}
|
|
|
|
#if 1 /* DEBUG */
|
|
static void
|
|
print_header(const uint8_t *name,
|
|
size_t namelen,
|
|
const uint8_t *value,
|
|
size_t valuelen)
|
|
{
|
|
clicon_debug(1, "%s %s", name, value);
|
|
}
|
|
|
|
/* Print HTTP headers to |f|. Please note that this function does not
|
|
take into account that header name and value are sequence of
|
|
octets, therefore they may contain non-printable characters. */
|
|
static void
|
|
print_headers(nghttp2_nv *nva,
|
|
size_t nvlen)
|
|
{
|
|
size_t i;
|
|
|
|
for (i = 0; i < nvlen; ++i)
|
|
print_header(nva[i].name, nva[i].namelen, nva[i].value, nva[i].valuelen);
|
|
}
|
|
#endif /* DEBUG */
|
|
|
|
|
|
/* Transmit the |data|, |length| bytes, to the network.
|
|
* type is: nghttp2_on_header_callback
|
|
*/
|
|
static ssize_t
|
|
send_callback(nghttp2_session *session,
|
|
const uint8_t *data,
|
|
size_t length,
|
|
int flags,
|
|
void *user_data)
|
|
{
|
|
session_data *sd = (session_data*)user_data;
|
|
int ret;
|
|
|
|
|
|
clicon_debug(1, "%s %d:", __FUNCTION__, length);
|
|
#if 0
|
|
{
|
|
int i;
|
|
for (i=0; i<length; i++)
|
|
fprintf(stderr, "%02x", data[i]&255);
|
|
fprintf(stderr, "\n");
|
|
}
|
|
#endif
|
|
/* encrypt & send message */
|
|
if ((ret = SSL_write(sd->sd_ssl, data, length)) < 0)
|
|
return ret;
|
|
return ret;
|
|
}
|
|
|
|
/*!
|
|
*/
|
|
static int
|
|
on_frame_recv_callback(nghttp2_session *session,
|
|
const nghttp2_frame *frame,
|
|
void *user_data)
|
|
{
|
|
//session_data *sd = (session_data*)user_data;
|
|
|
|
clicon_debug(1, "%s %d", __FUNCTION__, frame->hd.stream_id);
|
|
if (frame->hd.type == NGHTTP2_HEADERS &&
|
|
frame->headers.cat == NGHTTP2_HCAT_RESPONSE)
|
|
clicon_debug(1, "All headers received %d", frame->hd.stream_id);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*!
|
|
*/
|
|
static int
|
|
on_data_chunk_recv_callback(nghttp2_session *session,
|
|
uint8_t flags,
|
|
int32_t stream_id,
|
|
const uint8_t *data,
|
|
size_t len,
|
|
void *user_data)
|
|
{
|
|
session_data *sd = (session_data*)user_data;
|
|
|
|
clicon_debug(1, "%s %d", __FUNCTION__, stream_id);
|
|
if (sd->sd_session == session &&
|
|
sd->sd_stream_id == stream_id)
|
|
fwrite(data, 1, len, stdout); /* This is where data is printed */
|
|
return 0;
|
|
}
|
|
|
|
/*!
|
|
*/
|
|
static int
|
|
on_stream_close_callback(nghttp2_session *session,
|
|
int32_t stream_id,
|
|
nghttp2_error_code error_code,
|
|
void *user_data)
|
|
{
|
|
//session_data *sd = (session_data*)user_data;
|
|
|
|
clicon_debug(1, "%s", __FUNCTION__);
|
|
return 0;
|
|
}
|
|
|
|
/*!
|
|
*/
|
|
static int
|
|
on_header_callback(nghttp2_session *session,
|
|
const nghttp2_frame *frame,
|
|
const uint8_t *name,
|
|
size_t namelen,
|
|
const uint8_t *value,
|
|
size_t valuelen,
|
|
uint8_t flags,
|
|
void *user_data)
|
|
{
|
|
// session_data *sd = (session_data*)user_data;
|
|
|
|
if (frame->hd.type == NGHTTP2_HEADERS &&
|
|
frame->headers.cat == NGHTTP2_HCAT_RESPONSE){
|
|
clicon_debug(1, "%s %d:", __FUNCTION__, frame->hd.stream_id);
|
|
print_header(name, namelen, value, valuelen);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*!
|
|
*/
|
|
static int
|
|
on_begin_headers_callback(nghttp2_session *session,
|
|
const nghttp2_frame *frame,
|
|
void *user_data)
|
|
{
|
|
// session_data *sd = (session_data*)user_data;
|
|
|
|
if (frame->hd.type == NGHTTP2_HEADERS &&
|
|
frame->headers.cat == NGHTTP2_HCAT_RESPONSE)
|
|
clicon_debug(1, "%s Response headers %d",
|
|
__FUNCTION__, frame->hd.stream_id);
|
|
return 0;
|
|
}
|
|
|
|
/*! Initialize callbacks
|
|
*/
|
|
static int
|
|
session_init(nghttp2_session **session,
|
|
session_data *sd)
|
|
{
|
|
nghttp2_session_callbacks *callbacks = NULL;
|
|
|
|
nghttp2_session_callbacks_new(&callbacks);
|
|
nghttp2_session_callbacks_set_send_callback(callbacks, send_callback);
|
|
nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks,
|
|
on_frame_recv_callback);
|
|
nghttp2_session_callbacks_set_on_data_chunk_recv_callback(
|
|
callbacks, on_data_chunk_recv_callback);
|
|
nghttp2_session_callbacks_set_on_stream_close_callback(
|
|
callbacks, on_stream_close_callback);
|
|
nghttp2_session_callbacks_set_on_header_callback(callbacks,
|
|
on_header_callback);
|
|
nghttp2_session_callbacks_set_on_begin_headers_callback(
|
|
callbacks, on_begin_headers_callback);
|
|
nghttp2_session_client_new(session, callbacks, sd);
|
|
|
|
nghttp2_session_callbacks_del(callbacks);
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
send_client_connection_header(nghttp2_session *session)
|
|
{
|
|
int retval = -1;
|
|
nghttp2_settings_entry iv[1] = {
|
|
{NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 100}};
|
|
int rv;
|
|
|
|
clicon_debug(1, "%s", __FUNCTION__);
|
|
/* client 24 bytes magic string will be sent by nghttp2 library */
|
|
rv = nghttp2_submit_settings(session, NGHTTP2_FLAG_NONE, iv, ARRLEN(iv));
|
|
if (rv != 0) {
|
|
clicon_err(OE_XML, 0, "Could not submit SETTINGS: %s", nghttp2_strerror(rv));
|
|
goto done;
|
|
}
|
|
retval = 0;
|
|
done:
|
|
return retval;
|
|
}
|
|
|
|
|
|
/*!
|
|
* Sets sd->sd_stream_id
|
|
*/
|
|
static int
|
|
submit_request(session_data *sd,
|
|
char *schema,
|
|
char *hostname,
|
|
char *path)
|
|
{
|
|
int retval = -1;
|
|
nghttp2_nv hdrs[] = {
|
|
MAKE_NV2(":method", "GET"),
|
|
MAKE_NV(":scheme", schema, strlen(schema)),
|
|
MAKE_NV(":authority", hostname, strlen(hostname)),
|
|
MAKE_NV(":path", path, strlen(path))
|
|
};
|
|
|
|
clicon_debug(1, "%s Request headers:", __FUNCTION__);
|
|
print_headers(hdrs, ARRLEN(hdrs));
|
|
if ((sd->sd_stream_id = nghttp2_submit_request(sd->sd_session,
|
|
NULL,
|
|
hdrs,
|
|
ARRLEN(hdrs),
|
|
NULL,
|
|
NULL)) < 0){
|
|
clicon_err(OE_XML, 0, "Could not submit HTTP request: %s",
|
|
nghttp2_strerror(sd->sd_stream_id));
|
|
goto done;
|
|
}
|
|
|
|
retval = 0;
|
|
done:
|
|
return retval;
|
|
}
|
|
|
|
static int
|
|
socket_connect_inet(char *hostname,
|
|
uint16_t port,
|
|
int *sock0)
|
|
{
|
|
int retval = -1;
|
|
int s = -1;
|
|
struct sockaddr_in addr;
|
|
int ret;
|
|
struct hostent *host;
|
|
int one = 1;
|
|
|
|
clicon_debug(1, "%s to %s:%hu", __FUNCTION__, hostname, port);
|
|
memset(&addr, 0, sizeof(addr));
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_port = htons(port);
|
|
if ((ret = inet_pton(addr.sin_family, hostname, &addr.sin_addr)) < 0){
|
|
clicon_err(OE_UNIX, errno, "inet_pton");
|
|
goto done;
|
|
}
|
|
if (ret == 0){ /* Try DNS NOTE OBSOLETE */
|
|
if ((host = gethostbyname(hostname)) == NULL){
|
|
clicon_err(OE_UNIX, errno, "gethostbyname");
|
|
goto done;
|
|
}
|
|
addr.sin_addr.s_addr = *(long*)(host->h_addr); /* XXX Just to get it to work */
|
|
}
|
|
/* special error handling to get understandable messages (otherwise ENOENT) */
|
|
if ((s = socket(addr.sin_family, SOCK_STREAM, 0)) < 0) {
|
|
clicon_err(OE_CFG, errno, "socket");
|
|
return -1;
|
|
}
|
|
if (connect(s, (struct sockaddr*)&addr, sizeof(addr)) < 0){
|
|
clicon_err(OE_CFG, errno, "connecting socket inet4");
|
|
close(s);
|
|
goto done;
|
|
}
|
|
/* libev requires this */
|
|
setsockopt(s, IPPROTO_TCP, TCP_NODELAY, (char *)&one, sizeof(one));
|
|
if (sock0 != NULL)
|
|
*sock0 = s;
|
|
retval = 0;
|
|
done:
|
|
if (sock0 == NULL && s >= 0)
|
|
close(s);
|
|
return retval;
|
|
}
|
|
|
|
/* NPN TLS extension client callback. We check that server advertised
|
|
the HTTP/2 protocol the nghttp2 library supports. If not, exit
|
|
the program. */
|
|
static int
|
|
select_next_proto_cb(SSL *ssl,
|
|
unsigned char **out,
|
|
unsigned char *outlen,
|
|
const unsigned char *in,
|
|
unsigned int inlen,
|
|
void *arg)
|
|
{
|
|
clicon_debug(1, "%s", __FUNCTION__);
|
|
if (nghttp2_select_next_protocol(out, outlen, in, inlen) <= 0)
|
|
return -1;
|
|
clicon_debug(1, "%s out: %s in:%s", __FUNCTION__, *out, in);
|
|
return SSL_TLSEXT_ERR_OK;
|
|
}
|
|
|
|
|
|
static SSL_CTX*
|
|
InitCTX(void)
|
|
{
|
|
const SSL_METHOD *method;
|
|
SSL_CTX *ctx;
|
|
|
|
#if 1
|
|
method = SSLv23_client_method();
|
|
#else
|
|
OpenSSL_add_all_algorithms(); /* Load cryptos, et.al. */
|
|
SSL_load_error_strings(); /* Bring in and register error messages */
|
|
method = TLSv1_2_client_method(); /* Create new client-method instance */
|
|
#endif
|
|
/* Create new context */
|
|
if ((ctx = SSL_CTX_new(method)) == NULL){
|
|
clicon_err(OE_XML, errno, "SSL_CTX_new");
|
|
goto done;
|
|
}
|
|
|
|
SSL_CTX_set_options(ctx,
|
|
SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 |
|
|
SSL_OP_NO_COMPRESSION |
|
|
SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION);
|
|
#ifndef OPENSSL_NO_NEXTPROTONEG
|
|
SSL_CTX_set_next_proto_select_cb(ctx, select_next_proto_cb, NULL);
|
|
#endif
|
|
#if OPENSSL_VERSION_NUMBER >= 0x10002000L
|
|
SSL_CTX_set_alpn_protos(ctx, (const unsigned char *)"\x02h2", 3);
|
|
#endif
|
|
|
|
done:
|
|
return ctx;
|
|
}
|
|
|
|
static int
|
|
ssl_input_cb(int s,
|
|
void *arg)
|
|
{
|
|
int retval = -1;
|
|
session_data *sd = (session_data *)arg;
|
|
SSL *ssl;
|
|
char buf[1024];
|
|
int n;
|
|
nghttp2_session *session;
|
|
int readlen;
|
|
|
|
ssl = sd->sd_ssl;
|
|
session = sd->sd_session;
|
|
/* get reply & decrypt */
|
|
if ((n = SSL_read(ssl, buf, sizeof(buf))) < 0){
|
|
clicon_err(OE_XML, errno, "SSL_read");
|
|
goto done;
|
|
}
|
|
if (n == 0){
|
|
fprintf(stdout, "%s closed\n", __FUNCTION__);
|
|
goto done;
|
|
}
|
|
if ((readlen = nghttp2_session_mem_recv(session, (unsigned char*)buf, n)) < 0){
|
|
clicon_err(OE_XML, errno, "nghttp2_session_mem_recv");
|
|
goto done;
|
|
}
|
|
nghttp2_session_send(session);
|
|
#if 0
|
|
buf[n] = 0;
|
|
fprintf(stdout, "%s\n", buf);
|
|
#endif
|
|
retval = 0;
|
|
done:
|
|
return retval;
|
|
}
|
|
|
|
static int
|
|
usage(char *argv0)
|
|
{
|
|
fprintf(stderr, "usage:%s [options] with xml on stdin\n"
|
|
"where options are\n"
|
|
"\t-h \t\tHelp\n"
|
|
"\t-D <level> \tDebug\n"
|
|
"\t-H <hostname> \tURI hostname\n"
|
|
,
|
|
argv0);
|
|
exit(0);
|
|
}
|
|
|
|
int
|
|
main(int argc,
|
|
char **argv)
|
|
{
|
|
int retval = -1;
|
|
clicon_handle h;
|
|
int c;
|
|
char *hostname = NULL;
|
|
uint16_t port = 443;
|
|
SSL_CTX *ctx = NULL;
|
|
int ss = -1;
|
|
SSL *ssl;
|
|
int ret;
|
|
nghttp2_session *session = NULL;
|
|
session_data *sd = NULL;
|
|
|
|
/* In the startup, logs to stderr & debug flag set later */
|
|
clicon_log_init(__FILE__, LOG_INFO, CLICON_LOG_STDERR);
|
|
|
|
if ((h = clicon_handle_init()) == NULL)
|
|
goto done;
|
|
while ((c = getopt(argc, argv, UTIL_GRPC_OPTS)) != -1)
|
|
switch (c) {
|
|
case 'h':
|
|
usage(argv[0]);
|
|
break;
|
|
case 'D':
|
|
if (sscanf(optarg, "%d", &debug) != 1)
|
|
usage(argv[0]);
|
|
break;
|
|
case 'H': /* hostname */
|
|
hostname = optarg;
|
|
break;
|
|
default:
|
|
usage(argv[0]);
|
|
break;
|
|
}
|
|
if (hostname == NULL){
|
|
fprintf(stderr, "-H <hostname> is mandatory\n");
|
|
usage(argv[0]);
|
|
}
|
|
SSL_library_init();
|
|
if ((ctx = InitCTX()) == NULL)
|
|
goto done;
|
|
if (socket_connect_inet(hostname, port, &ss) < 0)
|
|
goto done;
|
|
ssl = SSL_new(ctx); /* create new SSL connection state */
|
|
SSL_set_fd(ssl, ss); /* attach the socket descriptor */
|
|
/* perform the connection */
|
|
if ((ret = SSL_connect(ssl)) < 0){
|
|
clicon_err(OE_XML, errno, "SSL_connect");
|
|
goto done;
|
|
}
|
|
/* In the nghttp2 code, there is an asynchronous step for
|
|
* a connected socket, here I just assume it is connected. */
|
|
if ((sd = malloc(sizeof(*sd))) == NULL){
|
|
clicon_err(OE_UNIX, errno, "malloc");
|
|
goto done;
|
|
}
|
|
memset(sd, 0, sizeof(*sd));
|
|
sd->sd_s = ss;
|
|
sd->sd_ssl = ssl;
|
|
if (session_init(&session, sd) < 0)
|
|
goto done;
|
|
sd->sd_session = session;
|
|
if (send_client_connection_header(session) < 0)
|
|
goto done;
|
|
if (submit_request(sd, "https", hostname, "/") < 0)
|
|
goto done;
|
|
if (nghttp2_session_send(session) != 0){
|
|
clicon_err(OE_XML, errno, "nghttp2_session_send");
|
|
goto done;
|
|
}
|
|
if (event_reg_fd(ss, ssl_input_cb, sd, "ssl socket") < 0)
|
|
goto done;
|
|
if (event_loop() < 0)
|
|
goto done;
|
|
retval = 0;
|
|
done:
|
|
if (ss != -1)
|
|
close(ss);
|
|
if (ctx)
|
|
SSL_CTX_free(ctx); /* release context */
|
|
return retval;
|
|
}
|
|
|
|
|