495 lines
15 KiB
C
495 lines
15 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[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)
|
|
{
|
|
int retval = 0;
|
|
char *path = NULL;
|
|
char *http_data_path;
|
|
|
|
if (restconf_http_data_get(h) == 0)
|
|
goto done;
|
|
if ((path = restconf_uripath(h)) == NULL)
|
|
goto done;
|
|
if ((http_data_path = clicon_option_str(h, "CLICON_HTTP_DATA_PATH")) == NULL)
|
|
goto done;
|
|
if (strlen(path) < strlen(http_data_path))
|
|
goto done;
|
|
if (path[0] != '/')
|
|
goto done;
|
|
if (strncmp(path, http_data_path, strlen(http_data_path)) != 0)
|
|
goto done;
|
|
retval = 1;
|
|
done:
|
|
if (path)
|
|
free(path);
|
|
return retval;
|
|
}
|
|
|
|
/*! 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;
|
|
}
|
|
|
|
/*! Check validity of path, may only be regular dir or file
|
|
* No .., soft link, ~, etc
|
|
* @param[in] h Clicon handle
|
|
* @param[in] req Generic Www handle (can be part of clixon handle)
|
|
* @param[in] prefix Prefix of path0, where to start file check
|
|
* @param[in,out] cbpath Filepath as cbuf, internal redirection may change it
|
|
* @param[out] fp Open file, if retval = 1
|
|
* @param[out] fsz Size of file, if retval = 1
|
|
* @retval -1 Error
|
|
* @retval 0 Invalid
|
|
* @retval 1 OK, fp,fsz set
|
|
*/
|
|
static int
|
|
http_data_check_file_path(clicon_handle h,
|
|
void *req,
|
|
char *prefix,
|
|
cbuf *cbpath,
|
|
FILE **fp,
|
|
off_t *fsz)
|
|
{
|
|
int retval = -1;
|
|
struct stat fstat;
|
|
char *p;
|
|
int i;
|
|
int code = 0;
|
|
FILE *f;
|
|
|
|
if (prefix == NULL || cbpath == NULL || fp == NULL){
|
|
clicon_err(OE_UNIX, EINVAL, "prefix, cbpath0 or fp is NULL");
|
|
goto done;
|
|
}
|
|
p = cbuf_get(cbpath);
|
|
clicon_debug(1, "%s %s", __FUNCTION__, p);
|
|
if (strncmp(prefix, p, strlen(prefix)) != 0){
|
|
clicon_err(OE_UNIX, EINVAL, "prefix is not prefix of cbpath");
|
|
goto done;
|
|
}
|
|
for (i=strlen(prefix); i<strlen(p); i++){
|
|
if (p[i] == '/'){ /* Check valid dir */
|
|
p[i] = '\0';
|
|
/* Ensure not soft link */
|
|
if (lstat(p, &fstat) < 0){
|
|
clicon_debug(1, "%s Error lstat(%s):%s", __FUNCTION__, p, strerror(errno));
|
|
code = 404;
|
|
goto invalid;
|
|
}
|
|
if (!S_ISDIR(fstat.st_mode)){
|
|
clicon_debug(1, "%s Error lstat(%s): Not dir", __FUNCTION__, p);
|
|
code = 403;
|
|
goto invalid;
|
|
}
|
|
p[i] = '/';
|
|
}
|
|
else if (p[i] == '~'){
|
|
clicon_debug(1, "%s Error lstat(%s): ~ not allowed in file path", __FUNCTION__, p);
|
|
code = 403;
|
|
goto invalid;
|
|
}
|
|
else if (p[i] == '.' && i>strlen(prefix) && p[i-1] == '.'){
|
|
clicon_debug(1, "%s Error lstat(%s): .. not allowed in file path", __FUNCTION__, p);
|
|
code = 403;
|
|
goto invalid;
|
|
}
|
|
}
|
|
/* Resulting file (ensure not soft link) */
|
|
if (lstat(p, &fstat) < 0){
|
|
clicon_debug(1, "%s Error lstat(%s):%s", __FUNCTION__, p, strerror(errno));
|
|
code = 404;
|
|
goto invalid;
|
|
}
|
|
#ifdef HTTP_DATA_INTERNAL_REDIRECT
|
|
/* If dir try redirect, not cbpath is extended */
|
|
if (S_ISDIR(fstat.st_mode)){
|
|
cprintf(cbpath, "/%s", HTTP_DATA_INTERNAL_REDIRECT);
|
|
p = cbuf_get(cbpath);
|
|
clicon_debug(1, "%s internal redirect: %s", __FUNCTION__, p);
|
|
if (lstat(p, &fstat) < 0){
|
|
clicon_debug(1, "%s Error lstat(%s):%s", __FUNCTION__, p, strerror(errno));
|
|
code = 404;
|
|
goto invalid;
|
|
}
|
|
}
|
|
#endif
|
|
if (!S_ISREG(fstat.st_mode)){
|
|
clicon_debug(1, "%s Error lstat(%s): Not regular file", __FUNCTION__, p);
|
|
code = 403;
|
|
goto invalid;
|
|
}
|
|
*fsz = fstat.st_size;
|
|
if ((f = fopen(p, "rb")) == NULL){
|
|
clicon_debug(1, "%s Error fopen(%s) %s", __FUNCTION__, p, strerror(errno));
|
|
code = 403;
|
|
goto invalid;
|
|
}
|
|
*fp = f;
|
|
retval = 1; /* OK */
|
|
done:
|
|
return retval;
|
|
invalid:
|
|
if (api_http_data_err(h, req, code) < 0)
|
|
goto done;
|
|
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)
|
|
* @param[in] pathname With stripped prefix (eg /data), ultimately a filename
|
|
* @param[in] head HEAD not GET
|
|
* @note: primitive file handling, just check if file exists and read it all
|
|
* XXX 1: Buffer copying once too many, see #if 0 below
|
|
*/
|
|
static int
|
|
api_http_data_file(clicon_handle h,
|
|
void *req,
|
|
char *pathname,
|
|
int head)
|
|
{
|
|
int retval = -1;
|
|
cbuf *cbfile = NULL;
|
|
char *filename = NULL;
|
|
cbuf *cbdata = NULL;
|
|
FILE *f = NULL;
|
|
off_t fsz = 0;
|
|
long fsize;
|
|
char *www_data_root = NULL;
|
|
char *suffix;
|
|
char *media;
|
|
int ret;
|
|
char *buf = NULL;
|
|
size_t sz;
|
|
|
|
clicon_debug(1, "%s", __FUNCTION__);
|
|
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;
|
|
}
|
|
|
|
cprintf(cbfile, "%s", www_data_root);
|
|
if (pathname){
|
|
if (strlen(pathname) && pathname[0] != '/'){
|
|
clicon_debug(1, "%s Error fopen(%s) pathname not prefixed with /",
|
|
__FUNCTION__, pathname);
|
|
if (api_http_data_err(h, req, 404) < 0)
|
|
goto done;
|
|
goto ok;
|
|
}
|
|
cprintf(cbfile, "%s", pathname); /* Assume pathname starts with '/' */
|
|
}
|
|
if ((ret = http_data_check_file_path(h, req, www_data_root, cbfile, &f, &fsz)) < 0)
|
|
goto done;
|
|
if (ret == 0) /* Invalid, return code set */
|
|
goto ok;
|
|
filename = cbuf_get(cbfile);
|
|
/* Find media from file suffix, note there may have been internal indirection */
|
|
if ((suffix = rindex(filename, '.')) == NULL){
|
|
media = "application/octet-stream";
|
|
}
|
|
else {
|
|
suffix++;
|
|
if ((media = clicon_str2str(mime_map, suffix)) == NULL)
|
|
media = "application/octet-stream";
|
|
}
|
|
/* 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);
|
|
/* Extra sanity check, had some problems with wrong file types */
|
|
if (fsz != fsize){
|
|
clicon_debug(1, "%s Error file %s size mismatch sz:%zu vs %li",
|
|
__FUNCTION__, filename, (size_t)fsz, fsize);
|
|
if (api_http_data_err(h, req, 500) < 0) /* Internal error? */
|
|
goto done;
|
|
goto ok;
|
|
}
|
|
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;
|
|
}
|
|
/* 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 ((buf = malloc(fsize)) == NULL){
|
|
clicon_err(OE_UNIX, errno, "malloc");
|
|
goto done;
|
|
}
|
|
if ((sz = fread(buf, fsize, 1, f)) < 0){
|
|
clicon_err(OE_UNIX, errno, "fread");
|
|
goto done;
|
|
}
|
|
if (sz != 1){
|
|
clicon_debug(1, "%s Error fread(%s) sz:%zu", __FUNCTION__, filename, sz);
|
|
if (api_http_data_err(h, req, 500) < 0) /* Internal error? */
|
|
goto done;
|
|
goto ok;
|
|
}
|
|
if (cbuf_append_buf(cbdata, buf, fsize) < 0){
|
|
clicon_err(OE_UNIX, errno, "cbuf_append_str");
|
|
goto done;
|
|
}
|
|
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 */
|
|
clicon_debug(1, "%s Read %s OK", __FUNCTION__, filename);
|
|
ok:
|
|
retval = 0;
|
|
done:
|
|
if (buf)
|
|
free(buf);
|
|
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 *path = 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)){
|
|
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){
|
|
}
|
|
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, path, head) < 0)
|
|
goto done;
|
|
ok:
|
|
retval = 0;
|
|
done:
|
|
if (path)
|
|
free(path);
|
|
clicon_debug(1, "%s %d", __FUNCTION__, retval);
|
|
return retval;
|
|
}
|