From 4aa74fa1d849b05c028f665dc9496ed5c006e102 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Thu, 27 Jan 2022 17:05:52 +0100 Subject: [PATCH] * 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 --- apps/restconf/Makefile.in | 4 +- apps/restconf/clixon_http1.c | 248 --------- apps/restconf/clixon_http1_parse.h | 1 - apps/restconf/clixon_http1_parse.l | 38 +- apps/restconf/clixon_http1_parse.y | 176 +++++-- apps/restconf/restconf_handle.c | 2 +- apps/restconf/restconf_http1.c | 492 ++++++++++++++++++ .../{clixon_http1.h => restconf_http1.h} | 8 +- apps/restconf/restconf_main_native.c | 436 +--------------- apps/restconf/restconf_methods_post.c | 2 +- apps/restconf/restconf_native.c | 412 ++++++++++++++- apps/restconf/restconf_native.h | 10 +- apps/restconf/restconf_nghttp2.c | 4 + configure | 1 + configure.ac | 1 + docker/base/Dockerfile | 4 +- docker/main/Dockerfile.native | 4 +- lib/src/clixon_string.c | 9 +- lib/src/clixon_yang_parse.y | 44 +- test/config.sh.in | 8 +- test/lib.sh | 1 + test/test_restconf.sh | 2 +- test/test_restconf_continue.sh | 131 +++++ test/test_restconf_err.sh | 33 +- test/test_restconf_internal.sh | 2 +- yang/clixon/clixon-config@2021-12-05.yang | 4 +- yang/clixon/clixon-restconf@2021-05-20.yang | 3 +- 27 files changed, 1291 insertions(+), 789 deletions(-) delete mode 100644 apps/restconf/clixon_http1.c create mode 100644 apps/restconf/restconf_http1.c rename apps/restconf/{clixon_http1.h => restconf_http1.h} (88%) create mode 100755 test/test_restconf_continue.sh diff --git a/apps/restconf/Makefile.in b/apps/restconf/Makefile.in index 4c52e621..965a8df3 100644 --- a/apps/restconf/Makefile.in +++ b/apps/restconf/Makefile.in @@ -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 diff --git a/apps/restconf/clixon_http1.c b/apps/restconf/clixon_http1.c deleted file mode 100644 index b8f22452..00000000 --- a/apps/restconf/clixon_http1.c +++ /dev/null @@ -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 -#include -#include -#include -#include -#include -#include -#include - -#ifdef HAVE_LIBNGHTTP2 -#include -#endif - -/* cligen */ -#include - -/* clixon */ -#include - -#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 will be: - * @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; -} diff --git a/apps/restconf/clixon_http1_parse.h b/apps/restconf/clixon_http1_parse.h index 1578b236..f0afd189 100644 --- a/apps/restconf/clixon_http1_parse.h +++ b/apps/restconf/clixon_http1_parse.h @@ -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; diff --git a/apps/restconf/clixon_http1_parse.l b/apps/restconf/clixon_http1_parse.l index 2ebd0099..40cc064f 100644 --- a/apps/restconf/clixon_http1_parse.l +++ b/apps/restconf/clixon_http1_parse.l @@ -49,6 +49,7 @@ #include #include #include +#include #include #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 %% <> { return X_EOF; } [ ] { BEGIN(REQTARG); return SP; } -{token} { clixon_http1_parselval.string = yytext; +{token} { clixon_http1_parselval.string = strdup(yytext); return TOKEN; } . { clixon_http1_parseerror(_HY, "LEXICAL ERROR\n"); return -1; } \? { BEGIN(REQUERY); return QMARK; } \/ { return SLASH; } -[ ] { return SP; } -HTTP { BEGIN(REQHTTP); return HTTP; } -{pchar}+ { clixon_http1_parselval.string = yytext; +[ ] { BEGIN(REQHTTP); return SP; } +{pchar}+ { clixon_http1_parselval.string = yytext; return PCHARS; } . { clixon_http1_parseerror(_HY, "LEXICAL ERROR\n"); return -1; } \/ { return SLASH; } -[ ] { return SP; } -HTTP { BEGIN(REQHTTP); return HTTP; } -{query}+ { clixon_http1_parselval.string = yytext; +[ ] { BEGIN(REQHTTP); return SP; } +{query}+ { clixon_http1_parselval.string = strdup(yytext); return QUERY; } . { clixon_http1_parseerror(_HY, "LEXICAL ERROR\n"); return -1; } \r\n { BEGIN(FLDNAME); return CRLF; _HY->hy_linenum++; } \/ { return SLASH; } \. { return DOT; } +HTTP { BEGIN(REQHTTP); return HTTP; } [0-9] { clixon_http1_parselval.intval = atoi(yytext); return DIGIT; } . { clixon_http1_parseerror(_HY, "LEXICAL ERROR\n"); return -1; } : { BEGIN(FLDVALUE); return COLON; } -\r\n { return CRLF; _HY->hy_linenum++; } -[ \t\n]+ { return RWS; } -{token} { clixon_http1_parselval.string = yytext; +\r\n { BEGIN(BODYM); return CRLF; _HY->hy_linenum++; } +[ \t]+ { return RWS; } +{token} { clixon_http1_parselval.string = strdup(yytext); return TOKEN; } . { clixon_http1_parseerror(_HY, "LEXICAL ERROR\n"); return -1; } + \r\n { BEGIN(FLDNAME); return CRLF; _HY->hy_linenum++; } -[ \t\n]+ { return RWS; } -. { clixon_http1_parselval.string = yytext; - return VCHAR; } +[ \t]+ { return RWS; } +[^ \t\n\r]+ { clixon_http1_parselval.string = strdup(yytext); + return VCHARS; } +. { clixon_http1_parseerror(_HY, "LEXICAL ERROR\n"); return -1; } + +.+ { clixon_http1_parselval.string = strdup(yytext); /* note \n not . */ + return BODY; } +\n { clixon_http1_parselval.string = strdup(yytext); + _HY->hy_linenum++; + return BODY; } +<> { return X_EOF; } +. { clixon_http1_parseerror(_HY, "LEXICAL ERROR\n"); return ERROR; } + %% /*! Initialize scanner. diff --git a/apps/restconf/clixon_http1_parse.y b/apps/restconf/clixon_http1_parse.y index f722a841..4b206107 100644 --- a/apps/restconf/clixon_http1_parse.y +++ b/apps/restconf/clixon_http1_parse.y @@ -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 PCHARS %token QUERY %token TOKEN -%token VCHAR +%token VCHARS +%token BODY %token DIGIT +%type body %type absolute_paths %type absolute_path +%type field_vchars +%type field_values %lex-param {void *_hy} /* Add this argument to parse() and lex() function */ %parse-param {void *_hy} @@ -81,6 +87,7 @@ #include #include #include +#include #include #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 = *( 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 = *( 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 diff --git a/apps/restconf/restconf_handle.c b/apps/restconf/restconf_handle.c index eb94931e..21c9843f 100644 --- a/apps/restconf/restconf_handle.c +++ b/apps/restconf/restconf_handle.c @@ -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; diff --git a/apps/restconf/restconf_http1.c b/apps/restconf/restconf_http1.c new file mode 100644 index 00000000..bb6a2bb4 --- /dev/null +++ b/apps/restconf/restconf_http1.c @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_LIBNGHTTP2 +#include +#endif + +/* cligen */ +#include + +/* clixon */ +#include + +#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 */ + 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; +} diff --git a/apps/restconf/clixon_http1.h b/apps/restconf/restconf_http1.h similarity index 88% rename from apps/restconf/clixon_http1.h rename to apps/restconf/restconf_http1.h index b91f3773..cd53bf9a 100644 --- a/apps/restconf/clixon_http1.h +++ b/apps/restconf/restconf_http1.h @@ -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_ */ diff --git a/apps/restconf/restconf_main_native.c b/apps/restconf/restconf_main_native.c index ca27bece..383b763c 100644 --- a/apps/restconf/restconf_main_native.c +++ b/apps/restconf/restconf_main_native.c @@ -127,17 +127,16 @@ #include #include +#ifdef HAVE_LIBNGHTTP2 +#include +#endif + /* cligen */ #include -/* clicon */ +/* libclixon */ #include -#ifdef HAVE_LIBNGHTTP2 -/* nghttp2 */ -#include -#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", - "protocolmalformed-messageThe requested URL or a header is in some way badly formed") < 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, "protocolmalformed-messageALPN: protocol not recognized: %s", 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", "protocolmalformed-messageThe plain HTTP request was sent to HTTPS port") < 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", "protocolmalformed-messagePeer certificate required") < 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: diff --git a/apps/restconf/restconf_methods_post.c b/apps/restconf/restconf_methods_post.c index a0fe7c16..d0d0cc19 100644 --- a/apps/restconf/restconf_methods_post.c +++ b/apps/restconf/restconf_methods_post.c @@ -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; diff --git a/apps/restconf/restconf_native.c b/apps/restconf/restconf_native.c index f7a5ee3b..3b2c6286 100644 --- a/apps/restconf/restconf_native.c +++ b/apps/restconf/restconf_native.c @@ -55,23 +55,32 @@ #include #include #include +#ifdef HAVE_LIBNGHTTP2 +#include +#endif /* cligen */ #include -/* clicon */ +/* libclixon */ #include /* 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 +#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", + "protocolmalformed-messageThe requested URL or a header is in some way badly formed") < 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 */ diff --git a/apps/restconf/restconf_native.h b/apps/restconf/restconf_native.h index 482bf2fa..5a1db990 100644 --- a/apps/restconf/restconf_native.h +++ b/apps/restconf/restconf_native.h @@ -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_ */ diff --git a/apps/restconf/restconf_nghttp2.c b/apps/restconf/restconf_nghttp2.c index 5d275ed9..9ec1b150 100644 --- a/apps/restconf/restconf_nghttp2.c +++ b/apps/restconf/restconf_nghttp2.c @@ -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? */ diff --git a/configure b/configure index ac5a1067..466ee165 100755 --- a/configure +++ b/configure @@ -5184,6 +5184,7 @@ $as_echo "checking http1 is enabled: $ac_enable_http1" >&6; } if test "$ac_enable_http1" = "yes"; then $as_echo "#define HAVE_HTTP1 true" >>confdefs.h # Must be tree/false (not 0/1) used in shells + HAVE_HTTP1=true fi # Check if nghttp2 is enabled for http/2 diff --git a/configure.ac b/configure.ac index 8d0dbae2..bff862f5 100644 --- a/configure.ac +++ b/configure.ac @@ -238,6 +238,7 @@ elif test "x${with_restconf}" == xnative; then AC_MSG_RESULT(checking http1 is enabled: $ac_enable_http1) if test "$ac_enable_http1" = "yes"; then AC_DEFINE(HAVE_HTTP1,true) # Must be tree/false (not 0/1) used in shells + HAVE_HTTP1=true fi # Check if nghttp2 is enabled for http/2 diff --git a/docker/base/Dockerfile b/docker/base/Dockerfile index f5692d2e..b145eca2 100644 --- a/docker/base/Dockerfile +++ b/docker/base/Dockerfile @@ -81,8 +81,8 @@ RUN apk add --update flex bison # need to add www user manually RUN adduser -D -H -G www-data www-data -# for libevtp -RUN apk add --update openssl libevent + +RUN apk add --update openssl # nghttp2 dependencies RUN apk add --update nghttp2 diff --git a/docker/main/Dockerfile.native b/docker/main/Dockerfile.native index 6300da3c..12573aef 100644 --- a/docker/main/Dockerfile.native +++ b/docker/main/Dockerfile.native @@ -117,8 +117,8 @@ RUN apk add --update flex bison # need to add www user manually RUN adduser -D -H -G www-data www-data -# for libevtp -RUN apk add --update openssl libevent + +RUN apk add --update openssl # nghttp2 dependencies RUN apk add --update nghttp2 diff --git a/lib/src/clixon_string.c b/lib/src/clixon_string.c index bfcb2eeb..e2844eaf 100644 --- a/lib/src/clixon_string.c +++ b/lib/src/clixon_string.c @@ -152,8 +152,9 @@ clicon_strjoin(int argc, /*! Join two string with delimiter. * @param[in] str1 string 1 (will be freed) (optional) * @param[in] del delimiter string (not freed) cannot be NULL (but "") - * @param[in] str2 string 2 (will be freed) + * @param[in] str2 string 2 (not freed) mandatory * @see clicon_strjoin + * This is somewhat of a special case. */ char* clixon_string_del_join(char *str1, @@ -163,8 +164,11 @@ clixon_string_del_join(char *str1, char *str; int len; + if (str2 == NULL){ + clicon_err(OE_UNIX, EINVAL, "str2 is NULL"); + return NULL; + } len = strlen(str2) + 1; - if (str1) len += strlen(str1); len += strlen(del); @@ -178,7 +182,6 @@ clixon_string_del_join(char *str1, } else snprintf(str, len, "%s%s", del, str2); - free(str2); return str; } diff --git a/lib/src/clixon_yang_parse.y b/lib/src/clixon_yang_parse.y index cd0dc1e7..80f82c58 100644 --- a/lib/src/clixon_yang_parse.y +++ b/lib/src/clixon_yang_parse.y @@ -1608,23 +1608,35 @@ deviate_replace_substmt : type_stmt { _PARSE_DEBUG("deviate-replace-subs * */ unknown_stmt : ustring ':' ustring optsep ';' - { char *id; if ((id=clixon_string_del_join($1, ":", $3)) == NULL) _YYERROR("unknown_stmt"); - if (ysp_add(_yy, Y_UNKNOWN, id, NULL) == NULL) _YYERROR("unknown_stmt"); - _PARSE_DEBUG("unknown-stmt -> ustring : ustring ;"); + { + char *id; + if ((id=clixon_string_del_join($1, ":", $3)) == NULL) _YYERROR("unknown_stmt"); + free($3); + if (ysp_add(_yy, Y_UNKNOWN, id, NULL) == NULL) _YYERROR("unknown_stmt"); + _PARSE_DEBUG("unknown-stmt -> ustring : ustring ;"); } | ustring ':' ustring sep string optsep ';' - { char *id; if ((id=clixon_string_del_join($1, ":", $3)) == NULL) _YYERROR("unknown_stmt"); - if (ysp_add(_yy, Y_UNKNOWN, id, $5) == NULL){ _YYERROR("unknown_stmt"); } - _PARSE_DEBUG("unknown-stmt -> ustring : ustring sep string ;"); + { + char *id; + if ((id=clixon_string_del_join($1, ":", $3)) == NULL) _YYERROR("unknown_stmt"); + free($3); + if (ysp_add(_yy, Y_UNKNOWN, id, $5) == NULL){ _YYERROR("unknown_stmt"); } + _PARSE_DEBUG("unknown-stmt -> ustring : ustring sep string ;"); } | ustring ':' ustring optsep - { char *id; if ((id=clixon_string_del_join($1, ":", $3)) == NULL) _YYERROR("unknown_stmt"); + { + char *id; + if ((id=clixon_string_del_join($1, ":", $3)) == NULL) _YYERROR("unknown_stmt"); + free($3); if (ysp_add_push(_yy, Y_UNKNOWN, id, NULL) == NULL) _YYERROR("unknown_stmt"); } '{' yang_stmts '}' { if (ystack_pop(_yy) < 0) _YYERROR("unknown_stmt"); _PARSE_DEBUG("unknown-stmt -> ustring : ustring { yang-stmts }"); } | ustring ':' ustring sep string optsep - { char *id; if ((id=clixon_string_del_join($1, ":", $3)) == NULL) _YYERROR("unknown_stmt"); + { + char *id; + if ((id=clixon_string_del_join($1, ":", $3)) == NULL) _YYERROR("unknown_stmt"); + free($3); if (ysp_add_push(_yy, Y_UNKNOWN, id, $5) == NULL) _YYERROR("unknown_stmt"); } '{' yang_stmts '}' { if (ystack_pop(_yy) < 0) _YYERROR("unknown_stmt"); @@ -1818,10 +1830,14 @@ ustring : ustring CHARS ; abs_schema_nodeid : abs_schema_nodeid '/' node_identifier - { if (($$=clixon_string_del_join($1, "/", $3)) == NULL) _YYERROR("abs_schema_nodeid"); + { + if (($$=clixon_string_del_join($1, "/", $3)) == NULL) _YYERROR("abs_schema_nodeid"); + free($3); _PARSE_DEBUG("absolute-schema-nodeid -> absolute-schema-nodeid / node-identifier"); } | '/' node_identifier - { if (($$=clixon_string_del_join(NULL, "/", $2)) == NULL) _YYERROR("abs_schema_nodeid"); + { + if (($$=clixon_string_del_join(NULL, "/", $2)) == NULL) _YYERROR("abs_schema_nodeid"); + free($2); _PARSE_DEBUG("absolute-schema-nodeid -> / node-identifier"); } ; @@ -1855,7 +1871,9 @@ desc_schema_nodeid : node_identifier { $$= $1; _PARSE_DEBUG("descendant-schema-nodeid -> node_identifier"); } | node_identifier abs_schema_nodeid - { if (($$=clixon_string_del_join($1, "", $2)) == NULL) _YYERROR("desc_schema_nodeid"); + { + if (($$=clixon_string_del_join($1, "", $2)) == NULL) _YYERROR("desc_schema_nodeid"); + free($2); _PARSE_DEBUG("descendant-schema-nodeid -> node_identifier abs_schema_nodeid"); } ; @@ -1894,7 +1912,9 @@ node_identifier : IDENTIFIER { $$=$1; _PARSE_DEBUG("identifier-ref-arg-str -> string"); } | IDENTIFIER ':' IDENTIFIER - { if (($$=clixon_string_del_join($1, ":", $3)) == NULL) _YYERROR("node_identifier"); + { + if (($$=clixon_string_del_join($1, ":", $3)) == NULL) _YYERROR("node_identifier"); + free($3); _PARSE_DEBUG("identifier-ref-arg-str -> prefix : string"); } ; diff --git a/test/config.sh.in b/test/config.sh.in index 82ec7276..5baa7e03 100755 --- a/test/config.sh.in +++ b/test/config.sh.in @@ -40,7 +40,11 @@ # --with-restconf=native Integration with embedded web server WITH_RESTCONF=@with_restconf@ # native, fcgi or "" -HAVE_LIBNGHTTP2=@HAVE_LIBNGHTTP2@ +# HTTP/2? +# If set, curl options are set to use --http2 which may not be what you want, ie +# you may want to force it to http/1 for example +# If so, override before test +: ${HAVE_LIBNGHTTP2:=@HAVE_LIBNGHTTP2@}o HAVE_HTTP1=@HAVE_HTTP1@ # This is for libxml2 XSD regex engine @@ -85,4 +89,6 @@ LIBSTATIC_SUFFIX=@LIBSTATIC_SUFFIX@ LIBS="@LIBS@" CLIXON_YANG_PATCH=@CLIXON_YANG_PATCH@ YANG_STANDARD_DIR=@YANG_STANDARD_DIR@ + YANG_INSTALLDIR=@YANG_INSTALLDIR@ + diff --git a/test/lib.sh b/test/lib.sh index 5bf0a0c2..5432a4e6 100755 --- a/test/lib.sh +++ b/test/lib.sh @@ -110,6 +110,7 @@ if ${HAVE_LIBNGHTTP2}; then CURLOPTS="${CURLOPTS} --http2-prior-knowledge" fi else + CURLOPTS="${CURLOPTS} --http1.1" HVER=1.1 fi diff --git a/test/test_restconf.sh b/test/test_restconf.sh index b0c88b9e..d75ec0a4 100755 --- a/test/test_restconf.sh +++ b/test/test_restconf.sh @@ -454,7 +454,7 @@ function testrun() expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:description":"The-first-interface"}' $proto://$addr/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "HTTP/$HVER 201" new "Add nothing using POST (expect fail)" - expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" $proto://$addr/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "HTTP/$HVER 400" '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"The message-body MUST contain exactly one instance of the expected data resource"}}}' + expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" $proto://$addr/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "HTTP/$HVER 400" '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"The message-body of POST MUST contain exactly one instance of the expected data resource"}}}' new "restconf Check description added" expectpart "$(curl $CURLOPTS -X GET $proto://$addr/restconf/data/ietf-interfaces:interfaces)" 0 "HTTP/$HVER 200" '{"ietf-interfaces:interfaces":{"interface":\[{"name":"eth/0/0","description":"The-first-interface","type":"clixon-example:eth","enabled":true,"oper-status":"up","clixon-example:my-status":{"int":42,"str":"foo"}}\]}}' diff --git a/test/test_restconf_continue.sh b/test/test_restconf_continue.sh new file mode 100755 index 00000000..eded682f --- /dev/null +++ b/test/test_restconf_continue.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +# Restconf HTTP/1.1 Expect/Continue functionality +# Trigger Expect by curl -H. Some curls seem to trigger one on large PUTs but not all + +# Override default to use http/1.1 +HAVE_LIBNGHTTP2=false + +# Magic line must be first in script (see README.md) +s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi + +APPNAME=example + +cfg=$dir/conf.xml +fyang=$dir/restconf.yang +fjson=$dir/large.json + +# Define default restconfig config: RESTCONFIG +RESTCONFIG=$(restconf_config none false) + +# example +cat < $cfg + + clixon-restconf:allow-auth-none + $cfg + /usr/local/share/clixon + $fyang + /usr/local/var/$APPNAME/$APPNAME.sock + $dir/restconf.pidfile + /usr/local/var/$APPNAME + $RESTCONFIG + +EOF + +cat < $fyang +module example{ + yang-version 1.1; + namespace "urn:example:clixon"; + prefix ex; + /* Generic config data */ + container table{ + list parameter{ + key name; + leaf name{ + type string; + } + leaf value{ + type string; + } + } + } +} +EOF + +new "test params: -f $cfg" + +if [ $BE -ne 0 ]; then + new "kill old backend" + sudo clixon_backend -zf $cfg + if [ $? -ne 0 ]; then + err + fi + sudo pkill -f clixon_backend # to be sure + + new "start backend -s init -f $cfg" + start_backend -s init -f $cfg +fi + +new "wait backend" +wait_backend + +if [ $RC -ne 0 ]; then + new "kill old restconf daemon" + stop_restconf_pre + + new "start restconf daemon" + start_restconf -f $cfg +fi + +new "wait restconf" +wait_restconf + + +new "generate large request" +# Add large put, curl seems to create a Expect:100-continue after 1024 bytes +# Alt: add in file if nr=5000 reacts with "Argument list too long" +echo -n '{"example:table":{"parameter":[' > $fjson + +nr=10000 +for (( i=0; i<$nr; i++ )); do + if [ $i -ne 0 ]; then + echo -n ", +" >> $fjson + fi + echo -n "{\"name\":\"A$i\",\"value\":\"$i\"}" >> $fjson +done +echo -n "]}}" >> $fjson + +new "restconf large PUT" +expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -H "Expect: 100-continue" -d @$fjson $RCPROTO://localhost/restconf/data)" 0 "HTTP/$HVER 100 Continue" "HTTP/$HVER 201" + +new "restconf PUT with expect" +expectpart "$(curl $CURLOPTS -H "Expect: 100-continue" -X POST -H "Content-Type: application/yang-data+json" -d '{"example:parameter":[{"name":"A","value":"42"}]}' $RCPROTO://localhost/restconf/data/example:table)" 0 "HTTP/$HVER 100 Continue" "HTTP/$HVER 201" + +new "restconf GET" +expectpart "$(curl $CURLOPTS -X GET $RCPROTO://localhost/restconf/data/example:table/parameter=A)" 0 "HTTP/$HVER 200" '{"example:parameter":\[{"name":"A","value":"42"}\]}' + +if [ $RC -ne 0 ]; then + new "Kill restconf daemon" + stop_restconf +fi + +if [ $BE -ne 0 ]; then + new "Kill backend" + # Check if premature kill + pid=$(pgrep -u root -f clixon_backend) + if [ -z "$pid" ]; then + err "backend already dead" + fi + # kill backend + stop_backend -f $cfg +fi + +# Set by restconf_config +unset RESTCONFIG +unset nr +unset HAVE_LIBNGHTTP2 + +rm -rf $dir + +new "endtest" +endtest diff --git a/test/test_restconf_err.sh b/test/test_restconf_err.sh index 7657c791..9dce3537 100755 --- a/test/test_restconf_err.sh +++ b/test/test_restconf_err.sh @@ -20,6 +20,11 @@ # plugin should be visible in the error message. # XXX does not test rpc-error from backend in api_return_err? +# Override default to use http/1.1 +# Force to HTTP 1.1 no SSL due to netcat +RCPROTO=http +HAVE_LIBNGHTTP2=false + # Magic line must be first in script (see README.md) s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi @@ -37,9 +42,6 @@ fyang2=$dir/augment.yang fxml=$dir/initial.xml fstate=$dir/state.xml -RCPROTO=http # Force to http due to netcat -HVER=1.1 - # Define default restconfig config: RESTCONFIG RESTCONFIG=$(restconf_config none false) @@ -203,15 +205,15 @@ expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPR # But leave it here for debugging where netcat works properly # Alt try something like: # printf "Hello World!" | (exec 3<>/dev/tcp/127.0.0.1/80; cat >&3; cat <&3; exec 3<&-) -if [ false -a ! ${HAVE_LIBNGHTTP2} ] ; then - # Look for netcat or nc for direct socket http calls - if [ -n "$(type netcat 2> /dev/null)" ]; then - netcat="netcat -w 1" # -N does not work on fcgi - elif [ -n "$(type nc 2> /dev/null)" ]; then - netcat=nc - else - err1 "netcat/nc not found" - fi +# Look for netcat or nc for direct socket http calls +if [ -n "$(type netcat 2> /dev/null)" ]; then + netcat="netcat -w 1" # -N does not work on fcgi +elif [ -n "$(type nc 2> /dev/null)" ]; then + netcat=nc +else + netcat= +fi +if [ -n "$netcat" ]; then # new "restconf try fuzz crash" # expectpart "$(${netcat} 127.0.0.1 80 < ~/tmp/crashes/id:000000,sig:06,src:000493+000365,op:splice,rep:8)" 0 "HTTP/$HVER 400" @@ -236,12 +238,12 @@ EOF new "restconf PUT not allowed" expectpart "$(${netcat} 127.0.0.1 80 <malformed-messageThe requested URL or a header is in some way badly formed' -fi # Http/1.1 and netcat Cannot get to work on all platforms +fi # netcat Cannot get to work on all platforms new "restconf XYZ not found" expectpart "$(curl $CURLOPTS -X XYS -H 'Accept: application/yang-data+xml' $RCPROTO://localhost/restconf/data/example:a=0)" 0 "HTTP/$HVER 404" @@ -335,6 +337,7 @@ fi unset RESTCONFIG unset HVER unset RCPROTO +unset HAVE_LIBNGHTTP2 rm -rf $dir diff --git a/test/test_restconf_internal.sh b/test/test_restconf_internal.sh index 8e27e905..44eb9921 100755 --- a/test/test_restconf_internal.sh +++ b/test/test_restconf_internal.sh @@ -145,7 +145,7 @@ function rpcoperation() } # This test is confusing: -# The whole restconf config is in clixon-config wich binds 0.0.0.0:80 which will be the only +# The whole restconf config is in clixon-config which binds 0.0.0.0:80 which will be the only # config the restconf daemon ever reads. # However, enable (and debug) flag is stored in running db but only backend will ever read that. # It just controls how restconf is started, but thereafter the restconf daemon reads the static db in clixon-config file diff --git a/yang/clixon/clixon-config@2021-12-05.yang b/yang/clixon/clixon-config@2021-12-05.yang index 6457f6ad..fb3935fc 100644 --- a/yang/clixon/clixon-config@2021-12-05.yang +++ b/yang/clixon/clixon-config@2021-12-05.yang @@ -601,9 +601,9 @@ module clixon-config { type boolean; default false; description - "Applies to plan (non-tls) http/2 ie when clixon is configured with --enable-nghttp2 + "Applies to plain (non-tls) http/2 ie when clixon is configured with --enable-nghttp2 If false, disable direct and upgrade for plain(non-tls) HTTP/2. - If true, allows direct and upgrade for plain(non-tls) HTTP/2. + If true, allow direct and upgrade for plain(non-tls) HTTP/2. It may especially useful to disable in http/1 + http/2 mode to avoid the complex upgrade/switch from http/1 to http/2. Note this also disables plain http/2 in prior-knowledge, that is, in http/2-only mode. diff --git a/yang/clixon/clixon-restconf@2021-05-20.yang b/yang/clixon/clixon-restconf@2021-05-20.yang index 1c557240..5f48c1d2 100644 --- a/yang/clixon/clixon-restconf@2021-05-20.yang +++ b/yang/clixon/clixon-restconf@2021-05-20.yang @@ -207,7 +207,8 @@ module clixon-restconf { } list socket { description - "List of server sockets that the restconf daemon listens to"; + "List of server sockets that the restconf daemon listens to. + Not fcgi"; key "namespace address port"; leaf namespace { type string;