* Restconf native http/1, first working version

* Renamed files clixon_http -> restconf_http
  * Split main file into restconf_native.c
  * Remove all evhtp code and libevhtp/libevent dependency
This commit is contained in:
Olof hagsand 2022-01-27 17:05:52 +01:00
parent dadf4a778a
commit 4aa74fa1d8
27 changed files with 1291 additions and 789 deletions

View file

@ -100,9 +100,9 @@ APPSRC += restconf_methods_post.c
APPSRC += restconf_methods_get.c
APPSRC += restconf_methods_patch.c
APPSRC += restconf_root.c
APPSRC += clixon_http1.c
APPSRC += restconf_main_$(with_restconf).c
ifeq ($(with_restconf),native)
APPSRC += restconf_http1.c
APPSRC += restconf_native.c
APPSRC += restconf_nghttp2.c # HTTP/2
endif
@ -128,8 +128,6 @@ LIBSRC += restconf_api_$(with_restconf).c
LIBOBJ = $(LIBSRC:.c=.o)
# This lib is very small but used for clixon restconf applications to access clixon restconf lib
# functions. Mostly for future use
MYNAME = clixon_restconf

View file

@ -1,248 +0,0 @@
/*
*
***** 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 <openssl/ssl.h>
#ifdef HAVE_LIBNGHTTP2
#include <nghttp2/nghttp2.h>
#endif
/* cligen */
#include <cligen/cligen.h>
/* clixon */
#include <clixon/clixon.h>
#include "restconf_lib.h"
#include "restconf_native.h"
#include "clixon_http1_parse.h"
/* Size of xml read buffer */
#define BUFLEN 1024
static int
_http1_parse(clicon_handle h,
restconf_conn *rc,
char *str,
const char *filename)
{
int retval = -1;
clixon_http1_yacc hy = {0,};
char *ptr;
size_t sz;
clicon_debug(2, "%s", __FUNCTION__);
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;
ptr = clixon_http1_parsetext;
if (clixon_http1_parseparse(&hy) != 0) { /* yacc returns 1 on error */
if (filename)
clicon_log(LOG_NOTICE, "HTTP1 error: on line %d in %s", hy.hy_linenum, filename);
else
clicon_log(LOG_NOTICE, "HTTP1 error: on line %d", hy.hy_linenum);
if (clicon_errno == 0)
clicon_err(OE_RESTCONF, 0, "HTTP1 parser error with no error code (should not happen)");
goto done;
}
if (0){
sz = (clixon_http1_parsetext - ptr) + strlen(clixon_http1_parsetext);
fprintf(stderr,"%s %p diff:%ld %ld\n", __FUNCTION__,
clixon_http1_parsetext,
sz,
strlen(ptr)
);
}
http1_parse_exit(&hy);
http1_scan_exit(&hy);
ok:
retval = 0;
done:
return retval;
}
/*! Read an XML definition from file and parse it into a parse-tree, advanced API
*
* @param[in] fd A file descriptor containing the XML file (as ASCII characters)
* @param[in] yb How to bind yang to XML top-level when parsing
* @param[in] yspec Yang specification (only if bind is TOP or CONFIG)
* @param[in,out] xt Pointer to XML parse tree. If empty, create.
* @param[out] xerr Pointer to XML error tree, if retval is 0
* @retval 1 Parse OK and all yang assignment made
* @retval 0 Parse OK but yang assigment not made (or only partial) and xerr set
* @retval -1 Error with clicon_err called. Includes parse error
*
* @code
* cxobj *xt = NULL;
* cxobj *xerr = NULL;
* FILE *f;
* if ((f = fopen(filename, "r")) == NULL)
* err;
* if ((ret = clixon_xml_parse_file(f, YB_MODULE, yspec, &xt, &xerr)) < 0)
* err;
* xml_free(xt);
* @endcode
* @see clixon_xml_parse_string
* @see clixon_json_parse_file
* @note, If xt empty, a top-level symbol will be added so that <tree../> will be: <top><tree.../></tree></top>
* @note May block on file I/O
*/
int
clixon_http1_parse_file(clicon_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;
clicon_debug(1, "%s %s", __FUNCTION__, filename);
if (f == NULL){
clicon_err(OE_RESTCONF, EINVAL, "f is NULL");
goto done;
}
if ((buf = malloc(buflen)) == NULL){
clicon_err(OE_XML, errno, "malloc");
goto done;
}
memset(buf, 0, buflen);
ptr = buf;
while (1){
if ((ret = fread(&ch, 1, 1, f)) < 0){
clicon_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){
clicon_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;
}
int
clixon_http1_parse_string(clicon_handle h,
restconf_conn *rc,
char *str)
{
return _http1_parse(h, rc, str, "http1-parse");
}
/*! Convert buffer to null-terminated string
* I dont know how to do this without copying, OR
* input flex with a non-null terminated string
*/
int
clixon_http1_parse_buf(clicon_handle h,
restconf_conn *rc,
char *buf,
size_t n)
{
char *str = NULL;
if ((str = malloc(n+1)) == NULL){
clicon_err(OE_RESTCONF, errno, "malloc");
return -1;
}
memcpy(str, buf, n);
str[n] = '\0';
return _http1_parse(h, rc, str, "http1-parse");
}
/*!
* @param[in] h Clixon handle
* @param[in] rc Clixon request connect pointer
*/
int
restconf_http1_path_root(clicon_handle h,
restconf_conn *rc)
{
int retval = -1;
restconf_stream_data *sd;
clicon_debug(1, "------------");
if ((sd = restconf_stream_find(rc, 0)) == NULL){
clicon_err(OE_RESTCONF, EINVAL, "No stream_data");
goto done;
}
retval = 0;
done:
clicon_debug(1, "%s %d", __FUNCTION__, retval);
return retval;
}

View file

@ -46,7 +46,6 @@ struct clixon_http1_yacc {
int hy_linenum; /* Number of \n in parsed buffer */
char *hy_parse_string; /* original (copy of) parse string */
void *hy_lexbuf; /* internal parse buffer from lex */
void *hy_top;
};
typedef struct clixon_http1_yacc clixon_http1_yacc;

View file

@ -49,6 +49,7 @@
#include <string.h>
#include <stdint.h>
#include <errno.h>
#include <signal.h>
#include <openssl/ssl.h>
#ifdef HAVE_LIBNGHTTP2
@ -93,46 +94,57 @@ query [A-Za-z0-9\-\._~!$&'()*+,;=:@?/]|%[0-9a-fA-F][0-9a-fA-F]
%x REQHTTP
%x FLDNAME
%x FLDVALUE
%x BODYM
%%
<REQLINE,REQTARG,REQUERY,REQHTTP,FLDNAME,FLDVALUE><<EOF>> { return X_EOF; }
<REQLINE>[ ] { BEGIN(REQTARG); return SP; }
<REQLINE>{token} { clixon_http1_parselval.string = yytext;
<REQLINE>{token} { clixon_http1_parselval.string = strdup(yytext);
return TOKEN; }
<REQLINE>. { clixon_http1_parseerror(_HY, "LEXICAL ERROR\n"); return -1; }
<REQTARG>\? { BEGIN(REQUERY); return QMARK; }
<REQTARG>\/ { return SLASH; }
<REQTARG>[ ] { return SP; }
<REQTARG>HTTP { BEGIN(REQHTTP); return HTTP; }
<REQUERY>{pchar}+ { clixon_http1_parselval.string = yytext;
<REQTARG>[ ] { BEGIN(REQHTTP); return SP; }
<REQTARG>{pchar}+ { clixon_http1_parselval.string = yytext;
return PCHARS; }
<REQTARG>. { clixon_http1_parseerror(_HY, "LEXICAL ERROR\n"); return -1; }
<REQUERY>\/ { return SLASH; }
<REQUERY>[ ] { return SP; }
<REQUERY>HTTP { BEGIN(REQHTTP); return HTTP; }
<REQUERY>{query}+ { clixon_http1_parselval.string = yytext;
<REQUERY>[ ] { BEGIN(REQHTTP); return SP; }
<REQUERY>{query}+ { clixon_http1_parselval.string = strdup(yytext);
return QUERY; }
<REQUERY>. { clixon_http1_parseerror(_HY, "LEXICAL ERROR\n"); return -1; }
<REQHTTP>\r\n { BEGIN(FLDNAME); return CRLF; _HY->hy_linenum++; }
<REQHTTP>\/ { return SLASH; }
<REQHTTP>\. { return DOT; }
<REQHTTP>HTTP { BEGIN(REQHTTP); return HTTP; }
<REQHTTP>[0-9] { clixon_http1_parselval.intval = atoi(yytext);
return DIGIT; }
<REQHTTP>. { clixon_http1_parseerror(_HY, "LEXICAL ERROR\n"); return -1; }
<FLDNAME>: { BEGIN(FLDVALUE); return COLON; }
<FLDNAME>\r\n { return CRLF; _HY->hy_linenum++; }
<FLDNAME>[ \t\n]+ { return RWS; }
<FLDNAME>{token} { clixon_http1_parselval.string = yytext;
<FLDNAME>\r\n { BEGIN(BODYM); return CRLF; _HY->hy_linenum++; }
<FLDNAME>[ \t]+ { return RWS; }
<FLDNAME>{token} { clixon_http1_parselval.string = strdup(yytext);
return TOKEN; }
<FLDNAME>. { clixon_http1_parseerror(_HY, "LEXICAL ERROR\n"); return -1; }
<FLDVALUE>\r\n { BEGIN(FLDNAME); return CRLF; _HY->hy_linenum++; }
<FLDVALUE>[ \t\n]+ { return RWS; }
<FLDVALUE>. { clixon_http1_parselval.string = yytext;
return VCHAR; }
<FLDVALUE>[ \t]+ { return RWS; }
<FLDVALUE>[^ \t\n\r]+ { clixon_http1_parselval.string = strdup(yytext);
return VCHARS; }
<FLDVALUE>. { clixon_http1_parseerror(_HY, "LEXICAL ERROR\n"); return -1; }
<BODYM>.+ { clixon_http1_parselval.string = strdup(yytext); /* note \n not . */
return BODY; }
<BODYM>\n { clixon_http1_parselval.string = strdup(yytext);
_HY->hy_linenum++;
return BODY; }
<BODYM><<EOF>> { return X_EOF; }
<BODYM>. { clixon_http1_parseerror(_HY, "LEXICAL ERROR\n"); return ERROR; }
%%
/*! Initialize scanner.

View file

@ -32,6 +32,7 @@
***** END LICENSE BLOCK *****
* HTTP/1.1 parser according to RFC 7230 Appendix B
* XXX field_values : field_values field_vchars : Only handle one field
*/
%start http_message
@ -51,15 +52,20 @@
%token HTTP
%token COLON
%token X_EOF
%token ERROR
%token <string> PCHARS
%token <string> QUERY
%token <string> TOKEN
%token <string> VCHAR
%token <string> VCHARS
%token <string> BODY
%token <intval> DIGIT
%type <string> body
%type <string> absolute_paths
%type <string> absolute_path
%type <string> field_vchars
%type <string> field_values
%lex-param {void *_hy} /* Add this argument to parse() and lex() function */
%parse-param {void *_hy}
@ -81,6 +87,7 @@
#include <string.h>
#include <stdint.h>
#include <errno.h>
#include <signal.h>
#include <openssl/ssl.h>
#ifdef HAVE_LIBNGHTTP2
@ -142,11 +149,50 @@ http1_parse_query(clixon_http1_yacc *hy,
int retval = -1;
restconf_stream_data *sd = NULL;
if ((sd = restconf_stream_find(hy->hy_rc, 0)) == NULL)
goto ok;
clicon_debug(1, "%s: ?%s ", __FUNCTION__, query);
if ((sd = restconf_stream_find(hy->hy_rc, 0)) == NULL){
clicon_err(OE_RESTCONF, 0, "stream 0 not found");
goto done;
}
if (uri_str2cvec(query, '&', '=', 1, &sd->sd_qvec) < 0)
goto done;
ok:
retval = 0;
done:
return retval;
}
static int
http1_body(clixon_http1_yacc *hy,
char *body)
{
int retval = -1;
restconf_stream_data *sd = NULL;
clicon_debug(1, "%s: %s ", __FUNCTION__, body);
if ((sd = restconf_stream_find(hy->hy_rc, 0)) == NULL){
clicon_err(OE_RESTCONF, 0, "stream 0 not found");
goto done;
}
if (cbuf_append_buf(sd->sd_indata, body, strlen(body)) < 0){
clicon_err(OE_RESTCONF, errno, "cbuf_append_buf");
goto done;
}
retval = 0;
done:
return retval;
}
/*!
*/
static int
http1_parse_header_field(clixon_http1_yacc *hy,
char *name,
char *field)
{
int retval = -1;
if (restconf_convert_hdr(hy->hy_h, name, field) < 0)
goto done;
retval = 0;
done:
return retval;
@ -159,14 +205,34 @@ http1_parse_query(clixon_http1_yacc *hy,
/* start-line *( header-field CRLF ) CRLF [ message-body ]
* start-line = request-line / status-line (only request-line here, ignore status-line)
*/
http_message : request_line header_fields CRLF
{ _HY->hy_top=NULL; _PARSE_DEBUG("http-message -> request-line header-fields ACCEPT"); YYACCEPT; }
http_message : request_line header_fields CRLF body
{
if ($4) {
if (http1_body(_HY, $4) < 0) YYABORT;
free($4);
}
_PARSE_DEBUG("http-message -> request-line header-fields body");
YYACCEPT;
}
;
/* request-line = method SP request-target SP HTTP-version CRLF */
request_line : method SP request_target SP HTTP_version CRLF
body : body BODY
{
if (($$ = clixon_string_del_join($1, "", $2)) == NULL) {
free($2);
YYABORT;
}
else
free($2);
_PARSE_DEBUG("body -> body BODY");
}
| ERROR { _PARSE_DEBUG("body -> ERROR"); YYABORT; /* shouldnt happen */ }
| { _PARSE_DEBUG("body -> "); $$ = NULL; }
;
/* request-line = method SP request-target SP HTTP-version CRLF */request_line : method SP request_target SP HTTP_version CRLF
{
_PARSE_DEBUG("request-line -> method request-target HTTP_version CRLF");
_PARSE_DEBUG("request-line -> method request-target HTTP_version CRLF");
}
;
@ -179,50 +245,46 @@ method : TOKEN
{
if (restconf_param_set(_HY->hy_h, "REQUEST_METHOD", $1) < 0)
YYABORT;
free($1);
_PARSE_DEBUG("method -> TOKEN");
}
;
/* request-target = origin-form / absolute-form / authority-form / asterisk-form *
* origin-form = absolute-path [ "?" query ] */
* origin-form = absolute-path [ "?" query ]
* query = <query, see [RFC3986], Section 3.4>
* query = *( pchar / "/" / "?" )
*/
request_target : absolute_paths
{
if (restconf_param_set(_HY->hy_h, "REQUEST_URI", $1) < 0)
YYABORT;
free($1);
_PARSE_DEBUG("request-target -> absolute-paths");
}
| absolute_paths QMARK QUERY
{
if (restconf_param_set(_HY->hy_h, "REQUEST_URI", $1) < 0)
YYABORT;
if (http1_parse_query(_HY->hy_h, $3) < 0)
free($1);
if (http1_parse_query(_HY, $3) < 0)
YYABORT;
free($3);
_PARSE_DEBUG("request-target -> absolute-paths ? query");
}
;
/* query = <query, see [RFC3986], Section 3.4>
* query = *( pchar / "/" / "?" )
*/
/*
query : query query1 { _PARSE_DEBUG("query -> query1"); }
| { _PARSE_DEBUG("query -> "); }
;
query1 : PCHARS { _PARSE_DEBUG("query1 -> PCHARS"); }
| SLASH { _PARSE_DEBUG("query1 -> /"); }
| QMARK { _PARSE_DEBUG("query1 -> ?"); }
;
*/
/* absolute-path = 1*( "/" segment ) */
absolute_paths : absolute_paths absolute_path
{
if (($$ = clixon_string_del_join($1, "/", $2)) == NULL) YYABORT;
if (($$ = clixon_string_del_join($1, "/", $2)) == NULL) { free($2); YYABORT;}
free($2);
_PARSE_DEBUG("absolute-paths -> absolute-paths absolute -path");
}
| absolute_path
{ $$ = strdup($1);
{
if (($$ = clixon_string_del_join(NULL, "/", $1)) == NULL) { free($1); YYABORT;}
free($1);
_PARSE_DEBUG("absolute-paths -> absolute -path");
}
;
@ -236,11 +298,21 @@ absolute_paths : absolute_paths absolute_path
* / "*" / "+" / "," / ";" / "="
*/
absolute_path : SLASH PCHARS
{ $$=$2; _PARSE_DEBUG("absolute-path -> PCHARS"); }
{
if (($$=strdup($2)) == NULL) YYABORT;
_PARSE_DEBUG("absolute-path -> PCHARS");
}
;
/* HTTP-version = HTTP-name "/" DIGIT "." DIGIT */
HTTP_version : HTTP SLASH DIGIT DOT DIGIT
{ _PARSE_DEBUG("HTTP-version -> HTTP / DIGIT . DIGIT"); }
{
/* make sanity check later */
_HY->hy_rc->rc_proto_d1 = $3;
_HY->hy_rc->rc_proto_d2 = $5;
clicon_debug(1, "clixon_http1_parse: http/%d.%d", $3, $5);
_PARSE_DEBUG("HTTP-version -> HTTP / DIGIT . DIGIT");
}
;
/*------------------------------------------ hdr fields
@ -250,29 +322,41 @@ header_fields : header_fields header_field CRLF
| { _PARSE_DEBUG("header-fields -> "); }
;
/* header-field = field-name ":" OWS field-value OWS */
header_field : field_name COLON ows field_values ows
{ _PARSE_DEBUG("header-field -> field-name : field-values"); }
/* header-field = field-name ":" OWS field-value OWS
field-name = token */
header_field : TOKEN COLON ows field_values ows
{
if (http1_parse_header_field(_HY, $1, $4) < 0)
YYABORT;
free($1);
free($4);
_PARSE_DEBUG("header-field -> field-name : field-values");
}
;
/* field-name = token */
field_name : TOKEN { _PARSE_DEBUG("field-name -> TOKEN"); }
;
/* field-value = *( field-content / obs-fold ) */
field_values : field_values field_content
{ _PARSE_DEBUG("field-values -> field-values field-content"); }
/* field-value = *( field-content / obs-fold )
field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
field-vchar = VCHAR / obs-text */
field_values : field_vchars
{
$$ = $1; // XXX is there more than one??
_PARSE_DEBUG("field-values -> field-values field-vchars");
}
| { _PARSE_DEBUG("field-values -> "); }
;
/* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] */
field_content : field_vchars { _PARSE_DEBUG("field-content -> field-vchars"); }
;
/* field-vchar = VCHAR / obs-text */
field_vchars : field_vchars RWS VCHAR
{ _PARSE_DEBUG("field-vchars -> field-vchars VCHAR"); }
| VCHAR { _PARSE_DEBUG("field-vchars -> VCHAR"); }
field_vchars : field_vchars RWS VCHARS
{
if (($$ = clixon_string_del_join($1, " ", $3)) == NULL) YYABORT;
free($3);
_PARSE_DEBUG("field-vchars -> field-vchars VCHARS");
}
| VCHARS
{
$$ = $1;
_PARSE_DEBUG("field-vchars -> VCHARS");
}
;
/* The OWS rule is used where zero or more linear whitespace octets

View file

@ -155,7 +155,7 @@ restconf_param_set(clicon_handle h,
{
struct restconf_handle *rh = handle(h);
clicon_debug(1, "%s=%s", param, val);
clicon_debug(1, "%s: %s=%s", __FUNCTION__, param, val);
if (rh->rh_params == NULL)
if ((rh->rh_params = clicon_hash_init()) == NULL)
return -1;

View file

@ -0,0 +1,492 @@
/*
*
***** 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"
/* 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 with clicon_err called.
*/
static int
_http1_parse(clicon_handle h,
restconf_conn *rc,
char *str,
const char *filename)
{
int retval = -1;
clixon_http1_yacc hy = {0,};
int ret;
clicon_debug(1, "%s:\n%s", __FUNCTION__, 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)
clicon_log(LOG_NOTICE, "HTTP1 error: on line %d in %s", hy.hy_linenum, filename);
else
clicon_log(LOG_NOTICE, "HTTP1 error: on line %d", hy.hy_linenum);
if (clicon_errno == 0)
clicon_err(OE_RESTCONF, 0, "HTTP1 parser error with no error code (should not happen)");
goto done;
}
ok:
retval = 0;
done:
clicon_debug(1, "%s %d", __FUNCTION__, 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 with clicon_err called.
*/
int
clixon_http1_parse_file(clicon_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;
clicon_debug(1, "%s %s", __FUNCTION__, filename);
if (f == NULL){
clicon_err(OE_RESTCONF, EINVAL, "f is NULL");
goto done;
}
if ((buf = malloc(buflen)) == NULL){
clicon_err(OE_XML, errno, "malloc");
goto done;
}
memset(buf, 0, buflen);
ptr = buf;
while (1){
if ((ret = fread(&ch, 1, 1, f)) < 0){
clicon_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){
clicon_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 with clicon_err called.
*/
int
clixon_http1_parse_string(clicon_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 with clicon_err called.
* @note Had preferred to do this without copying, OR
* input flex with a non-null terminated string
*/
int
clixon_http1_parse_buf(clicon_handle h,
restconf_conn *rc,
char *buf,
size_t n)
{
char *str = NULL;
int ret;
if ((str = malloc(n+1)) == NULL){
clicon_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(clicon_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){
clicon_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;
/* 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)
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){
cbuf_append_str(sd->sd_outp_buf, cbuf_get(sd->sd_body));
}
retval = 0;
done:
return retval;
}
/*!
* @param[in] h Clixon handle
* @param[in] rc Clixon request connect pointer
*/
int
restconf_http1_path_root(clicon_handle h,
restconf_conn *rc)
{
int retval = -1;
restconf_stream_data *sd;
cvec *cvv = NULL;
char *cn;
char *subject = NULL;
cxobj *xerr = NULL;
int pretty;
int ret;
clicon_debug(1, "------------");
pretty = restconf_pretty_get(h);
if ((sd = restconf_stream_find(rc, 0)) == NULL){
clicon_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 ((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;
#ifdef HAVE_LIBNGHTTP2
if ((ret = http1_upgrade_http2(h, sd)) < 0)
goto done;
if (ret == 0) /* upgrade */
goto upgrade;
#endif
/* call generic function */
if (strcmp(sd->sd_path, RESTCONF_WELL_KNOWN) == 0){
if (api_well_known(h, sd) < 0)
goto done;
}
else if (api_root_restconf(h, sd, sd->sd_qvec) < 0)
goto done;
fail:
if (restconf_param_del_all(h) < 0)
goto done;
upgrade:
if (sd->sd_code)
if (restconf_http1_reply(rc, sd) < 0)
goto done;
retval = 0;
done:
clicon_debug(1, "%s %d", __FUNCTION__, retval);
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(clicon_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?
*
* @param[in] h Clixon handle
* @param[in] sd Restconf stream data (for http1 only stream 0)
* @retval 1 OK, message is fully read, proceed to processing
* @retval 0 OK, but message is partially read, need more data before processing
* @retval -1 Error
*/
int
http1_check_readmore(clicon_handle h,
restconf_stream_data *sd)
{
int retval = -1;
char *val;
int len;
if ((val = restconf_param_get(h, "HTTP_CONTENT_LENGTH")) != NULL &&
(len = atoi(val)) != 0){
if (cbuf_len(sd->sd_indata) < len)
goto readmore;
}
retval = 1;
done:
return retval;
readmore:
retval = 0;
goto done;
}

View file

@ -33,8 +33,8 @@
* HTTP/1.1 parser according to RFC 7230
*/
#ifndef _CLIXON_HTTP1_H_
#define _CLIXON_HTTP1_H_
#ifndef _RESTCONF_HTTP1_H_
#define _RESTCONF_HTTP1_H_
/*
* Prototypes
@ -43,5 +43,7 @@ int clixon_http1_parse_file(clicon_handle h, restconf_conn *rc, FILE *f, const c
int clixon_http1_parse_string(clicon_handle h, restconf_conn *rc, char *str);
int clixon_http1_parse_buf(clicon_handle h, restconf_conn *rc, char *buf, size_t n);
int restconf_http1_path_root(clicon_handle h, restconf_conn *rc);
int http1_check_expect(clicon_handle h, restconf_conn *rc, restconf_stream_data *sd);
int http1_check_readmore(clicon_handle h, restconf_stream_data *sd);
#endif /* _CLIXON_HTTP1_H_ */
#endif /* _RESTCONF_HTTP1_H_ */

View file

@ -127,17 +127,16 @@
#include <openssl/err.h>
#include <openssl/x509v3.h>
#ifdef HAVE_LIBNGHTTP2
#include <nghttp2/nghttp2.h>
#endif
/* cligen */
#include <cligen/cligen.h>
/* clicon */
/* libclixon */
#include <clixon/clixon.h>
#ifdef HAVE_LIBNGHTTP2
/* nghttp2 */
#include <nghttp2/nghttp2.h>
#endif
/* restconf */
#include "restconf_lib.h" /* generic shared with plugins */
#include "restconf_handle.h"
@ -149,7 +148,7 @@
#include "restconf_nghttp2.h" /* http/2 */
#endif
#ifdef HAVE_HTTP1
#include "clixon_http1.h"
#include "restconf_http1.h"
#endif
/* Command line options to be passed to getopt(3) */
@ -170,9 +169,6 @@
/* Cert verify depth: dont know what to set here? */
#define VERIFY_DEPTH 5
/* Forward */
static int restconf_connection(int s, void* arg);
static int session_id_context = 1;
/*! Get restconf native handle
@ -209,98 +205,6 @@ restconf_native_handle_set(clicon_handle h,
return 0;
}
/* Write evbuf to socket
* see also this function in restcont_api_openssl.c
*/
static int
buf_write(char *buf,
size_t buflen,
int s,
SSL *ssl)
{
int retval = -1;
ssize_t len;
ssize_t totlen = 0;
int er;
/* Two problems with debugging buffers from libevent that this fixes:
* 1. they are not "strings" in the sense they are not NULL-terminated
* 2. they are often very long
*/
if (clicon_debug_get()) {
char *dbgstr = NULL;
size_t sz;
sz = buflen>256?256:buflen; /* Truncate to 256 */
if ((dbgstr = malloc(sz+1)) == NULL){
clicon_err(OE_UNIX, errno, "malloc");
goto done;
}
memcpy(dbgstr, buf, sz);
dbgstr[sz] = '\0';
clicon_debug(1, "%s buflen:%zu buf:%s", __FUNCTION__, 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 */
if (ssl)
SSL_free(ssl);
close(s);
clixon_event_unreg_fd(s, restconf_connection);
goto ok; /* Close socket and ssl */
}
else if (er == EAGAIN){
clicon_debug(1, "%s write EAGAIN", __FUNCTION__);
usleep(10000);
continue;
}
else{
clicon_err(OE_RESTCONF, er, "SSL_write %d", er);
goto done;
}
break;
default:
clicon_err(OE_SSL, 0, "SSL_write");
goto done;
break;
}
goto done;
}
}
else{
if ((len = write(s, buf+totlen, buflen-totlen)) < 0){
if (errno == EAGAIN){
clicon_debug(1, "%s write EAGAIN", __FUNCTION__);
usleep(10000);
continue;
}
#if 1
else if (errno == ECONNRESET) {/* Connection reset by peer */
close(s);
clixon_event_unreg_fd(s, restconf_connection);
goto ok; /* Close socket and ssl */
}
#endif
else{
clicon_err(OE_UNIX, errno, "write");
goto done;
}
}
assert(len != 0);
}
totlen += len;
} /* while */
ok:
retval = 0;
done:
clicon_debug(1, "%s retval:%d", __FUNCTION__, retval);
return retval;
}
/* util function to append log string
*/
static int
@ -596,324 +500,6 @@ Note that in this case SSL_ERROR_ZERO_RETURN does not necessarily indicate that
return retval;
}
/*! Send early handcoded bad request reply before actual packet received, just after accept
* @param[in] h Clixon handle
* @param[in] s Socket
* @param[in] ssl If set, it will be freed
* @param[in] body If given add message body using media
* @see restconf_badrequest which can only be called in a request context
*/
static int
send_badrequest(clicon_handle h,
int s,
SSL *ssl,
char *media,
char *body)
{
int retval = -1;
cbuf *cb = NULL;
clicon_debug(1, "%s", __FUNCTION__);
if ((cb = cbuf_new()) == NULL){
clicon_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);
if (buf_write(cbuf_get(cb), cbuf_len(cb), s, ssl) < 0)
goto done;
retval = 0;
done:
if (cb)
cbuf_free(cb);
return retval;
}
#if 0
#define IFILE "/var/tmp/clixon-mirror/ifile"
#define FMTDIR "/var/tmp/clixon-mirror/"
static FILE *myf = NULL;
static int
mirror_pkt(const char *buf,
ssize_t n)
{
int retval = -1;
if (fwrite(buf, 1, n, myf) != n){
perror("fopen");
goto done;
}
retval = 0;
done:
return retval;
}
static int
mirror_new(void)
{
int retval = -1;
static uint64_t u64 = 0;
cbuf *cb = cbuf_new();
FILE *ifile;
if ((ifile = fopen(IFILE, "r+")) == NULL){
perror("fopen r+ ifile");
}
else {
if (fscanf(ifile, "%" PRIu64, &u64) < 0){
perror("fscanf ifile");
goto done;
}
fclose(ifile);
}
if (myf != NULL)
fclose(myf);
cprintf(cb, FMTDIR "%" PRIu64 ".dump", u64);
if ((myf = fopen(cbuf_get(cb), "w")) == NULL){
perror("fopen");
goto done;
}
cbuf_free(cb);
u64++;
if ((ifile = fopen(IFILE, "w")) == NULL){
perror("fopen w+ ifile");
goto done;
}
fprintf(ifile, "%" PRIu64, u64);
fclose(ifile);
retval = 0;
done:
return retval;
}
#endif
/*! 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.
*/
static int
restconf_connection(int s,
void *arg)
{
int retval = -1;
restconf_conn *rc = NULL;
ssize_t n;
char buf[BUFSIZ]; /* from stdio.h, typically 8K XXX: reduce for test */
int readmore = 1;
int sslerr;
#ifdef HAVE_LIBNGHTTP2
int ret;
#endif
#ifdef HAVE_HTTP1
clicon_handle h;
restconf_stream_data *sd;
#endif
clicon_debug(1, "%s %d", __FUNCTION__, s);
if ((rc = (restconf_conn*)arg) == NULL){
clicon_err(OE_RESTCONF, EINVAL, "arg is NULL");
goto done;
}
assert(s == rc->rc_s);
while (readmore) {
clicon_debug(1, "%s readmore", __FUNCTION__);
readmore = 0;
/* Example: curl -Ssik -u wilma:bar -X GET https://localhost/restconf/data/example:x */
if (rc->rc_ssl){
/* Non-ssl gets n == 0 here!
curl -Ssik --key /var/tmp/./test_restconf_ssl_certs.sh/certs/limited.key --cert /var/tmp/./test_restconf_ssl_certs.sh/certs/limited.crt -X GET https://localhost/restconf/data/example:x
*/
if ((n = SSL_read(rc->rc_ssl, buf, sizeof(buf))) < 0){
sslerr = SSL_get_error(rc->rc_ssl, n);
clicon_debug(1, "%s SSL_read() n:%zd errno:%d sslerr:%d", __FUNCTION__, n, 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
*/
clicon_debug(1, "%s SSL_read SSL_ERROR_WANT_READ", __FUNCTION__);
usleep(1000);
readmore = 1;
break;
default:
clicon_err(OE_XML, errno, "SSL_read");
goto done;
} /* switch */
continue; /* readmore */
}
}
else{
if ((n = read(rc->rc_s, buf, sizeof(buf))) < 0){ /* XXX atomicio ? */
switch(errno){
case ECONNRESET:/* Connection reset by peer */
clicon_debug(1, "%s %d Connection reset by peer", __FUNCTION__, rc->rc_s);
clixon_event_unreg_fd(rc->rc_s, restconf_connection);
close(rc->rc_s);
restconf_conn_free(rc);
goto ok; /* Close socket and ssl */
break;
case EAGAIN:
clicon_debug(1, "%s read EAGAIN", __FUNCTION__);
usleep(1000);
readmore = 1;
break;
default:;
clicon_err(OE_XML, errno, "read");
goto done;
break;
}
continue;
}
}
clicon_debug(1, "%s read:%zd", __FUNCTION__, n);
if (n == 0){
clicon_debug(1, "%s n=0 closing socket", __FUNCTION__);
if (restconf_close_ssl_socket(rc, 0) < 0)
goto done;
restconf_conn_free(rc);
rc = NULL;
goto ok;
}
#if 0
if (mirror_pkt(buf, n) < 0)
goto done;
#endif
switch (rc->rc_proto){
#ifdef HAVE_HTTP1
case HTTP_10:
case HTTP_11:
h = rc->rc_h;
if (clixon_http1_parse_buf(h, rc, buf, n) < 0){
if (send_badrequest(h, rc->rc_s, rc->rc_ssl, "application/yang-data+xml",
"<errors xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"><error><error-type>protocol</error-type><error-tag>malformed-message</error-tag><error-message>The requested URL or a header is in some way badly formed</error-message></error></errors>") < 0)
goto done;
}
else{
if (restconf_http1_path_root(h, rc) < 0)
goto done;
}
clicon_debug(1, "%s connection_parse OK", __FUNCTION__);
/* default stream */
if ((sd = restconf_stream_find(rc, 0)) == NULL){
clicon_err(OE_RESTCONF, EINVAL, "restconf stream not found");
goto done;
}
if (buf_write(cbuf_get(sd->sd_outp_buf), cbuf_len(sd->sd_outp_buf),
rc->rc_s, rc->rc_ssl) < 0)
goto done;
cvec_reset(sd->sd_outp_hdrs); /* Can be done in native_send_reply */
cbuf_reset(sd->sd_outp_buf);
if (rc->rc_exit){ /* Server-initiated exit for http/2 */
SSL_free(rc->rc_ssl);
rc->rc_ssl = NULL;
if (close(rc->rc_s) < 0){
clicon_err(OE_UNIX, errno, "close");
goto done;
}
clixon_event_unreg_fd(rc->rc_s, restconf_connection);
restconf_conn_free(rc);
goto ok;
}
#ifdef HAVE_LIBNGHTTP2
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, 1);
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){
clicon_err(OE_NGHTTP2, ngerr, "nghttp2_session_upgrade2");
goto done;
}
if (http2_send_server_connection(rc) < 0){
restconf_close_ssl_socket(rc, 1);
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);
}
}
#endif
break;
#endif /* HAVE_HTTP1 */
#ifdef HAVE_LIBNGHTTP2
case HTTP_2:
if (rc->rc_exit){ /* Server-initiated exit for http/2 */
nghttp2_error ngerr;
if ((ngerr = nghttp2_session_terminate_session(rc->rc_ngsession, 0)) < 0)
clicon_err(OE_NGHTTP2, ngerr, "nghttp2_session_terminate_session %d", ngerr);
}
else {
if ((ret = http2_recv(rc, (unsigned char *)buf, n)) < 0)
goto done;
if (ret == 0){
restconf_close_ssl_socket(rc, 1);
if (restconf_conn_free(rc) < 0)
goto done;
goto ok;
}
/* There may be more data frames */
readmore++;
}
break;
#endif /* HAVE_LIBNGHTTP2 */
default:
break;
} /* switch rc_proto */
} /* while readmore */
ok:
retval = 0;
done:
clicon_debug(1, "%s retval %d", __FUNCTION__, retval);
return retval;
} /* restconf_connection */
#if 0 /* debug */
/*! Debug print all loaded certs
*/
@ -1019,7 +605,7 @@ ssl_alpn_check(clicon_handle h,
if (alpn != NULL){
cprintf(cberr, "<errors xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"><error><error-type>protocol</error-type><error-tag>malformed-message</error-tag><error-message>ALPN: protocol not recognized: %s</error-message></error></errors>", alpn);
clicon_log(LOG_INFO, "%s Warning: %s", __FUNCTION__, cbuf_get(cberr));
if (send_badrequest(h, rc->rc_s, rc->rc_ssl,
if (native_send_badrequest(h, rc->rc_s, rc->rc_ssl,
"application/yang-data+xml",
cbuf_get(cberr)) < 0)
goto done;
@ -1160,7 +746,7 @@ restconf_accept_client(int fd,
case SSL_ERROR_SSL: /* 1 */
clicon_debug(1, "%s SSL_ERROR_SSL (non-ssl message on ssl socket)", __FUNCTION__);
#if 1
if (send_badrequest(h, rc->rc_s, NULL, "application/yang-data+xml",
if (native_send_badrequest(h, rc->rc_s, NULL, "application/yang-data+xml",
"<errors xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"><error><error-type>protocol</error-type><error-tag>malformed-message</error-tag><error-message>The plain HTTP request was sent to HTTPS port</error-message></error></errors>") < 0)
goto done;
#endif
@ -1243,7 +829,7 @@ restconf_accept_client(int fd,
}
else { /* Get certificates (if available) */
if (proto != HTTP_2 &&
send_badrequest(h, rc->rc_s, rc->rc_ssl, "application/yang-data+xml",
native_send_badrequest(h, rc->rc_s, rc->rc_ssl, "application/yang-data+xml",
"<errors xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"><error><error-type>protocol</error-type><error-tag>malformed-message</error-tag><error-message>Peer certificate required</error-message></error></errors>") < 0)
goto done;
restconf_conn_free(rc);
@ -1315,10 +901,6 @@ restconf_accept_client(int fd,
default:
break;
} /* switch proto */
#if 0
if (mirror_new() < 0)
goto done;
#endif
if (clixon_event_reg_fd(rc->rc_s, restconf_connection, (void*)rc, "restconf client socket") < 0)
goto done;
ok:

View file

@ -212,7 +212,7 @@ api_data_post(clicon_handle h,
* expected data resource. (tested again below)
*/
if (data == NULL || strlen(data) == 0){
if (netconf_malformed_message_xml(&xerr, "The message-body MUST contain exactly one instance of the expected data resource") < 0)
if (netconf_malformed_message_xml(&xerr, "The message-body of POST MUST contain exactly one instance of the expected data resource") < 0)
goto done;
if (api_return_err0(h, req, xerr, pretty, media_out, 0) < 0)
goto done;

View file

@ -55,23 +55,32 @@
#include <openssl/rand.h>
#include <openssl/err.h>
#include <openssl/x509v3.h>
#ifdef HAVE_LIBNGHTTP2
#include <nghttp2/nghttp2.h>
#endif
/* cligen */
#include <cligen/cligen.h>
/* clicon */
/* libclixon */
#include <clixon/clixon.h>
/* 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 <nghttp2/nghttp2.h>
#include "restconf_nghttp2.h" /* http/2 */
#endif
#ifdef HAVE_HTTP1
#include "restconf_http1.h"
#endif
#include "restconf_native.h" /* Restconf-openssl mode specific headers*/
/*!
* @see restconf_stream_free
*/
restconf_stream_data *
restconf_stream_data_new(restconf_conn *rc,
int32_t stream_id)
@ -297,3 +306,400 @@ restconf_connection_sanity(clicon_handle h,
xml_free(xerr);
return retval;
}
/* Write buf to socket
* see also this function in restcont_api_openssl.c
*/
static int
native_buf_write(char *buf,
size_t buflen,
int s,
SSL *ssl)
{
int retval = -1;
ssize_t len;
ssize_t totlen = 0;
int er;
/* 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 (clicon_debug_get()) {
char *dbgstr = NULL;
size_t sz;
sz = buflen>256?256:buflen; /* Truncate to 256 */
if ((dbgstr = malloc(sz+1)) == NULL){
clicon_err(OE_UNIX, errno, "malloc");
goto done;
}
memcpy(dbgstr, buf, sz);
dbgstr[sz] = '\0';
clicon_debug(1, "%s buflen:%zu buf:\n%s", __FUNCTION__, 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 */
if (ssl)
SSL_free(ssl);
close(s);
clixon_event_unreg_fd(s, restconf_connection);
goto ok; /* Close socket and ssl */
}
else if (er == EAGAIN){
clicon_debug(1, "%s write EAGAIN", __FUNCTION__);
usleep(10000);
continue;
}
else{
clicon_err(OE_RESTCONF, er, "SSL_write %d", er);
goto done;
}
break;
default:
clicon_err(OE_SSL, 0, "SSL_write");
goto done;
break;
}
goto done;
}
}
else{
if ((len = write(s, buf+totlen, buflen-totlen)) < 0){
switch (errno){
case EAGAIN: /* Operation would block */
clicon_debug(1, "%s write EAGAIN", __FUNCTION__);
usleep(10000);
continue;
break;
case ECONNRESET: /* Connection reset by peer */
case EPIPE: /* Broken pipe */
close(s);
clixon_event_unreg_fd(s, restconf_connection);
goto ok; /* Close socket and ssl */
break;
default:
clicon_err(OE_UNIX, errno, "write %d", errno);
goto done;
break;
}
}
assert(len != 0);
}
totlen += len;
} /* while */
ok:
retval = 0;
done:
clicon_debug(1, "%s retval:%d", __FUNCTION__, retval);
return retval;
}
/*! Send early handcoded bad request reply before actual packet received, just after accept
* @param[in] h Clixon handle
* @param[in] s Socket
* @param[in] ssl If set, it will be freed
* @param[in] body If given add message body using media
* @see restconf_badrequest which can only be called in a request context
*/
int
native_send_badrequest(clicon_handle h,
int s,
SSL *ssl,
char *media,
char *body)
{
int retval = -1;
cbuf *cb = NULL;
clicon_debug(1, "%s", __FUNCTION__);
if ((cb = cbuf_new()) == NULL){
clicon_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);
if (native_buf_write(cbuf_get(cb), cbuf_len(cb), s, ssl) < 0)
goto done;
retval = 0;
done:
if (cb)
cbuf_free(cb);
return retval;
}
/*! 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[BUFSIZ]; /* from stdio.h, typically 8K. 256 fails some tests*/
char *totbuf = NULL;
size_t totlen = 0;
int readmore = 1;
int sslerr;
int contnr = 0; /* Continue sent */
#ifdef HAVE_LIBNGHTTP2
int ret;
#endif
#ifdef HAVE_HTTP1
clicon_handle h;
restconf_stream_data *sd;
#endif
clicon_debug(1, "%s %d", __FUNCTION__, s);
if ((rc = (restconf_conn*)arg) == NULL){
clicon_err(OE_RESTCONF, EINVAL, "arg is NULL");
goto done;
}
assert(s == rc->rc_s);
while (readmore) {
clicon_debug(1, "%s readmore", __FUNCTION__);
readmore = 0;
/* Example: curl -Ssik -u wilma:bar -X GET https://localhost/restconf/data/example:x */
if (rc->rc_ssl){
/* Non-ssl gets n == 0 here!
curl -Ssik --key /var/tmp/./test_restconf_ssl_certs.sh/certs/limited.key --cert /var/tmp/./test_restconf_ssl_certs.sh/certs/limited.crt -X GET https://localhost/restconf/data/example:x
*/
if ((n = SSL_read(rc->rc_ssl, buf, sizeof(buf))) < 0){
sslerr = SSL_get_error(rc->rc_ssl, n);
clicon_debug(1, "%s SSL_read() n:%zd errno:%d sslerr:%d", __FUNCTION__, n, 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
*/
clicon_debug(1, "%s SSL_read SSL_ERROR_WANT_READ", __FUNCTION__);
usleep(1000);
readmore = 1;
break;
default:
clicon_err(OE_XML, errno, "SSL_read");
goto done;
} /* switch */
continue; /* readmore */
}
}
else{
if ((n = read(rc->rc_s, buf, sizeof(buf))) < 0){ /* XXX atomicio ? */
switch(errno){
case ECONNRESET:/* Connection reset by peer */
clicon_debug(1, "%s %d Connection reset by peer", __FUNCTION__, rc->rc_s);
clixon_event_unreg_fd(rc->rc_s, restconf_connection);
close(rc->rc_s);
restconf_conn_free(rc);
goto ok; /* Close socket and ssl */
break;
case EAGAIN:
clicon_debug(1, "%s read EAGAIN", __FUNCTION__);
usleep(1000);
readmore = 1;
break;
default:;
clicon_err(OE_XML, errno, "read");
goto done;
break;
}
continue;
}
}
clicon_debug(1, "%s read:%zd", __FUNCTION__, n);
if (n == 0){
clicon_debug(1, "%s n=0 closing socket", __FUNCTION__);
if (restconf_close_ssl_socket(rc, 0) < 0)
goto done;
restconf_conn_free(rc);
rc = NULL;
goto ok;
}
switch (rc->rc_proto){
#ifdef HAVE_HTTP1
case HTTP_10:
case HTTP_11:
h = rc->rc_h;
/* default stream */
if ((sd = restconf_stream_find(rc, 0)) == NULL){
clicon_err(OE_RESTCONF, EINVAL, "restconf stream not found");
goto done;
}
/* multi-buffer for multiple reads */
totlen += n;
if ((totbuf = realloc(totbuf, totlen+1)) == NULL){
clicon_err(OE_UNIX, errno, "realloc");
goto done;
}
memcpy(&totbuf[totlen-n], buf, n);
totbuf[totlen] = '\0';
if (clixon_http1_parse_string(h, rc, totbuf) < 0){
if (native_send_badrequest(h, rc->rc_s, rc->rc_ssl, "application/yang-data+xml",
"<errors xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"><error><error-type>protocol</error-type><error-tag>malformed-message</error-tag><error-message>The requested URL or a header is in some way badly formed</error-message></error></errors>") < 0)
goto done;
}
else{
/* Check for Continue and if so reply with 100 Continue
* ret == 1: send reply
*/
if (!contnr){
if ((ret = http1_check_expect(h, rc, sd)) < 0)
goto done;
if (ret == 1){
if (native_buf_write(cbuf_get(sd->sd_outp_buf), cbuf_len(sd->sd_outp_buf),
rc->rc_s, rc->rc_ssl) < 0)
goto done;
cvec_reset(sd->sd_outp_hdrs);
cbuf_reset(sd->sd_outp_buf);
contnr++;
}
}
/* Check whole message is read.
* ret == 0: need more bytes
*/
if ((ret = http1_check_readmore(h, sd)) < 0)
goto done;
if (ret == 0){
readmore++;
#if 1
/* Clear all stream data if reading more
* Alternative would be to not adding new data to totbuf ^
* and just append to sd->sd_indata but that would assume
* all headers read on first round. But that cant be done withut
* some probing on the socket if there is more data since it
* would hang on read otherwise
*/
cbuf_reset(sd->sd_indata);
if (sd->sd_qvec)
cvec_free(sd->sd_qvec);
if (restconf_param_del_all(h) < 0)
goto done;
#endif
continue;
}
if (restconf_http1_path_root(h, rc) < 0)
goto done;
if (native_buf_write(cbuf_get(sd->sd_outp_buf), cbuf_len(sd->sd_outp_buf),
rc->rc_s, rc->rc_ssl) < 0)
goto done;
cvec_reset(sd->sd_outp_hdrs); /* Can be done in native_send_reply */
cbuf_reset(sd->sd_outp_buf);
}
if (rc->rc_exit){ /* Server-initiated exit for http/2 */
SSL_free(rc->rc_ssl);
rc->rc_ssl = NULL;
if (close(rc->rc_s) < 0){
clicon_err(OE_UNIX, errno, "close");
goto done;
}
clixon_event_unreg_fd(rc->rc_s, restconf_connection);
restconf_conn_free(rc);
goto ok;
}
#ifdef HAVE_LIBNGHTTP2
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, 1);
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){
clicon_err(OE_NGHTTP2, ngerr, "nghttp2_session_upgrade2");
goto done;
}
if (http2_send_server_connection(rc) < 0){
restconf_close_ssl_socket(rc, 1);
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);
}
}
#endif
break;
#endif /* HAVE_HTTP1 */
#ifdef HAVE_LIBNGHTTP2
case HTTP_2:
if (rc->rc_exit){ /* Server-initiated exit for http/2 */
nghttp2_error ngerr;
if ((ngerr = nghttp2_session_terminate_session(rc->rc_ngsession, 0)) < 0)
clicon_err(OE_NGHTTP2, ngerr, "nghttp2_session_terminate_session %d", ngerr);
}
else {
if ((ret = http2_recv(rc, (unsigned char *)buf, n)) < 0)
goto done;
if (ret == 0){
restconf_close_ssl_socket(rc, 1);
if (restconf_conn_free(rc) < 0)
goto done;
goto ok;
}
/* There may be more data frames */
readmore++;
}
break;
#endif /* HAVE_LIBNGHTTP2 */
default:
break;
} /* switch rc_proto */
} /* while readmore */
ok:
retval = 0;
done:
if (totbuf)
free(totbuf);
clicon_debug(1, "%s retval %d", __FUNCTION__, retval);
return retval;
} /* restconf_connection */

View file

@ -88,9 +88,12 @@ typedef struct {
* Per connection request
*/
typedef struct restconf_conn {
// qelem_t rs_qelem; /* List header */
size_t rc_bufferevent_output_offset; /* Kludge to drain libevent output buffer */
/* XXX rc_proto and rc_proto_d1/d2 may not both be necessary.
* remove rc_proto?
*/
restconf_http_proto rc_proto; /* HTTP protocol: http/1 or http/2 */
int rc_proto_d1; /* parsed version digit 1 */
int rc_proto_d2; /* parsed version digit 2 */
int rc_s; /* Connection socket */
clicon_handle rc_h; /* Clixon handle */
SSL *rc_ssl; /* Structure for SSL connection */
@ -137,7 +140,8 @@ int ssl_x509_name_oneline(SSL *ssl, char **oneline);
int restconf_close_ssl_socket(restconf_conn *rc, int shutdown); /* XXX in restconf_main_native.c */
int restconf_connection_sanity(clicon_handle h, restconf_conn *rc, restconf_stream_data *sd);
int native_send_badrequest(clicon_handle h, int s, SSL *ssl, char *media, char *body);
int restconf_connection(int s, void *arg);
#endif /* _RESTCONF_NATIVE_H_ */

View file

@ -451,6 +451,10 @@ http2_exec(restconf_conn *rc,
int retval = -1;
clicon_debug(1, "%s", __FUNCTION__);
if (sd->sd_path){
free(sd->sd_path);
sd->sd_path = NULL;
}
if ((sd->sd_path = restconf_uripath(rc->rc_h)) == NULL)
goto done;
sd->sd_proto = HTTP_2; /* XXX is this necessary? */