* 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? */

1
configure vendored
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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;
}

View file

@ -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"); }
;

View file

@ -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@

View file

@ -110,6 +110,7 @@ if ${HAVE_LIBNGHTTP2}; then
CURLOPTS="${CURLOPTS} --http2-prior-knowledge"
fi
else
CURLOPTS="${CURLOPTS} --http1.1"
HVER=1.1
fi

View file

@ -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"}}\]}}'

131
test/test_restconf_continue.sh Executable file
View file

@ -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)
# <CLICON_YANG_MODULE_MAIN>example</CLICON_YANG_MODULE_MAIN>
cat <<EOF > $cfg
<clixon-config xmlns="http://clicon.org/config">
<CLICON_FEATURE>clixon-restconf:allow-auth-none</CLICON_FEATURE> <!-- Use auth-type=none -->
<CLICON_CONFIGFILE>$cfg</CLICON_CONFIGFILE>
<CLICON_YANG_DIR>/usr/local/share/clixon</CLICON_YANG_DIR>
<CLICON_YANG_MAIN_FILE>$fyang</CLICON_YANG_MAIN_FILE>
<CLICON_SOCK>/usr/local/var/$APPNAME/$APPNAME.sock</CLICON_SOCK>
<CLICON_BACKEND_PIDFILE>$dir/restconf.pidfile</CLICON_BACKEND_PIDFILE>
<CLICON_XMLDB_DIR>/usr/local/var/$APPNAME</CLICON_XMLDB_DIR>
$RESTCONFIG
</clixon-config>
EOF
cat <<EOF > $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

View file

@ -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 <<EOF
PUT /restconf/.well-known/host-meta HTTP/$HVER
PUT /.well-known/host-meta HTTP/$HVER
Host: localhost
Accept: application/yang-data+xml
EOF
)" 0 "HTTP/$HVER 405" kalle # nginx uses "method not allowed"
)" 0 "HTTP/$HVER 405" # nginx uses "method not allowed"
new "restconf GET wrong http version raw"
expectpart "$(${netcat} 127.0.0.1 80 <<EOF
@ -252,7 +254,7 @@ Accept: application/yang-data+xml
EOF
)" 0 "HTTP/$HVER 400" # native: '<error-tag>malformed-message</error-tag><error-message>The requested URL or a header is in some way badly formed</error-message>'
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

View file

@ -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

View file

@ -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.

View file

@ -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;