550 lines
17 KiB
C
550 lines
17 KiB
C
/*
|
|
*
|
|
***** BEGIN LICENSE BLOCK *****
|
|
|
|
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 *****
|
|
|
|
* HTTP/1.1 parser according to RFC 7230
|
|
*/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "clixon_config.h" /* generated by config & autoconf */
|
|
#endif
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
#include <inttypes.h>
|
|
#include <syslog.h>
|
|
#include <errno.h>
|
|
#include <signal.h>
|
|
#include <openssl/ssl.h>
|
|
|
|
#ifdef HAVE_LIBNGHTTP2
|
|
#include <nghttp2/nghttp2.h>
|
|
#endif
|
|
|
|
/* cligen */
|
|
#include <cligen/cligen.h>
|
|
|
|
/* clixon */
|
|
#include <clixon/clixon.h>
|
|
|
|
#include "restconf_handle.h"
|
|
#include "restconf_lib.h"
|
|
#include "restconf_root.h"
|
|
#include "restconf_native.h"
|
|
#include "restconf_api.h"
|
|
#include "restconf_err.h"
|
|
#include "clixon_http1_parse.h"
|
|
#include "restconf_http1.h"
|
|
#include "clixon_http_data.h"
|
|
#ifdef RESTCONF_NATIVE_STREAM
|
|
#include "restconf_stream.h"
|
|
#endif
|
|
|
|
/* Size of xml read buffer */
|
|
#define BUFLEN 1024
|
|
|
|
/*! HTTP/1 parsing function. Input is string and side-effect is populating connection structs
|
|
*
|
|
* @param[in] h Clixon handle
|
|
* @param[in] rc Restconf connection
|
|
* @param[in] str Pointer to string containing HTTP/1
|
|
* @param[in] filename Debug string identifying file or connection
|
|
* @retval 0 Parse OK
|
|
* @retval -1 Error
|
|
*/
|
|
static int
|
|
_http1_parse(clixon_handle h,
|
|
restconf_conn *rc,
|
|
char *str,
|
|
const char *filename)
|
|
{
|
|
int retval = -1;
|
|
clixon_http1_yacc hy = {0,};
|
|
int ret;
|
|
|
|
clixon_debug(CLIXON_DBG_PARSE, "%s", str);
|
|
if (strlen(str) == 0)
|
|
goto ok;
|
|
hy.hy_parse_string = str;
|
|
hy.hy_name = filename;
|
|
hy.hy_h = h;
|
|
hy.hy_rc = rc;
|
|
hy.hy_linenum = 1;
|
|
if (http1_scan_init(&hy) < 0)
|
|
goto done;
|
|
if (http1_parse_init(&hy) < 0)
|
|
goto done;
|
|
ret = clixon_http1_parseparse(&hy); /* yacc returns 1 on error */
|
|
/* yacc/lex terminates parsing after headers.
|
|
* Look for body after headers assuming str terminating with \n\n\0 and then <body> */
|
|
http1_parse_exit(&hy);
|
|
http1_scan_exit(&hy);
|
|
if (ret != 0){
|
|
if (filename)
|
|
clixon_log(h, LOG_NOTICE, "HTTP1 error: on line %d in %s", hy.hy_linenum, filename);
|
|
else
|
|
clixon_log(h, LOG_NOTICE, "HTTP1 error: on line %d", hy.hy_linenum);
|
|
if (clixon_err_category() == 0)
|
|
clixon_err(OE_RESTCONF, 0, "HTTP1 parser error with no error code (should not happen)");
|
|
goto done;
|
|
}
|
|
ok:
|
|
retval = 0;
|
|
done:
|
|
clixon_debug(CLIXON_DBG_PARSE, "retval:%d", retval);
|
|
return retval;
|
|
}
|
|
|
|
/*! HTTP/1 parsing function from file
|
|
*
|
|
* @param[in] h Clixon handle
|
|
* @param[in] rc Restconf connection
|
|
* @param[in] f A file descriptor containing HTTP/1 (as ASCII characters)
|
|
* @param[in] filename Debug string identifying file or connection
|
|
* @retval 0 Parse OK
|
|
* @retval -1 Error
|
|
*/
|
|
int
|
|
clixon_http1_parse_file(clixon_handle h,
|
|
restconf_conn *rc,
|
|
FILE *f,
|
|
const char *filename)
|
|
{
|
|
int retval = -1;
|
|
int ret;
|
|
char ch;
|
|
char *buf = NULL;
|
|
char *ptr;
|
|
int buflen = BUFLEN; /* start size */
|
|
int len = 0;
|
|
int oldbuflen;
|
|
|
|
clixon_debug(CLIXON_DBG_RESTCONF, "%s", filename);
|
|
if (f == NULL){
|
|
clixon_err(OE_RESTCONF, EINVAL, "f is NULL");
|
|
goto done;
|
|
}
|
|
if ((buf = malloc(buflen)) == NULL){
|
|
clixon_err(OE_XML, errno, "malloc");
|
|
goto done;
|
|
}
|
|
memset(buf, 0, buflen);
|
|
ptr = buf;
|
|
while (1){
|
|
if ((ret = fread(&ch, 1, 1, f)) < 0){
|
|
clixon_err(OE_XML, errno, "read");
|
|
break;
|
|
}
|
|
if (ret != 0){
|
|
buf[len++] = ch;
|
|
}
|
|
if (ret == 0) { /* buffer read */
|
|
if (_http1_parse(h, rc, ptr, filename) < 0)
|
|
goto done;
|
|
break;
|
|
}
|
|
if (len >= buflen-1){ /* Space: one for the null character */
|
|
oldbuflen = buflen;
|
|
buflen *= 2;
|
|
if ((buf = realloc(buf, buflen)) == NULL){
|
|
clixon_err(OE_XML, errno, "realloc");
|
|
goto done;
|
|
}
|
|
memset(buf+oldbuflen, 0, buflen-oldbuflen);
|
|
ptr = buf;
|
|
}
|
|
} /* while */
|
|
retval = 0;
|
|
done:
|
|
if (buf)
|
|
free(buf);
|
|
return retval;
|
|
}
|
|
|
|
/*! HTTP/1 parsing function from string
|
|
*
|
|
* @param[in] h Clixon handle
|
|
* @param[in] rc Restconf connection
|
|
* @param[in] str HTTP/1 string
|
|
* @retval 0 Parse OK
|
|
* @retval -1 Error
|
|
*/
|
|
int
|
|
clixon_http1_parse_string(clixon_handle h,
|
|
restconf_conn *rc,
|
|
char *str)
|
|
{
|
|
return _http1_parse(h, rc, str, "http1-parse");
|
|
}
|
|
|
|
/*! HTTP/1 parsing function from buffer (non-null terminated)
|
|
*
|
|
* Convert buffer to null-terminated string
|
|
* @param[in] h Clixon handle
|
|
* @param[in] rc Restconf connection
|
|
* @param[in] buf HTTP/1 buffer
|
|
* @param[in] n Length of buffer
|
|
* @retval 0 Parse OK
|
|
* @retval -1 Error
|
|
* @note Had preferred to do this without copying, OR
|
|
* input flex with a non-null terminated string
|
|
*/
|
|
int
|
|
clixon_http1_parse_buf(clixon_handle h,
|
|
restconf_conn *rc,
|
|
char *buf,
|
|
size_t n)
|
|
{
|
|
char *str = NULL;
|
|
int ret;
|
|
|
|
if ((str = malloc(n+1)) == NULL){
|
|
clixon_err(OE_RESTCONF, errno, "malloc");
|
|
return -1;
|
|
}
|
|
memcpy(str, buf, n);
|
|
str[n] = '\0';
|
|
ret = _http1_parse(h, rc, str, "http1-parse");
|
|
free(str);
|
|
return ret;
|
|
}
|
|
|
|
#ifdef HAVE_LIBNGHTTP2
|
|
/*! Check http/1 UPGRADE to http/2
|
|
*
|
|
* If upgrade headers are encountered AND http/2 is configured, then
|
|
* - add upgrade headers or signal error
|
|
* - set http2 flag get settings to and signal to upper layer to do the actual transition.
|
|
* @retval -1 Error
|
|
* @retval 0 Yes, upgrade dont proceed with request
|
|
* @retval 1 No upgrade, proceed with request
|
|
* @note currently upgrade header is checked always if nghttp2 is configured but may be a
|
|
* runtime config option
|
|
*/
|
|
static int
|
|
http1_upgrade_http2(clixon_handle h,
|
|
restconf_stream_data *sd)
|
|
{
|
|
int retval = -1;
|
|
char *str;
|
|
char *settings;
|
|
cxobj *xerr = NULL;
|
|
|
|
if ((str = restconf_param_get(h, "HTTP_UPGRADE")) != NULL &&
|
|
clicon_option_bool(h, "CLICON_RESTCONF_HTTP2_PLAIN") == 1){
|
|
/* Only accept "h2c" */
|
|
if (strcmp(str, "h2c") != 0){
|
|
if (netconf_invalid_value_xml(&xerr, "protocol", "Invalid upgrade token") < 0)
|
|
goto done;
|
|
if (api_return_err0(h, sd, xerr, 1, YANG_DATA_JSON, 0) < 0)
|
|
goto done;
|
|
if (xerr)
|
|
xml_free(xerr);
|
|
}
|
|
else {
|
|
if (restconf_reply_header(sd, "Connection", "Upgrade") < 0)
|
|
goto done;
|
|
if (restconf_reply_header(sd, "Upgrade", "h2c") < 0)
|
|
goto done;
|
|
if (restconf_reply_send(sd, 101, NULL, 0) < 0) /* Switch protocol */
|
|
goto done;
|
|
/* Signal http/2 upgrade to http/2 to upper restconf_connection handling */
|
|
sd->sd_upgrade2 = 1;
|
|
if ((settings = restconf_param_get(h, "HTTP_HTTP2_Settings")) != NULL &&
|
|
(sd->sd_settings2 = (uint8_t*)strdup(settings)) == NULL){
|
|
clixon_err(OE_UNIX, errno, "strdup");
|
|
goto done;
|
|
}
|
|
}
|
|
retval = 0; /* Yes, upgrade or error */
|
|
}
|
|
else
|
|
retval = 1; /* No upgrade, proceed with request */
|
|
done:
|
|
return retval;
|
|
}
|
|
#endif /* HAVE_LIBNGHTTP2 */
|
|
|
|
/*! Construct an HTTP/1 reply (dont actually send it)
|
|
*/
|
|
static int
|
|
restconf_http1_reply(restconf_conn *rc,
|
|
restconf_stream_data *sd)
|
|
{
|
|
int retval = -1;
|
|
cg_var *cv;
|
|
|
|
clixon_debug(CLIXON_DBG_RESTCONF, "");
|
|
/* If body, add a content-length header
|
|
* A server MUST NOT send a Content-Length header field in any response
|
|
* with a status code of 1xx (Informational) or 204 (No Content). A
|
|
* server MUST NOT send a Content-Length header field in any 2xx
|
|
* (Successful) response to a CONNECT request (Section 4.3.6 of
|
|
* [RFC7231]).
|
|
*/
|
|
if (sd->sd_code != 204 && sd->sd_code > 199 && !rc->rc_event_stream)
|
|
if (restconf_reply_header(sd, "Content-Length", "%zu", sd->sd_body_len) < 0)
|
|
goto done;
|
|
/* Create reply and write headers */
|
|
#if 0 /* XXX need some keep-alive logic here */
|
|
/* protocol is HTTP/1.0 and clients wants to keep established */
|
|
if (restconf_reply_header(sd, "Connection", "keep-alive") < 0)
|
|
goto done;
|
|
#endif
|
|
cprintf(sd->sd_outp_buf, "HTTP/%u.%u %u %s\r\n",
|
|
rc->rc_proto_d1,
|
|
rc->rc_proto_d2,
|
|
sd->sd_code,
|
|
restconf_code2reason(sd->sd_code));
|
|
/* Loop over headers */
|
|
cv = NULL;
|
|
while ((cv = cvec_each(sd->sd_outp_hdrs, cv)) != NULL)
|
|
cprintf(sd->sd_outp_buf, "%s: %s\r\n", cv_name_get(cv), cv_string_get(cv));
|
|
cprintf(sd->sd_outp_buf, "\r\n");
|
|
/* Write a body */
|
|
if (sd->sd_body){
|
|
if (cbuf_append_buf(sd->sd_outp_buf, cbuf_get(sd->sd_body), cbuf_len(sd->sd_body)) < 0){
|
|
clixon_err(OE_RESTCONF, errno, "cbuf_append_buf");
|
|
goto done;
|
|
}
|
|
cbuf_free(sd->sd_body);
|
|
sd->sd_body = NULL;
|
|
}
|
|
retval = 0;
|
|
done:
|
|
return retval;
|
|
}
|
|
|
|
/*! restconf http1 path root
|
|
*
|
|
* @param[in] h Clixon handle
|
|
* @param[in] rc Clixon request connect pointer
|
|
* @retval 0 OK
|
|
* @retval -1 Error
|
|
*/
|
|
int
|
|
restconf_http1_path_root(clixon_handle h,
|
|
restconf_conn *rc)
|
|
{
|
|
int retval = -1;
|
|
restconf_stream_data *sd = NULL;
|
|
cvec *cvv = NULL;
|
|
char *cn;
|
|
char *subject = NULL;
|
|
cxobj *xerr = NULL;
|
|
int pretty;
|
|
#ifdef HAVE_LIBNGHTTP2
|
|
int ret;
|
|
#endif
|
|
|
|
clixon_debug(CLIXON_DBG_RESTCONF, "------------");
|
|
pretty = restconf_pretty_get(h);
|
|
if ((sd = restconf_stream_find(rc, 0)) == NULL){
|
|
clixon_err(OE_RESTCONF, EINVAL, "No stream_data");
|
|
goto done;
|
|
}
|
|
/* Sanity check */
|
|
if (restconf_param_get(h, "REQUEST_URI") == NULL){
|
|
if (netconf_invalid_value_xml(&xerr, "protocol", "Missing REQUEST_URI ") < 0)
|
|
goto done;
|
|
/* Select json as default since content-type header may not be accessible yet */
|
|
if (api_return_err0(h, sd, xerr, pretty, YANG_DATA_JSON, 0) < 0)
|
|
goto done;
|
|
goto fail;
|
|
}
|
|
if ((rc->rc_proto != HTTP_10 && rc->rc_proto != HTTP_11) ||
|
|
rc->rc_proto_d1 != 1 ||
|
|
(rc->rc_proto_d2 != 0 && rc->rc_proto_d2 != 1)){
|
|
if (netconf_invalid_value_xml(&xerr, "protocol", "Invalid HTTP version number") < 0)
|
|
goto done;
|
|
/* Select json as default since content-type header may not be accessible yet */
|
|
if (api_return_err0(h, sd, xerr, pretty, YANG_DATA_JSON, 0) < 0)
|
|
goto done;
|
|
goto fail;
|
|
}
|
|
#if 1
|
|
/* XXX gives mem leak in multiple requests,
|
|
* but maybe the error is that sd is not freed.
|
|
*/
|
|
if (sd->sd_path != NULL){
|
|
free(sd->sd_path);
|
|
sd->sd_path = NULL;
|
|
}
|
|
#endif
|
|
if ((sd->sd_path = restconf_uripath(rc->rc_h)) == NULL)
|
|
goto done; // XXX SHOULDNT EXIT if no REQUEST_URI
|
|
if (rc->rc_proto_d2 == 0 && rc->rc_proto == HTTP_11)
|
|
rc->rc_proto = HTTP_10;
|
|
else if (rc->rc_proto_d2 == 1 && rc->rc_proto != HTTP_10)
|
|
rc->rc_proto = HTTP_11;
|
|
if (rc->rc_ssl != NULL){
|
|
/* Slightly awkward way of taking SSL cert subject and CN and add it to restconf parameters
|
|
* instead of accessing it directly
|
|
* SSL subject fields, eg CN (Common Name) , can add more here? */
|
|
if (ssl_x509_name_oneline(rc->rc_ssl, &subject) < 0)
|
|
goto done;
|
|
if (subject != NULL) {
|
|
if (uri_str2cvec(subject, '/', '=', 1, &cvv) < 0)
|
|
goto done;
|
|
if ((cn = cvec_find_str(cvv, "CN")) != NULL){
|
|
if (restconf_param_set(h, "SSL_CN", cn) < 0)
|
|
goto done;
|
|
}
|
|
}
|
|
}
|
|
/* Check sanity of session, eg ssl client cert validation, may set rc_exit */
|
|
if (restconf_connection_sanity(h, rc, sd) < 0)
|
|
goto done;
|
|
if (rc->rc_exit)
|
|
goto fail;
|
|
#ifdef HAVE_LIBNGHTTP2
|
|
if ((ret = http1_upgrade_http2(h, sd)) < 0)
|
|
goto done;
|
|
if (ret == 0) /* upgrade */
|
|
goto upgrade;
|
|
#endif
|
|
/* Matching algorithm:
|
|
* 1. try well-known
|
|
* 2. try /restconf
|
|
* 3. try /data
|
|
* 4. call restconf anyway (because it handles errors a la restconf)
|
|
* This is for the situation where data is / and /restconf is more specific
|
|
*/
|
|
if (strcmp(sd->sd_path, RESTCONF_WELL_KNOWN) == 0){
|
|
if (api_well_known(h, sd) < 0)
|
|
goto done;
|
|
}
|
|
else if (api_path_is_restconf(h)){
|
|
if (api_root_restconf(h, sd, sd->sd_qvec) < 0)
|
|
goto done;
|
|
}
|
|
else if (api_path_is_data(h)){
|
|
if (api_http_data(h, sd, sd->sd_qvec) < 0)
|
|
goto done;
|
|
}
|
|
#ifdef RESTCONF_NATIVE_STREAM
|
|
else if (api_path_is_stream(h)){
|
|
restconf_socket *rs = rc->rc_socket;
|
|
if (api_stream(h, sd, sd->sd_qvec, rs->rs_stream_timeout, NULL) < 0)
|
|
goto done;
|
|
}
|
|
#endif
|
|
else
|
|
sd->sd_code = 404; /* catch all without body/media */
|
|
fail:
|
|
if (restconf_param_del_all(h) < 0)
|
|
goto done;
|
|
#ifdef HAVE_LIBNGHTTP2
|
|
upgrade:
|
|
#endif
|
|
if (sd->sd_code)
|
|
if (restconf_http1_reply(rc, sd) < 0)
|
|
goto done;
|
|
retval = 0;
|
|
done:
|
|
clixon_debug(CLIXON_DBG_RESTCONF, "retval:%d", retval);
|
|
if (subject)
|
|
free(subject);
|
|
if (xerr)
|
|
xml_free(xerr);
|
|
if (cvv)
|
|
cvec_free(cvv);
|
|
return retval;
|
|
}
|
|
|
|
/*! Check expect header, if found generate a Continue reply
|
|
*
|
|
* @param[in] h Clixon handle
|
|
* @param[in] rc Restconf connection
|
|
* @param[in] sd Restconf stream data (for http1 only stream 0)
|
|
* @retval 1 OK, Send continue
|
|
* @retval 0 OK, Dont send continue
|
|
* @retval -1 Error
|
|
* @see rfc7231 Sec 5.1.1
|
|
*/
|
|
int
|
|
http1_check_expect(clixon_handle h,
|
|
restconf_conn *rc,
|
|
restconf_stream_data *sd)
|
|
{
|
|
int retval = -1;
|
|
char *val;
|
|
|
|
if ((val = restconf_param_get(h, "HTTP_EXPECT")) != NULL &&
|
|
strcmp(val, "100-continue") == 0){ /* just drop if not well-formed */
|
|
sd->sd_code = 100;
|
|
if (restconf_http1_reply(rc, sd) < 0)
|
|
goto done;
|
|
retval = 1; /* send continue by flushing stream buffer after the call */
|
|
}
|
|
else
|
|
retval = 0;
|
|
done:
|
|
return retval;
|
|
}
|
|
|
|
/*! Is there more data to be read?
|
|
*
|
|
* Use Content-Length header as an indicator on the status of reading an input message:
|
|
* 0: No Content-Length or 0
|
|
* Either message header not fully read OR header does not contain Content-Length
|
|
* 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
|
|
* @param[in] h Clixon handle
|
|
* @param[in] sd Restconf stream data (for http1 only stream 0)
|
|
* @param[out] status 0-2, see description above
|
|
* @retval 0 OK, see status param
|
|
* @retval -1 Error
|
|
*/
|
|
int
|
|
http1_check_content_length(clixon_handle h,
|
|
restconf_stream_data *sd,
|
|
int *status)
|
|
{
|
|
int retval = -1;
|
|
char *val;
|
|
int len;
|
|
|
|
if ((val = restconf_param_get(h, "HTTP_CONTENT_LENGTH")) == NULL ||
|
|
(len = atoi(val)) == 0)
|
|
*status = 0;
|
|
else{
|
|
if (cbuf_len(sd->sd_indata) < len)
|
|
*status = 1;
|
|
else
|
|
*status = 2;
|
|
}
|
|
retval = 0;
|
|
return retval;
|
|
}
|