From 84d88c8ad8dac325c23687826f05528242eb6917 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Sun, 24 Apr 2022 09:44:27 +0200 Subject: [PATCH] Restconf http-data server updates Check data paths for .., ~ and soft links Changed semantics of `CLICON_HTTP_DATA_PATH` and `_ROOT` Change URI catch-all to 404 instead of 400 Fixed some memory leaks --- apps/restconf/clixon_http_data.c | 178 ++++++++++++++++------ apps/restconf/clixon_http_data.h | 2 +- apps/restconf/restconf_http1.c | 11 +- apps/restconf/restconf_nghttp2.c | 11 +- apps/restconf/restconf_root.c | 17 ++- apps/restconf/restconf_stream_fcgi.c | 20 ++- configure.ac | 1 + doc/DEVELOP.md | 10 ++ test/lib.sh | 11 ++ test/test_http_data.sh | 114 +++++++++++--- test/test_restconf_err.sh | 13 -- yang/clixon/clixon-config@2022-03-21.yang | 21 ++- 12 files changed, 303 insertions(+), 106 deletions(-) diff --git a/apps/restconf/clixon_http_data.c b/apps/restconf/clixon_http_data.c index 72249aed..ba98acb0 100644 --- a/apps/restconf/clixon_http_data.c +++ b/apps/restconf/clixon_http_data.c @@ -86,32 +86,34 @@ static const map_str2str mime_map[] = { /*! Check if uri path denotes a data path * - * @param[out] data Pointer to string where data starts if retval = 1 + * @param[in] h Clixon handle * @retval 0 No, not a data path, or not enabled * @retval 1 Yes, a data path and "data" points to www-data if given */ int -api_path_is_data(clicon_handle h, - char **data) +api_path_is_data(clicon_handle h) { - char *path; + int retval = 0; + char *path = NULL; char *http_data_path; if (restconf_http_data_get(h) == 0) - return 0; + goto done; if ((path = restconf_uripath(h)) == NULL) - return 0; + goto done; if ((http_data_path = clicon_option_str(h, "CLICON_HTTP_DATA_PATH")) == NULL) - return 0; + goto done; if (strlen(path) < strlen(http_data_path)) - return 0; + goto done; if (path[0] != '/') - return 0; + goto done; if (strncmp(path, http_data_path, strlen(http_data_path)) != 0) - return 0; - if (data) - *data = path + strlen(http_data_path); - return 1; + goto done; + retval = 1; + done: + if (path) + free(path); + return retval; } /*! Generic restconf error function on get/head request @@ -156,6 +158,75 @@ api_http_data_err(clicon_handle h, return retval; } +/*! Check validity of path, may only be regular dir or file + * No .., soft link, ~, etc + * @param[in] prefix Prefix of path0, where to start file check + * @param[in] path0 Filepath + * @param[out] code Error code, if retval = 0 + * @retval -1 Error + * @retval 0 Invalid, code set + * @retval 1 OK + */ +static int +check_file_path(char *prefix, + char *path0, + int *code) +{ + int retval = -1; + char *path = NULL; + int i; + struct stat fstat; + + if (prefix == NULL || path0 == NULL || code == NULL){ + clicon_err(OE_UNIX, EINVAL, "prefix, path0 or code is NULL"); + goto done; + } + if ((path = strdup(path0)) == NULL){ + clicon_err(OE_UNIX, errno, "strdup"); + goto done; + } + for (i=strlen(prefix); istrlen(prefix) && path[i-1] == '.'){ + *code = 403; + goto invalid; + } + } + /* Resulting file (ensure not soft link) */ + if (lstat(path, &fstat) < 0){ + *code = 404; + goto invalid; + } + if (!S_ISREG(fstat.st_mode)){ + *code = 403; + goto invalid; + } + retval = 1; /* OK */ + done: + if (path) + free(path); + return retval; + invalid: + retval = 0; + goto done; +} + /*! Read file data request * @param[in] h Clicon handle * @param[in] req Generic Www handle (can be part of clixon handle) @@ -167,21 +238,23 @@ api_http_data_err(clicon_handle h, */ static int api_http_data_file(clicon_handle h, - void *req, - char *pathname, - int head) + void *req, + char *pathname, + int head) { int retval = -1; cbuf *cbfile = NULL; char *filename; - struct stat fstat; cbuf *cbdata = NULL; FILE *f = NULL; long fsize; - size_t sz; char *www_data_root = NULL; char *suffix; char *media; + int ret; + int code = 0; + char *str = NULL; + size_t sz; if ((cbfile = cbuf_new()) == NULL){ clicon_err(OE_UNIX, errno, "cbuf_new"); @@ -204,8 +277,12 @@ api_http_data_file(clicon_handle h, if (pathname) cprintf(cbfile, "/%s", pathname); filename = cbuf_get(cbfile); - if (stat(filename, &fstat) < 0){ - if (api_http_data_err(h, req, 404) < 0) /* not found */ + clicon_debug(1, "%s %s", __FUNCTION__, filename); + if ((ret = check_file_path(www_data_root, filename, &code)) < 0) + goto done; + if (ret == 0){ + clicon_debug(1, "%s code:%d", __FUNCTION__, code); + if (api_http_data_err(h, req, code) < 0) goto done; goto ok; } @@ -214,6 +291,9 @@ api_http_data_file(clicon_handle h, goto done; goto ok; } + /* Size could have been taken from stat() but this reduces the race condition interval + * There is still one without flock + */ fseek(f, 0, SEEK_END); fsize = ftell(f); fseek(f, 0, SEEK_SET); /* same as rewind(f); */ @@ -221,31 +301,28 @@ api_http_data_file(clicon_handle h, clicon_err(OE_UNIX, errno, "cbuf_new_alloc"); goto done; } -#if 0 /* Direct read but cannot set cb_len via API */ - fread(cbuf_get(cbdata), fsize, 1, f); -#else - { - char *str; - if ((str = malloc(fsize + 1)) == NULL){ - clicon_err(OE_UNIX, errno, "malloc"); - goto done; - } - if ((sz = fread(str, fsize, 1, f)) < 0){ - clicon_err(OE_UNIX, errno, "fread"); - goto done; - } - if (sz != 1){ - clicon_log(LOG_NOTICE, "%s: file read %s", __FUNCTION__, filename); - // XXX error handling: file read - goto done; - } - str[fsize] = 0; - if (cbuf_append_str(cbdata, str) < 0){ - clicon_err(OE_UNIX, errno, "cbuf_append_str"); - goto done; - } + /* Unoptimized, no direct read but requires an extra copy, + * the cligen buf API should have some mechanism for this case without the extra copy. + */ + if ((str = malloc(fsize + 1)) == NULL){ + clicon_err(OE_UNIX, errno, "malloc"); + goto done; + } + if ((sz = fread(str, fsize, 1, f)) < 0){ + clicon_err(OE_UNIX, errno, "fread"); + goto done; + } + if (sz != 1){ + if (api_http_data_err(h, req, 500) < 0) /* Internal error? */ + goto done; + goto ok; + } + clicon_debug(1, "%s code:%d", __FUNCTION__, code); + str[fsize] = 0; + if (cbuf_append_str(cbdata, str) < 0){ + clicon_err(OE_UNIX, errno, "cbuf_append_str"); + goto done; } -#endif if (restconf_reply_header(req, "Content-Type", "%s", media) < 0) goto done; if (restconf_reply_send(req, 200, cbdata, head) < 0) @@ -254,6 +331,8 @@ api_http_data_file(clicon_handle h, ok: retval = 0; done: + if (str) + free(str); if (f) fclose(f); if (cbfile) @@ -291,8 +370,8 @@ api_http_data(clicon_handle h, int options = 0; int ret; cbuf *indata = NULL; - char *pathname = NULL; - + char *path = NULL; + clicon_debug(1, "%s", __FUNCTION__); if (req == NULL){ errno = EINVAL; @@ -300,11 +379,12 @@ api_http_data(clicon_handle h, } /* 1. path: with stripped prefix, ultimately: dir/filename */ - if (!api_path_is_data(h, &pathname)){ + if (!api_path_is_data(h)){ if (api_http_data_err(h, req, 404) < 0) /* not found */ goto done; goto ok; } + path = restconf_uripath(h); /* 2. operation GET or HEAD */ request_method = restconf_param_get(h, "REQUEST_METHOD"); if (strcmp(request_method, "GET") == 0){ @@ -360,11 +440,13 @@ api_http_data(clicon_handle h, if (restconf_reply_send(req, 200, NULL, 0) < 0) goto done; } - else if (api_http_data_file(h, req, pathname, head) < 0) + else if (api_http_data_file(h, req, path, head) < 0) goto done; ok: retval = 0; done: + if (path) + free(path); clicon_debug(1, "%s %d", __FUNCTION__, retval); return retval; } diff --git a/apps/restconf/clixon_http_data.h b/apps/restconf/clixon_http_data.h index 0174f0e1..f1c70c44 100644 --- a/apps/restconf/clixon_http_data.h +++ b/apps/restconf/clixon_http_data.h @@ -41,7 +41,7 @@ /* * Prototypes */ -int api_path_is_data(clicon_handle h, char **data); +int api_path_is_data(clicon_handle h); int api_http_data(clicon_handle h, void *req, cvec *qvec); #endif /* _CLIXON_HTTP_DATA_H_ */ diff --git a/apps/restconf/restconf_http1.c b/apps/restconf/restconf_http1.c index f5ea9c6b..7b3332e5 100644 --- a/apps/restconf/restconf_http1.c +++ b/apps/restconf/restconf_http1.c @@ -345,7 +345,7 @@ restconf_http1_path_root(clicon_handle h, restconf_conn *rc) { int retval = -1; - restconf_stream_data *sd; + restconf_stream_data *sd = NULL; cvec *cvv = NULL; char *cn; char *subject = NULL; @@ -380,6 +380,13 @@ restconf_http1_path_root(clicon_handle h, goto done; goto fail; } +#if 1 + /* XXX gives mem leak in multiple requests, + * but maybe the error is that sd is not freed. + */ + if (sd->sd_path != NULL) + free(sd->sd_path); +#endif if ((sd->sd_path = restconf_uripath(rc->rc_h)) == NULL) goto done; // XXX SHOULDNT EXIT if no REQUEST_URI if (rc->rc_proto_d2 == 0 && rc->rc_proto == HTTP_11) @@ -427,7 +434,7 @@ restconf_http1_path_root(clicon_handle h, if (api_root_restconf(h, sd, sd->sd_qvec) < 0) goto done; } - else if (api_path_is_data(h, NULL)){ + else if (api_path_is_data(h)){ if (api_http_data(h, sd, sd->sd_qvec) < 0) goto done; } diff --git a/apps/restconf/restconf_nghttp2.c b/apps/restconf/restconf_nghttp2.c index 059c50b2..4e1a222c 100644 --- a/apps/restconf/restconf_nghttp2.c +++ b/apps/restconf/restconf_nghttp2.c @@ -325,7 +325,7 @@ restconf_nghttp2_path(restconf_stream_data *sd) if (api_root_restconf(h, sd, sd->sd_qvec) < 0) goto done; } - else if (api_path_is_data(h, NULL)){ + else if (api_path_is_data(h)){ if (api_http_data(h, sd, sd->sd_qvec) < 0) goto done; } @@ -475,14 +475,17 @@ http2_exec(restconf_conn *rc, sd->sd_proto = HTTP_2; /* XXX is this necessary? */ if (strncmp(sd->sd_path, "/" RESTCONF_API, strlen("/" RESTCONF_API)) == 0 || strcmp(sd->sd_path, RESTCONF_WELL_KNOWN) == 0 - || api_path_is_data(rc->rc_h, NULL)){ + || api_path_is_data(rc->rc_h)){ if (restconf_nghttp2_path(sd) < 0) goto done; } else{ - sd->sd_code = 400; - ; /* ignore */ + sd->sd_code = 404; /* not found */ + } + if (restconf_param_del_all(rc->rc_h) < 0) // XXX + goto done; + /* 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 diff --git a/apps/restconf/restconf_root.c b/apps/restconf/restconf_root.c index c75d5b85..e5893dcb 100644 --- a/apps/restconf/restconf_root.c +++ b/apps/restconf/restconf_root.c @@ -78,18 +78,23 @@ int api_path_is_restconf(clicon_handle h) { - char *path; + int retval = 0; + char *path = NULL; char *restconf_path = RESTCONF_API; if ((path = restconf_uripath(h)) == NULL) - return 0; + goto done; if (strlen(path) < 1 + strlen(restconf_path)) /* "/" + restconf */ - return 0; + goto done; if (path[0] != '/') - return 0; + goto done; if (strncmp(path+1, restconf_path, strlen(restconf_path)) != 0) - return 0; - return 1; + goto done; + retval = 1; + done: + if (path) + free(path); + return retval; } /*! Determine the root of the RESTCONF API by accessing /.well-known diff --git a/apps/restconf/restconf_stream_fcgi.c b/apps/restconf/restconf_stream_fcgi.c index 2792168c..1b14367a 100644 --- a/apps/restconf/restconf_stream_fcgi.c +++ b/apps/restconf/restconf_stream_fcgi.c @@ -126,20 +126,26 @@ static struct stream_child *STREAM_CHILD = NULL; int api_path_is_stream(clicon_handle h) { - char *path; + int retval = 0; + char *path = NULL; char *stream_path; if ((path = restconf_uripath(h)) == NULL) - return 0; + goto done; if ((stream_path = clicon_option_str(h, "CLICON_STREAM_PATH")) == NULL) - return 0; + goto done; if (strlen(path) < 1 + strlen(stream_path)) /* "/" + stream */ - return 0; + goto done; if (path[0] != '/') - return 0; + goto done; if (strncmp(path+1, stream_path, strlen(stream_path)) != 0) - return 0; - return 1; + goto done; + retval = 1; + done: + if (path) + free(path); + return retval; + } /*! Find restconf child using PID and cleanup FCGI Request data diff --git a/configure.ac b/configure.ac index 5b20156f..1cdf5601 100644 --- a/configure.ac +++ b/configure.ac @@ -86,6 +86,7 @@ AC_DEFINE_UNQUOTED(CLIXON_VERSION_PATCH, $CLIXON_VERSION_PATCH, [Clixon path ver AC_CHECK_LIB(m, main) +# defines: target_cpu, target_vendor, and target_os. AC_CANONICAL_TARGET # AC_SUBST(var) makes @var@ appear in makefiles. diff --git a/doc/DEVELOP.md b/doc/DEVELOP.md index 60b3447f..55b42d6f 100644 --- a/doc/DEVELOP.md +++ b/doc/DEVELOP.md @@ -113,6 +113,16 @@ Use: - PRIu64 for uint64 - %p for pointers +### Include files + +Avoid include statements in .h files, place them in .c files whenever possible. + +The reason is to avoid deep include chains where file dependencies are +difficult to analyze and understand. If include statements are only placed in .c +files, there is only a single level of include file dependencies. + +The drawback is that the same include file may need to be repeated in many .c files. + ## How to work in git Clixon uses semantic versioning (https://semver.org). diff --git a/test/lib.sh b/test/lib.sh index 74cbe64c..dbf58f1e 100755 --- a/test/lib.sh +++ b/test/lib.sh @@ -94,6 +94,17 @@ DEFAULTNS="$DEFAULTONLY message-id=\"42\"" # Minimal hello message as a prelude to netconf rpcs DEFAULTHELLO="urn:ietf:params:netconf:base:1.0urn:ietf:params:netconf:base:1.1]]>]]>" +# XXX cannot get this to work for all combinations of nc/netcat fcgi/native +# But leave it here for debugging where netcat works properly +if [ -n "$(type netcat 2> /dev/null)" ]; then + netcat="netcat -w 1" # -N does not work on fcgi +# nc on freebsd does not work either +#elif [ -n "$(type nc 2> /dev/null)" ]; then +# netcat=nc +else + netcat= +fi + # Options passed to curl calls # -s : silent # -S : show error diff --git a/test/test_http_data.sh b/test/test_http_data.sh index 84612b13..44292c80 100755 --- a/test/test_http_data.sh +++ b/test/test_http_data.sh @@ -14,6 +14,7 @@ APPNAME=example cfg=$dir/conf.xml rm -rf $dir/www mkdir $dir/www +mkdir $dir/www/data # Does not work with fcgi if [ "${WITH_RESTCONF}" = "fcgi" ]; then @@ -22,7 +23,7 @@ if [ "${WITH_RESTCONF}" = "fcgi" ]; then fi # Data file -cat < $dir/www/index.html +cat < $dir/www/data/index.html @@ -43,7 +44,7 @@ working. Further configuration is required.

EOF -cat < $dir/www/example.css +cat < $dir/www/data/example.css img { display: inline; border: @@ -67,6 +68,54 @@ h1,h2,h3,h4,h5,h6 { } EOF +# Outside wwwdir, should not be able to access this +cat < $dir/outside.html + + + +Dont access this + + + +

Dont access this!

+

If you see this page, you accessed a file outside the root domain

+ + +EOF + +# Create a soft link from inside to outside +ln -s $dir/outside.html $dir/www/data/inside.html + +# Disable read access +cat < $dir/www/data/noread.html + + + +No read + + + +

No read!

+

If you see this page, you have read access to root

+ + +EOF + +# remove read access +chmod 660 $dir/www/data/noread.html + # Http test routine with arguments: # 1. proto:http/https function testrun() @@ -134,32 +183,55 @@ EOF new "wait restconf" wait_restconf $proto -# echo "curl $CURLOPTS -X GET -H 'Accept: text/html' $proto://localhost/data/index.html" - if $enable; then + echo "curl $CURLOPTS -X GET -H 'Accept: text/html' $proto://localhost/data/index.html" + + if ! $enable; then + # XXX or bad request? + new "WWW get html, not enabled, expect not found" +# echo "curl $CURLOPTS -X GET -H 'Accept: text/html' $proto://localhost/data/index.html" + expectpart "$(curl $CURLOPTS -X GET -H 'Accept: text/html' $proto://localhost/data/index.html)" 0 "HTTP/$HVER 404" + else new "WWW get html" expectpart "$(curl $CURLOPTS -X GET -H 'Accept: text/html' $proto://localhost/data/index.html)" 0 "HTTP/$HVER 200" "Content-Type: text/html" "Welcome to Clixon!" - else - new "WWW get html, not enabled, expect bad request" - expectpart "$(curl $CURLOPTS -X GET -H 'Accept: text/html' $proto://localhost/data/index.html)" 0 "HTTP/$HVER 400" - return - fi - new "WWW get css" - expectpart "$(curl $CURLOPTS -X GET -H 'Accept: text/html' $proto://localhost/data/example.css)" 0 "HTTP/$HVER 200" "Content-Type: text/css" "display: inline;" --not-- "Content-Type: text/html" + new "WWW get css" + expectpart "$(curl $CURLOPTS -X GET -H 'Accept: text/html' $proto://localhost/data/example.css)" 0 "HTTP/$HVER 200" "Content-Type: text/css" "display: inline;" --not-- "Content-Type: text/html" - new "WWW head" - expectpart "$(curl $CURLOPTS --head -H 'Accept: text/html' $proto://localhost/data/index.html)" 0 "HTTP/$HVER 200" "Content-Type: text/html" --not-- "Welcome to Clixon!" + new "WWW head" + expectpart "$(curl $CURLOPTS --head -H 'Accept: text/html' $proto://localhost/data/index.html)" 0 "HTTP/$HVER 200" "Content-Type: text/html" --not-- "Welcome to Clixon!" - new "WWW options" - expectpart "$(curl $CURLOPTS -X OPTIONS $proto://localhost/data/index.html)" 0 "HTTP/$HVER 200" "allow: OPTIONS,HEAD,GET" + new "WWW options" + expectpart "$(curl $CURLOPTS -X OPTIONS $proto://localhost/data/index.html)" 0 "HTTP/$HVER 200" "allow: OPTIONS,HEAD,GET" - # negative errors - new "WWW get http not found" - expectpart "$(curl $CURLOPTS -X GET -H 'Accept: text/html' $proto://localhost/data/notfound.html)" 0 "HTTP/$HVER 404" "Content-Type: text/html" "404 Not Found" + # negative errors + new "WWW get http not found" + expectpart "$(curl $CURLOPTS -X GET -H 'Accept: text/html' $proto://localhost/data/notfound.html)" 0 "HTTP/$HVER 404" "Content-Type: text/html" "404 Not Found" - new "WWW post not allowed" - expectpart "$(curl $CURLOPTS -X POST -H 'Accept: text/html' -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}}' $proto://localhost/data/notfound.html)" 0 "HTTP/$HVER 405" "Content-Type: text/html" "405 Method Not Allowed" + new "WWW get http soft link" + expectpart "$(curl $CURLOPTS -X GET -H 'Accept: text/html' $proto://localhost/data/inside.html)" 0 "HTTP/$HVER 403" "Content-Type: text/html" "403 Forbidden" --not-- "Dont access this" + + if [ ! -f /.dockerenv ] ; then # XXX Privs dont not work on docker/alpine? + new "WWW get http not read access" + expectpart "$(curl $CURLOPTS -X GET -H 'Accept: text/html' $proto://localhost/data/noread.html)" 0 "HTTP/$HVER 403" "Content-Type: text/html" "403 Forbidden" + fi + # Try .. Cannot get .. in path to work in curl (it seems to remove it) + if [ "$proto" = http -a -n "$netcat" ]; then + new "WWW get outside using .. netcat" + expectpart "$(${netcat} 127.0.0.1 80 <405 Method Not Allowed" + + fi + if [ $RC -ne 0 ]; then new "Kill restconf daemon" stop_restconf @@ -190,7 +262,7 @@ if [ "${WITH_RESTCONF}" = "native" ]; then fi for proto in $protos; do - for enable in true false; do + for enable in true false; do # false new "http-data proto:$proto enabled:$enable" testrun $proto $enable done diff --git a/test/test_restconf_err.sh b/test/test_restconf_err.sh index 1a936cde..ef242012 100755 --- a/test/test_restconf_err.sh +++ b/test/test_restconf_err.sh @@ -209,19 +209,6 @@ expectpart "$(curl $CURLOPTS -X POST -H 'Content-Type: application/yang-data+xml new "restconf GET initial datastore" expectpart "$(curl $CURLOPTS -X GET -H 'Accept: application/yang-data+xml' $RCPROTO://localhost/restconf/data/example:a=0)" 0 "HTTP/$HVER 200" "$XML" -# XXX cannot get this to work for all combinations of nc/netcat fcgi/native -# 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<&-) -# 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 -# nc on freebsd does not work either -#elif [ -n "$(type nc 2> /dev/null)" ]; then -# netcat=nc -else - netcat= -fi if [ -n "$netcat" -a "${WITH_RESTCONF}" != "fcgi" ]; then # new "restconf try fuzz crash" diff --git a/yang/clixon/clixon-config@2022-03-21.yang b/yang/clixon/clixon-config@2022-03-21.yang index 4ea0af1f..c1e9eb3c 100644 --- a/yang/clixon/clixon-config@2022-03-21.yang +++ b/yang/clixon/clixon-config@2022-03-21.yang @@ -605,16 +605,28 @@ module clixon-config { } leaf CLICON_HTTP_DATA_PATH { if-feature "clrc:http-data"; + default "/"; type string; description - "If set, enable www data on this sub-path, must start with / (example: /data)"; + "URI match for http-data serving files specified by CLICON_HTTP_DATA_ROOT. + Must start with / (example: /) + Restconf paths at /restconf is always done before data (or streams) + The PATH is appended to CLICON_HTTP_DATA_ROOT to find a file. + Example, if PATH is /data and ROOT is /www, and a GET /index.html, the + corresponding file is '/www/data/index.html' + Both feature clixon-restconf:http-data and restconf/enable-http-data + must be enabled for this match to occur."; } leaf CLICON_HTTP_DATA_ROOT { if-feature "clrc:http-data"; type string; default "/var/www"; description - "public web root"; + "Location in file system where http-data files are looked for. + Soft links, '..', '~' etc are not followed. + See also CLICON_HTTP_DATA_PATH + Both feature clixon-restconf:http-data and restconf/enable-http-data + must be enabled for this match to occur."; } leaf CLICON_CLI_DIR { type string; @@ -1051,8 +1063,9 @@ module clixon-config { leaf CLICON_STREAM_PATH { type string; default "streams"; - description "Stream path appended to CLICON_STREAM_URL to form - stream subscription URL."; + description + "Stream path appended to CLICON_STREAM_URL to form + stream subscription URL."; } leaf CLICON_STREAM_URL { type string;