Check of enable-http-data config option HTTP/2 Return 400 bad request if no path match Test: updated yang file revisions, extended restconf config with http-data
370 lines
11 KiB
C
370 lines
11 KiB
C
/*
|
|
*
|
|
***** BEGIN LICENSE BLOCK *****
|
|
|
|
Copyright (C) 2009-2019 Olof Hagsand
|
|
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 *****
|
|
*
|
|
* Limited static http data service embedded in restconf code
|
|
*/
|
|
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "clixon_config.h" /* generated by config & autoconf */
|
|
#endif
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
#include <signal.h>
|
|
#include <syslog.h>
|
|
#include <fcntl.h>
|
|
#include <time.h>
|
|
#include <limits.h>
|
|
#include <signal.h>
|
|
#include <sys/time.h>
|
|
#include <sys/wait.h>
|
|
#include <libgen.h>
|
|
#include <sys/stat.h> /* chmod */
|
|
|
|
/* cligen */
|
|
#include <cligen/cligen.h>
|
|
|
|
/* clicon */
|
|
#include <clixon/clixon.h>
|
|
|
|
/* restconf */
|
|
#include "restconf_lib.h"
|
|
#include "restconf_handle.h"
|
|
#include "restconf_api.h"
|
|
#include "restconf_err.h"
|
|
#include "clixon_http_data.h"
|
|
|
|
/* File extension <-> HTTP Content media mapping
|
|
* File extensions (on the left) MIME types (to the right)
|
|
* @see https://www.iana.org/assignments/media-types/media-types.xhtml
|
|
*/
|
|
static const map_str2str mime_map[] = {
|
|
{"html", "text/html"},
|
|
{"css", "text/css"},
|
|
{"eot", "application/vnd.ms-fontobject"},
|
|
{"woff", "application/font-woff"},
|
|
{"js", "application/javascript"},
|
|
{"svg", "image/svg+xml"},
|
|
{"ico", "image/x-icon"},
|
|
{"woff2", "application/font-woff2"}, /* font/woff2? */
|
|
{ NULL, NULL} /* if not found: application/octet-stream */
|
|
};
|
|
|
|
/*! Check if uri path denotes a data path
|
|
*
|
|
* @param[out] data Pointer to string where data starts if retval = 1
|
|
* @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)
|
|
{
|
|
char *path;
|
|
char *http_data_path;
|
|
|
|
if (restconf_http_data_get(h) == 0)
|
|
return 0;
|
|
if ((path = restconf_uripath(h)) == NULL)
|
|
return 0;
|
|
if ((http_data_path = clicon_option_str(h, "CLICON_HTTP_DATA_PATH")) == NULL)
|
|
return 0;
|
|
if (strlen(path) < strlen(http_data_path))
|
|
return 0;
|
|
if (path[0] != '/')
|
|
return 0;
|
|
if (strncmp(path, http_data_path, strlen(http_data_path)) != 0)
|
|
return 0;
|
|
if (data)
|
|
*data = path + strlen(http_data_path);
|
|
return 1;
|
|
}
|
|
|
|
/*! Generic restconf error function on get/head request
|
|
* @param[in] h Clixon handle
|
|
* @param[in] req Generic http handle
|
|
* @param[in] code Error code
|
|
* @see api_return_err
|
|
*/
|
|
static int
|
|
api_http_data_err(clicon_handle h,
|
|
void *req,
|
|
int code)
|
|
{
|
|
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;
|
|
}
|
|
if (restconf_reply_header(req, "Content-Type", "text/html") < 0)
|
|
goto done;
|
|
cprintf(cb, "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\r\n");
|
|
cprintf(cb, "<html><head>\r\n");
|
|
cprintf(cb, "<title>%d %s</title>\r\n", code, restconf_code2reason(code));
|
|
cprintf(cb, "</head><body>\r\n");
|
|
cprintf(cb, "<h1>%s</h1>\r\n", restconf_code2reason(code));
|
|
#if 0 /* Cant find this sentence in any RFC, is it ad-hoc? */
|
|
cprintf(cb, "<p>The requested URL was not found on this server.</p>\r\n");
|
|
#endif
|
|
cprintf(cb, "</body></html>\r\n");
|
|
if (restconf_reply_send(req, code, cb, 0) < 0)
|
|
goto done;
|
|
cb = NULL;
|
|
// ok:
|
|
retval = 0;
|
|
done:
|
|
clicon_debug(1, "%s retval:%d", __FUNCTION__, retval);
|
|
if (cb)
|
|
cbuf_free(cb);
|
|
return retval;
|
|
}
|
|
|
|
/*! Read file data request
|
|
* @param[in] h Clicon handle
|
|
* @param[in] req Generic Www handle (can be part of clixon handle)
|
|
* @param[in] pathname With stripped prefix (eg /data), ultimately a filename
|
|
* @note: primitive file handling, just check if file exists and read it all
|
|
* XXX 1: Buffer copying once too many, see #if 0 below
|
|
* XXX 2: Generic file system below CLICON_HTTP_DATA_ROOT, no checks for links or ..
|
|
* Need pathname santitization: no .. or ~, just a directory structure.
|
|
*/
|
|
static int
|
|
api_http_data_file(clicon_handle h,
|
|
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;
|
|
|
|
if ((cbfile = cbuf_new()) == NULL){
|
|
clicon_err(OE_UNIX, errno, "cbuf_new");
|
|
goto done;
|
|
}
|
|
if ((www_data_root = clicon_option_str(h, "CLICON_HTTP_DATA_ROOT")) == NULL){
|
|
clicon_err(OE_RESTCONF, ENOENT, "CLICON_HTTP_DATA_ROOT missing");
|
|
goto done;
|
|
}
|
|
|
|
if ((suffix = rindex(pathname, '.')) == NULL){
|
|
media = "application/octet-stream";
|
|
}
|
|
else {
|
|
suffix++;
|
|
if ((media = clicon_str2str(mime_map, suffix)) == NULL)
|
|
media = "application/octet-stream";
|
|
}
|
|
cprintf(cbfile, "%s", www_data_root);
|
|
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 */
|
|
goto done;
|
|
goto ok;
|
|
}
|
|
if ((f = fopen(filename, "rb")) == NULL){
|
|
if (api_http_data_err(h, req, 403) < 0) /* Forbidden or 500? */
|
|
goto done;
|
|
goto ok;
|
|
}
|
|
fseek(f, 0, SEEK_END);
|
|
fsize = ftell(f);
|
|
fseek(f, 0, SEEK_SET); /* same as rewind(f); */
|
|
if ((cbdata = cbuf_new_alloc(fsize+1)) == NULL){
|
|
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;
|
|
}
|
|
}
|
|
#endif
|
|
if (restconf_reply_header(req, "Content-Type", "%s", media) < 0)
|
|
goto done;
|
|
if (restconf_reply_send(req, 200, cbdata, head) < 0)
|
|
goto done;
|
|
cbdata = NULL; /* consumed by reply-send */
|
|
ok:
|
|
retval = 0;
|
|
done:
|
|
if (f)
|
|
fclose(f);
|
|
if (cbfile)
|
|
cbuf_free(cbfile);
|
|
if (cbdata)
|
|
cbuf_free(cbdata);
|
|
return retval;
|
|
}
|
|
|
|
/*! Get data request
|
|
*
|
|
* This implementation is constrained as follows:
|
|
* 1. Enable as part of restconf and set feature www-data and CLICON_HTTP_DATA_PATH
|
|
* 2. path: Local files within CLICON_HTTP_DATA_ROOT
|
|
* 3. operations: GET, HEAD, OPTIONS
|
|
* 4. query parameters not supported
|
|
* 5. indata should be NULL (no write operations)
|
|
* 6. Limited media: text/html, JavaScript, image, and css
|
|
* 7. Authentication as restconf
|
|
* @param[in] h Clicon handle
|
|
* @param[in] req Generic Www handle (can be part of clixon handle)
|
|
* @param[in] qvec Query parameters, ie the ?<id>=<val>&<id>=<val> stuff
|
|
* @param[in] pathname With stripped prefix (eg /data), ultimately a filename
|
|
* Need to enable clixon-restconf.yang www-data feature
|
|
*/
|
|
int
|
|
api_http_data(clicon_handle h,
|
|
void *req,
|
|
cvec *qvec)
|
|
{
|
|
int retval = -1;
|
|
char *request_method = NULL;
|
|
char *media_str = NULL;
|
|
int head = 0;
|
|
int options = 0;
|
|
int ret;
|
|
cbuf *indata = NULL;
|
|
char *pathname = NULL;
|
|
|
|
clicon_debug(1, "%s", __FUNCTION__);
|
|
if (req == NULL){
|
|
errno = EINVAL;
|
|
goto done;
|
|
}
|
|
/* 1. path: with stripped prefix, ultimately: dir/filename
|
|
*/
|
|
if (!api_path_is_data(h, &pathname)){
|
|
if (api_http_data_err(h, req, 404) < 0) /* not found */
|
|
goto done;
|
|
goto ok;
|
|
}
|
|
/* 2. operation GET or HEAD */
|
|
request_method = restconf_param_get(h, "REQUEST_METHOD");
|
|
if (strcmp(request_method, "GET") == 0){
|
|
}
|
|
else if (strcmp(request_method, "HEAD") == 0){
|
|
head = 1;
|
|
}
|
|
else if (strcmp(request_method, "OPTIONS") == 0){
|
|
options = 1;
|
|
}
|
|
else {
|
|
if (api_http_data_err(h, req, 405) < 0) /* method not allowed */
|
|
goto done;
|
|
goto ok;
|
|
}
|
|
/* 3. query parameters not accepted */
|
|
if (qvec != NULL){
|
|
if (api_http_data_err(h, req, 400) < 0) /* bad request */
|
|
goto done;
|
|
goto ok;
|
|
}
|
|
/* 4. indata should be NULL (no write operations) */
|
|
if ((indata = restconf_get_indata(req)) == NULL) {
|
|
clicon_err(OE_RESTCONF, ENOENT, "Unexpected no input cbuf");
|
|
goto done;
|
|
}
|
|
if (cbuf_len(indata)){
|
|
if (api_http_data_err(h, req, 400) < 0) /* bad request */
|
|
goto done;
|
|
goto ok;
|
|
}
|
|
/* 5. Accepted media_out: should check text/html, JavaScript, image, and css
|
|
*/
|
|
if ((media_str = restconf_param_get(h, "HTTP_ACCEPT")) == NULL){
|
|
}
|
|
else if (strcmp(media_str, "*/*") != 0 &&
|
|
strcmp(media_str, "text/html") != 0){
|
|
#ifdef NOTYET
|
|
clicon_log(LOG_NOTICE, "%s: media error %s", __FUNCTION__, media_str);
|
|
goto done;
|
|
#endif
|
|
}
|
|
/* 6. Authenticate
|
|
* Note, error handling may need change since it is restconf based
|
|
*/
|
|
if ((ret = restconf_authentication_cb(h, req, 1, 0 /*media_out */)) < 0)
|
|
goto done;
|
|
if (ret == 0)
|
|
goto ok;
|
|
if (options){
|
|
if (restconf_reply_header(req, "Allow", "OPTIONS,HEAD,GET") < 0)
|
|
goto done;
|
|
if (restconf_reply_send(req, 200, NULL, 0) < 0)
|
|
goto done;
|
|
}
|
|
else if (api_http_data_file(h, req, pathname, head) < 0)
|
|
goto done;
|
|
ok:
|
|
retval = 0;
|
|
done:
|
|
clicon_debug(1, "%s %d", __FUNCTION__, retval);
|
|
return retval;
|
|
}
|