/*
 *
  ***** 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 ((ret = fread(buf, fsize, 1, f)) < 0){
        clicon_err(OE_UNIX, errno, "fread");
        goto done;
    }
    sz = (size_t)ret;
    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;
}
