clixon/apps/restconf/restconf_nghttp2.c
2021-06-03 22:47:54 +02:00

727 lines
20 KiB
C

/*
*
***** BEGIN LICENSE BLOCK *****
Copyright (C) 2009-2019 Olof Hagsand
Copyright (C) 2020-2021 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
/* 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 <stdio.h>
#include <unistd.h>
#include <string.h>
#include <syslog.h>
#include <pwd.h>
#include <ctype.h>
#include <assert.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/resource.h>
#include <openssl/ssl.h>
#include <openssl/rand.h>
#include <openssl/err.h>
#include <openssl/x509v3.h>
/* cligen */
#include <cligen/cligen.h>
/* clicon */
#include <clixon/clixon.h>
#ifdef HAVE_LIBEVHTP /* To get restconf_native.h include files right */
/* evhtp */
#include <event2/buffer.h> /* evbuffer */
#define EVHTP_DISABLE_REGEX
#define EVHTP_DISABLE_EVTHR
#include <evhtp/evhtp.h>
#endif /* HAVE_LIBEVHTP */
#ifdef HAVE_LIBNGHTTP2
#include <nghttp2/nghttp2.h>
#endif
/* restconf */
#include "restconf_lib.h" /* generic shared with plugins */
#include "restconf_handle.h"
#include "restconf_api.h" /* generic not shared with plugins */
#include "restconf_err.h"
#include "restconf_root.h"
#include "restconf_native.h" /* Restconf-openssl mode specific headers*/
#include "restconf_nghttp2.h" /* Restconf-openssl mode specific headers*/
#ifdef HAVE_LIBNGHTTP2 /* XXX MOVE */
#define ARRLEN(x) (sizeof(x) / sizeof(x[0]))
static restconf_stream_data *
restconf_stream_data_new(restconf_conn_h *rc,
int32_t stream_id)
{
restconf_stream_data *sd;
sd = malloc(sizeof(restconf_stream_data));
memset(sd, 0, sizeof(restconf_stream_data));
sd->sd_stream_id = stream_id;
sd->sd_fd = -1;
INSQ(sd, rc->rc_streams);
return sd;
}
#ifdef NOTUSED
static void
delete_http2_stream_data(restconf_stream_data *sd)
{
if (sd->fd != -1) {
close(sd->fd);
}
free(sd->request_path);
free(sd);
}
#endif
#ifdef NOTUSED
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;
}
#endif /* NOTUSED */
/* Clixon error category specialized log callback for nghttp2
* @param[in] handle Application-specific handle
* @param[in] suberr Application-specific handle
* @param[out] cb Read log/error string into this buffer
*/
int
clixon_nghttp2_log_cb(void *handle,
int suberr,
cbuf *cb)
{
clicon_debug(1, "%s", __FUNCTION__);
cprintf(cb, "Fatal error: %s", nghttp2_strerror(suberr));
return 0;
}
#ifdef NOTUSED
static void
nghttp2_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
nghttp2_print_headers(nghttp2_nv *nva,
size_t nvlen)
{
size_t i;
for (i = 0; i < nvlen; ++i)
nghttp2_print_header(nva[i].name, nva[i].namelen, nva[i].value, nva[i].valuelen);
}
#endif /* NOTUSED */
/*! Transmit the |data|, |length| bytes, to the network.
* This callback is required if the application uses
* `nghttp2_session_send()` to send data to the remote endpoint. If
* the application uses solely `nghttp2_session_mem_send()` instead,
* this callback function is unnecessary.
*/
static ssize_t
send_callback(nghttp2_session *session,
const uint8_t *data,
size_t length,
int flags,
void *user_data)
{
restconf_conn_h *rc = (restconf_conn_h *)user_data;
int ret;
clicon_debug(1, "%s %zu:", __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(rc->rc_ssl, data, length)) < 0)
return ret;
return ret;
}
/*! Invoked when |session| wants to receive data from the remote peer.
*/
static ssize_t
recv_callback(nghttp2_session *session,
uint8_t *buf,
size_t length,
int flags,
void *user_data)
{
// restconf_conn_h *rc = (restconf_conn_h *)user_data;
clicon_debug(1, "%s", __FUNCTION__);
return 0;
}
/*! Callback for each incoming http request for path /
*
* This are all messages except /.well-known, Registered with evhtp_set_cb
*
* @param[in] req evhtp http request structure defining the incoming message
* @param[in] arg cx_evhtp handle clixon specific fields
* @retval void
* Discussion: problematic if fatal error -1 is returneod from clixon routines
* without actually terminating. Consider:
* 1) sending some error? and/or
* 2) terminating the process?
*/
static int
restconf_nghttp2_root(restconf_conn_h *rc)
{
int retval = -1;
clicon_handle h;
// int ret;
cvec *qvec = NULL;
clicon_debug(1, "------------");
if ((h = rc->rc_h) == NULL){
clicon_err(OE_RESTCONF, EINVAL, "arg is NULL");
goto done;
}
/* 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;
}
/* call generic function */
if (api_root_restconf(h, rc, qvec) < 0)
goto done;
// /* Clear (fcgi) paramaters from this request */
// if (restconf_param_del_all(h) < 0)
// goto done;
retval = 0;
done:
clicon_debug(1, "%s %d", __FUNCTION__, retval);
/* Catch all on fatal error. This does not terminate the process but closes request stream */
// if (retval < 0){
// evhtp_send_reply(req, EVHTP_RES_ERROR);
// }
if (qvec)
cvec_free(qvec);
return retval; /* void */
}
/*!
*/
static int
on_frame_recv_callback(nghttp2_session *session,
const nghttp2_frame *frame,
void *user_data)
{
int retval = -1;
restconf_conn_h *rc = (restconf_conn_h *)user_data;
restconf_stream_data *sd;
char *path;
clicon_debug(1, "%s %d", __FUNCTION__, frame->hd.stream_id);
switch (frame->hd.type) {
case NGHTTP2_DATA:
case NGHTTP2_HEADERS:
/* Check that the client request has finished */
if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
/* For DATA and HEADERS frame, this callback may be called after
on_stream_close_callback. Check that stream still alive. */
if ((sd = nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)) == NULL)
return 0;
if ((path = restconf_uripath(rc->rc_h)) == NULL)
goto ok;
if (strcmp(path, "/" RESTCONF_API) == 0){
if (restconf_nghttp2_root(rc) < 0)
goto done;
}
else if (strcmp(path, RESTCONF_WELL_KNOWN) == 0){
}
else
; /* ignore */
}
break;
default:
break;
}
ok:
retval = 0;
done:
return retval;
}
/*!
*/
static int
on_invalid_frame_recv_callback(nghttp2_session *session,
const nghttp2_frame *frame,
int lib_error_code,
void *user_data)
{
// restconf_conn_h *rc = (restconf_conn_h *)user_data;
clicon_debug(1, "%s", __FUNCTION__);
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)
{
// restconf_conn_h *rc = (restconf_conn_h *)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
before_frame_send_callback(nghttp2_session *session,
const nghttp2_frame *frame,
void *user_data)
{
// restconf_conn_h *rc = (restconf_conn_h *)user_data;
clicon_debug(1, "%s", __FUNCTION__);
return 0;
}
/*!
*/
static int
on_frame_send_callback(nghttp2_session *session,
const nghttp2_frame *frame,
void *user_data)
{
// restconf_conn_h *rc = (restconf_conn_h *)user_data;
clicon_debug(1, "%s", __FUNCTION__);
return 0;
}
/*!
*/
static int
on_frame_not_send_callback(nghttp2_session *session,
const nghttp2_frame *frame,
int lib_error_code,
void *user_data)
{
// restconf_conn_h *rc = (restconf_conn_h *)user_data;
clicon_debug(1, "%s", __FUNCTION__);
return 0;
}
/*!
*/
static int
on_stream_close_callback(nghttp2_session *session,
int32_t stream_id,
nghttp2_error_code error_code,
void *user_data)
{
// restconf_conn_h *rc = (restconf_conn_h *)user_data;
clicon_debug(1, "%s", __FUNCTION__);
//session_data *sd = (session_data*)user_data;
return 0;
}
/*! Reception of header block in HEADERS or PUSH_PROMISE is started.
*/
static int
on_begin_headers_callback(nghttp2_session *session,
const nghttp2_frame *frame,
void *user_data)
{
restconf_conn_h *rc = (restconf_conn_h *)user_data;
restconf_stream_data *sd;
clicon_debug(1, "%s", __FUNCTION__);
if (frame->hd.type == NGHTTP2_HEADERS &&
frame->headers.cat == NGHTTP2_HCAT_REQUEST) {
sd = restconf_stream_data_new(rc, frame->hd.stream_id);
nghttp2_session_set_stream_user_data(session, frame->hd.stream_id, sd);
}
return 0;
}
#ifdef XXX
/*! Translate http header by capitalizing, prepend w HTTP_ and - -> _
* Example: Host -> HTTP_HOST
*/
static int
evhtp_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; i<strlen(hdr->key); 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;
}
#endif
/*! Map from nghttp2 headers to "fcgi" type parameters used in clixon code
* Both |name| and |value| are guaranteed to be NULL-terminated.
*/
static int
nghttp2_hdr2clixon(clicon_handle h,
char *name,
char *value)
{
int retval = -1;
if (strcmp(name, ":path") == 0){
/* XXX "/restconf" Is PATH really REQUEST_URI? */
if (restconf_param_set(h, "REQUEST_URI", value) < 0) /* XXX string? */
goto done;
}
else if (strcmp(name, ":method") == 0){
if (restconf_param_set(h, "REQUEST_METHOD", value) < 0)
goto done;
}
else if (strcmp(name, ":scheme") == 0){
if (strcmp(value, "https") == 0 &&
restconf_param_set(h, "HTTPS", "https") < 0) /* some string or NULL */
goto done;
}
else if (strcmp(name, ":authority") == 0){
if (restconf_param_set(h, "HTTP_HOST", value) < 0)
goto done;
}
else if (restconf_convert_hdr(h, name, value) < 0)
goto done;
retval = 0;
done:
return retval;
}
/*! Header name/value pair is received
* Both |name| and |value| are guaranteed to be NULL-terminated.
* If the application uses `nghttp2_session_mem_recv()`, it can return
* :enum:`NGHTTP2_ERR_PAUSE` to make `nghttp2_session_mem_recv()`
* return without processing further input bytes.
*/
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)
{
int retval = -1;
restconf_conn_h *rc = (restconf_conn_h *)user_data;
restconf_stream_data *sd;
clicon_debug(1, "%s %d:", __FUNCTION__, frame->hd.stream_id);
switch (frame->hd.type){
case NGHTTP2_HEADERS:
assert (frame->headers.cat == NGHTTP2_HCAT_REQUEST);
clicon_debug(1, "%s %s %s", __FUNCTION__, name, value);
if ((sd = nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)) == NULL)
break;
if (nghttp2_hdr2clixon(rc->rc_h, (char*)name, (char*)value) < 0)
goto done;
break;
default:
break;
}
retval = 0;
done:
return retval;
}
/*!
*/
static ssize_t
select_padding_callback(nghttp2_session *session,
const nghttp2_frame *frame,
size_t max_payloadlen,
void *user_data)
{
// restconf_conn_h *rc = (restconf_conn_h *)user_data;
clicon_debug(1, "%s", __FUNCTION__);
return 0;
}
/*!
*/
static ssize_t
data_source_read_length_callback(nghttp2_session *session,
uint8_t frame_type,
int32_t stream_id,
int32_t session_remote_window_size,
int32_t stream_remote_window_size,
uint32_t remote_max_frame_size,
void *user_data)
{
// restconf_conn_h *rc = (restconf_conn_h *)user_data;
clicon_debug(1, "%s", __FUNCTION__);
return 0;
}
/*! Invoked when a frame header is received.
* Unlike :type:`nghttp2_on_frame_recv_callback`, this callback will
* also be called when frame header of CONTINUATION frame is received.
*/
static int
on_begin_frame_callback(nghttp2_session *session,
const nghttp2_frame_hd *hd,
void *user_data)
{
// restconf_conn_h *rc = (restconf_conn_h *)user_data;
clicon_debug(1, "%s type:%d", __FUNCTION__, hd->type);
if (hd->type == NGHTTP2_CONTINUATION)
assert(0);
return 0;
}
/*!
*/
static int
send_data_callback(nghttp2_session *session,
nghttp2_frame *frame,
const uint8_t *framehd, size_t length,
nghttp2_data_source *source,
void *user_data)
{
// restconf_conn_h *rc = (restconf_conn_h *)user_data;
clicon_debug(1, "%s", __FUNCTION__);
return 0;
}
/*!
*/
static ssize_t
pack_extension_callback(nghttp2_session *session,
uint8_t *buf, size_t len,
const nghttp2_frame *frame,
void *user_data)
{
// restconf_conn_h *rc = (restconf_conn_h *)user_data;
clicon_debug(1, "%s", __FUNCTION__);
return 0;
}
/*!
*/
static int
unpack_extension_callback(nghttp2_session *session,
void **payload,
const nghttp2_frame_hd *hd,
void *user_data)
{
// restconf_conn_h *rc = (restconf_conn_h *)user_data;
clicon_debug(1, "%s", __FUNCTION__);
return 0;
}
/*!
*/
static int
on_extension_chunk_recv_callback(nghttp2_session *session,
const nghttp2_frame_hd *hd,
const uint8_t *data,
size_t len,
void *user_data)
{
// restconf_conn_h *rc = (restconf_conn_h *)user_data;
clicon_debug(1, "%s", __FUNCTION__);
return 0;
}
/*!
*/
static int
error_callback(nghttp2_session *session,
const char *msg,
size_t len,
void *user_data)
{
// restconf_conn_h *rc = (restconf_conn_h *)user_data;
clicon_debug(1, "%s", __FUNCTION__);
return 0;
}
/*!
*/
static int
error_callback2(nghttp2_session *session,
int lib_error_code,
const char *msg,
size_t len,
void *user_data)
{
// restconf_conn_h *rc = (restconf_conn_h *)user_data;
clicon_debug(1, "%s", __FUNCTION__);
return 0;
}
/*
* XXX see session_recv
*/
int
http2_recv(restconf_conn_h *rc,
const unsigned char *buf,
size_t n)
{
int retval = -1;
nghttp2_error ngerr;
clicon_debug(1, "%s", __FUNCTION__);
if (rc->rc_ngsession == NULL){
clicon_err(OE_RESTCONF, EINVAL, "No nghttp2 session");
goto done;
}
if ((ngerr = nghttp2_session_mem_recv(rc->rc_ngsession, buf, n)) < 0){
clicon_err(OE_NGHTTP2, ngerr, "nghttp2_session_mem_recv");
goto done;
}
retval = 0;
done:
return retval;
}
/*! Initialize callbacks
*/
int
http2_session_init(restconf_conn_h *rc)
{
nghttp2_session_callbacks *callbacks = NULL;
nghttp2_session *session = NULL;
nghttp2_session_callbacks_new(&callbacks);
nghttp2_session_callbacks_set_send_callback(callbacks, send_callback);
nghttp2_session_callbacks_set_recv_callback(callbacks, recv_callback);
nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, on_frame_recv_callback);
nghttp2_session_callbacks_set_on_invalid_frame_recv_callback(callbacks, on_invalid_frame_recv_callback);
nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks, on_data_chunk_recv_callback);
nghttp2_session_callbacks_set_before_frame_send_callback(callbacks, before_frame_send_callback);
nghttp2_session_callbacks_set_on_frame_send_callback(callbacks, on_frame_send_callback);
nghttp2_session_callbacks_set_on_frame_not_send_callback(callbacks, on_frame_not_send_callback);
nghttp2_session_callbacks_set_on_stream_close_callback(callbacks, on_stream_close_callback);
nghttp2_session_callbacks_set_on_begin_headers_callback(callbacks, on_begin_headers_callback);
nghttp2_session_callbacks_set_on_header_callback(callbacks, on_header_callback);
nghttp2_session_callbacks_set_select_padding_callback(callbacks, select_padding_callback);
nghttp2_session_callbacks_set_data_source_read_length_callback(callbacks, data_source_read_length_callback);
nghttp2_session_callbacks_set_on_begin_frame_callback(callbacks, on_begin_frame_callback);
nghttp2_session_callbacks_set_send_data_callback(callbacks, send_data_callback);
nghttp2_session_callbacks_set_pack_extension_callback(callbacks, pack_extension_callback);
nghttp2_session_callbacks_set_unpack_extension_callback(callbacks, unpack_extension_callback);
nghttp2_session_callbacks_set_on_extension_chunk_recv_callback(callbacks, on_extension_chunk_recv_callback);
nghttp2_session_callbacks_set_error_callback(callbacks, error_callback);
nghttp2_session_callbacks_set_error_callback2(callbacks, error_callback2);
/* Register callbacks with nghttp2 */
nghttp2_session_server_new(&session, callbacks, rc);
nghttp2_session_callbacks_del(callbacks);
rc->rc_ngsession = session;
return 0;
}
#endif /* HAVE_LIBNGHTTP2 */