qse/lib/http/httpd-dir.c

690 lines
19 KiB
C

/*
* $Id$
*
Copyright (c) 2006-2019 Chung, Hyung-Hwan. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "httpd.h"
#include "../cmn/mem-prv.h"
#include <qse/cmn/str.h>
#include <qse/cmn/fmt.h>
typedef struct task_dir_t task_dir_t;
struct task_dir_t
{
qse_mcstr_t path;
qse_mcstr_t qpath;
qse_mcstr_t head;
qse_mcstr_t foot;
qse_http_version_t version;
int keepalive;
int method;
qse_httpd_hnd_t handle;
};
typedef struct task_dseg_t task_dseg_t;
struct task_dseg_t
{
qse_http_version_t version;
int keepalive;
int chunked;
qse_mcstr_t path;
qse_mcstr_t qpath;
qse_mcstr_t head;
qse_mcstr_t foot;
qse_httpd_hnd_t handle;
qse_httpd_dirent_t dent;
#define HEADER_ADDED (1 << 0)
#define FOOTER_ADDED (1 << 1)
#define FOOTER_PENDING (1 << 2)
#define DIRENT_PENDING (1 << 3)
#define DIRENT_NOMORE (1 << 4)
int state;
qse_size_t tcount; /* total directory entries */
qse_size_t dcount; /* the number of items in the buffer */
qse_mchar_t buf[4096*2]; /* is this large enough? */
int bufpos;
int buflen;
int bufrem;
qse_size_t chunklen;
};
static int task_init_dseg (
qse_httpd_t* httpd, qse_httpd_client_t* client, qse_httpd_task_t* task)
{
task_dseg_t* xtn = qse_httpd_gettaskxtn (httpd, task);
task_dseg_t* arg = (task_dseg_t*)task->ctx;
QSE_MEMCPY (xtn, arg, QSE_SIZEOF(*xtn));
xtn->path.ptr = (qse_mchar_t*)(xtn + 1);
qse_mbscpy ((qse_mchar_t*)xtn->path.ptr, arg->path.ptr);
xtn->qpath.ptr = xtn->path.ptr + xtn->path.len + 1;
qse_mbscpy ((qse_mchar_t*)xtn->qpath.ptr, arg->qpath.ptr);
xtn->head.ptr = xtn->qpath.ptr + xtn->qpath.len + 1;
qse_mbscpy ((qse_mchar_t*)xtn->head.ptr, arg->head.ptr);
xtn->foot.ptr = xtn->head.ptr + xtn->head.len + 1;
qse_mbscpy ((qse_mchar_t*)xtn->foot.ptr, arg->foot.ptr);
task->ctx = xtn;
return 0;
}
static void task_fini_dseg (
qse_httpd_t* httpd, qse_httpd_client_t* client, qse_httpd_task_t* task)
{
task_dseg_t* ctx = (task_dseg_t*)task->ctx;
httpd->opt.scb.dir.close (httpd, ctx->handle);
}
#define SIZE_CHLEN 4 /* the space size to hold the hexadecimal chunk length */
#define SIZE_CHLENCRLF 2 /* the space size to hold CRLF after the chunk length */
#define SIZE_CHENDCRLF 2 /* the sapce size to hold CRLF after the chunk data */
static QSE_INLINE void close_chunk_data (task_dseg_t* ctx, qse_size_t len)
{
ctx->chunklen = len;
/* CHENDCRLF - there is always space for these two.
* reserved with SIZE_CHENDCRLF */
ctx->buf[ctx->buflen++] = QSE_MT('\r');
ctx->buf[ctx->buflen++] = QSE_MT('\n');
}
static void fill_chunk_length (task_dseg_t* ctx)
{
int x;
/* right alignment with space padding on the left */
x = qse_fmtuintmaxtombs (
ctx->buf, (SIZE_CHLEN + SIZE_CHLENCRLF) - 1,
(ctx->chunklen - (SIZE_CHLEN + SIZE_CHLENCRLF)), /* value to convert */
16 | QSE_FMTUINTMAXTOMBS_UPPERCASE, /* uppercase hexadecimal letters */
-1, /* no particular precision - want space filled if less than 4 digits */
QSE_MT(' '), /* fill with space */
QSE_NULL
);
/* i don't check the buffer size error because i've secured the
* suffient space for the chunk length at the beginning of the buffer */
/* CHLENCRLF */
ctx->buf[x] = QSE_MT('\r');
ctx->buf[x+1] = QSE_MT('\n');
/* skip leading space padding */
QSE_ASSERT (ctx->bufpos == 0);
while (ctx->buf[ctx->bufpos] == QSE_MT(' ')) ctx->bufpos++;
}
static qse_size_t format_dirent (
qse_httpd_t* httpd,
qse_httpd_client_t* client,
const qse_httpd_dirent_t* dirent,
qse_mchar_t* buf, int bufsz)
{
qse_size_t x;
qse_mchar_t* encname;
qse_mchar_t* escname;
qse_btime_t bt;
qse_mchar_t tmbuf[32];
qse_mchar_t fszbuf[64];
/* TODO: better buffer management in case there are
* a lot of file names to escape. */
/* perform percent-encoding for the anchor */
encname = qse_perenchttpstrdup (0, dirent->name, qse_httpd_getmmgr(httpd));
if (encname == QSE_NULL)
{
httpd->errnum = QSE_HTTPD_ENOMEM;
return -1;
}
/* perform html escaping for the text */
escname = qse_httpd_escapehtml (httpd, dirent->name);
if (escname == QSE_NULL)
{
if (encname != dirent->name) QSE_MMGR_FREE (qse_httpd_getmmgr(httpd), encname);
return -1;
}
qse_localtime (&dirent->stat.mtime, &bt);
qse_mbsxfmt (tmbuf, QSE_COUNTOF(tmbuf),
QSE_MT("%04d-%02d-%02d %02d:%02d:%02d"),
bt.year + QSE_BTIME_YEAR_BASE, bt.mon + 1, bt.mday,
bt.hour, bt.min, bt.sec);
if (dirent->stat.isdir)
{
fszbuf[0] = QSE_MT('\0');
}
else
{
qse_fmtuintmaxtombs (
fszbuf, QSE_COUNTOF(fszbuf),
dirent->stat.size, 10, -1, QSE_MT('\0'), QSE_NULL
);
}
x = qse_mbsxfmts (
buf, bufsz,
QSE_MT("<tr><td class='name'><a href='%s%s' class='%s'>%s%s</a></td><td class='time'>%s</td><td class='size'>%s</td></tr>\n"),
encname,
(dirent->stat.isdir? QSE_MT("/"): QSE_MT("")),
(dirent->stat.isdir? QSE_MT("dir"): QSE_MT("file")),
escname,
(dirent->stat.isdir? QSE_MT("/"): QSE_MT("")),
tmbuf, fszbuf
);
if (escname != dirent->name) qse_httpd_freemem (httpd, escname);
if (encname != dirent->name) QSE_MMGR_FREE (qse_httpd_getmmgr(httpd), encname);
if (x == (qse_size_t)-1) httpd->errnum = QSE_HTTPD_ENOBUF;
return x;
}
static int add_footer (qse_httpd_t* httpd, qse_httpd_client_t* client, task_dseg_t* ctx)
{
qse_size_t x;
int rem;
rem = ctx->chunked? (ctx->bufrem - 5): ctx->bufrem;
if (rem < 1)
{
httpd->errnum = QSE_HTTPD_ENOBUF;
return -1;
}
x = qse_mbsxfmts (&ctx->buf[ctx->buflen], rem, QSE_MT("</table></div>\n<div class='footer'>%s</div>\n</body></html>"), ctx->foot.ptr);
if (x == (qse_size_t)-1)
{
httpd->errnum = QSE_HTTPD_ENOBUF;
return -1;
}
QSE_ASSERT (x < rem);
ctx->buflen += x;
ctx->bufrem -= x;
if (ctx->chunked)
{
qse_mbscpy (&ctx->buf[ctx->buflen], QSE_MT("\r\n0\r\n"));
ctx->buflen += 5;
ctx->bufrem -= 5;
/* -5 for \r\n0\r\n added above */
if (ctx->chunked) close_chunk_data (ctx, ctx->buflen - 5);
}
return 0;
}
static int task_main_dseg (
qse_httpd_t* httpd, qse_httpd_client_t* client, qse_httpd_task_t* task)
{
task_dseg_t* ctx = (task_dseg_t*)task->ctx;
qse_ssize_t n;
qse_size_t x;
if (ctx->bufpos < ctx->buflen)
{
/* buffer contents not fully sent yet */
goto send_dirlist;
}
/* the buffer size is fixed to QSE_COUNTOF(ctx->buf).
*
* the number of digits need to hold the the size converted to
* a hexadecimal notation is roughly (log16(QSE_COUNTOF(ctx->buf) + 1).
* it should be safter to use ceil(log16(QSE_COUNTOF(ctx->buf)) + 1
* for precision issues.
*
* 16**X = QSE_COUNTOF(ctx->buf).
* X = log16(QSE_COUNTOF(ctx->buf).
* X + 1 is a required number of digits.
*
* Since log16 is not provided, we should use a natural log function
* whose base is the constant e (2.718).
*
* log16(n) = log(n) / log(16)
*
* The final fomula is here.
*
* X = ceil((log(QSE_COUNTOF(ctx->buf)) / log(16))) + 1;
*
* However, i won't use these floating-point opertions.
* instead i'll reserve a hardcoded size. so when you change
* the size of the buffer arrray, you should check this size.
*/
/* initialize buffer */
ctx->dcount = 0; /* reset the entry counter */
ctx->bufpos = 0;
if (ctx->chunked)
{
/* reserve space to fill with the chunk length
* 4 for the actual chunk length and +2 for \r\n */
ctx->buflen = SIZE_CHLEN + SIZE_CHLENCRLF;
/* free space remaing in the buffer for the chunk data */
ctx->bufrem = QSE_COUNTOF(ctx->buf) - ctx->buflen - SIZE_CHENDCRLF;
}
else
{
ctx->buflen = 0;
ctx->bufrem = QSE_COUNTOF(ctx->buf);
}
if (ctx->state & FOOTER_PENDING)
{
/* only footers yet to be sent */
if (add_footer (httpd, client, ctx) <= -1)
{
/* return an error if the buffer is too small to hold the
* trailing footer. you need to increase the buffer size */
return -1;
}
ctx->state &= ~FOOTER_PENDING;
ctx->state |= FOOTER_ADDED;
if (ctx->chunked) fill_chunk_length (ctx);
goto send_dirlist;
}
if (!(ctx->state & HEADER_ADDED))
{
/* compose the header since this is the first time. */
int is_root;
qse_mchar_t* qpath_esc;
is_root = (qse_mbscmp (ctx->qpath.ptr, QSE_MT("/")) == 0);
qpath_esc = qse_httpd_escapehtml (httpd, ctx->qpath.ptr);
if (qpath_esc == QSE_NULL) return -1;
/* TODO: page encoding?? utf-8??? or derive name from cmgr or current locale??? */
/* TODO: SUPPORT character set. DON't HARD_CODE it to UTF8 */
x = qse_mbsxfmts (&ctx->buf[ctx->buflen], ctx->bufrem,
QSE_MT("<html><meta http-equiv=\"Content-Type\" content=\"text/html; charset='UTF-8'\">\n<head>%s</head>\n<body>\n<div class='header'>%s</div>\n<div class='body'><table>%s"),
ctx->head.ptr, qpath_esc,
(is_root? QSE_MT(""): QSE_MT("<tr><td class='name'><a href='../' class='dir'>..</a></td><td class='time'></td><td class='size'></td></tr>\n"))
);
if (qpath_esc != ctx->qpath.ptr) qse_httpd_freemem (httpd, qpath_esc);
if (x == (qse_size_t)-1)
{
/* return an error if the buffer is too small to
* hold the header(httpd->errnum == QSE_HTTPD_ENOBUF).
* i need to increase the buffer size. or i have make
* the buffer dynamic. */
httpd->errnum = QSE_HTTPD_ENOBUF;
return -1;
}
QSE_ASSERT (x < ctx->bufrem);
ctx->buflen += x;
ctx->bufrem -= x;
ctx->state |= HEADER_ADDED;
ctx->dcount++;
}
if (ctx->state & DIRENT_PENDING)
{
ctx->state &= ~DIRENT_PENDING;
}
else
{
if (httpd->opt.scb.dir.read (httpd, ctx->handle, &ctx->dent) <= 0)
ctx->state |= DIRENT_NOMORE;
}
do
{
if (ctx->state & DIRENT_NOMORE)
{
/* no more directory entry */
if (add_footer (httpd, client, ctx) <= -1)
{
/* failed to add the footer part */
if (ctx->chunked)
{
close_chunk_data (ctx, ctx->buflen);
fill_chunk_length (ctx);
}
ctx->state |= FOOTER_PENDING;
}
else if (ctx->chunked) fill_chunk_length (ctx);
break;
}
if (qse_mbscmp (ctx->dent.name, QSE_MT(".")) != 0 &&
qse_mbscmp (ctx->dent.name, QSE_MT("..")) != 0)
{
x = format_dirent (httpd, client, &ctx->dent, &ctx->buf[ctx->buflen], ctx->bufrem);
if (x == (qse_size_t)-1)
{
/* buffer not large enough to hold this entry */
if (ctx->dcount <= 0)
{
/* neither directory entry nor the header
* has been added to the buffer so far. and
* this attempt has failed. the buffer size must
* be too small. you must increase it */
httpd->errnum = QSE_HTTPD_EINTERN;
return -1;
}
if (ctx->chunked)
{
close_chunk_data (ctx, ctx->buflen);
fill_chunk_length (ctx);
}
ctx->state |= DIRENT_PENDING;
break;
}
else
{
QSE_ASSERT (x < ctx->bufrem);
ctx->buflen += x;
ctx->bufrem -= x;
ctx->dcount++;
ctx->tcount++;
}
}
if (httpd->opt.scb.dir.read (httpd, ctx->handle, &ctx->dent) <= 0)
ctx->state |= DIRENT_NOMORE;
}
while (1);
send_dirlist:
httpd->errnum = QSE_HTTPD_ENOERR;
n = httpd->opt.scb.client.send (
httpd, client, &ctx->buf[ctx->bufpos], ctx->buflen - ctx->bufpos);
if (n <= -1) return -1;
/* NOTE if (n == 0), it will enter an infinite loop */
ctx->bufpos += n;
return (ctx->bufpos < ctx->buflen || (ctx->state & FOOTER_PENDING) || !(ctx->state & DIRENT_NOMORE))? 1: 0;
}
static qse_httpd_task_t* entask_directory_segment (
qse_httpd_t* httpd, qse_httpd_client_t* client,
qse_httpd_task_t* pred, qse_httpd_hnd_t handle, task_dir_t* dir)
{
qse_httpd_task_t task;
task_dseg_t data;
QSE_MEMSET (&data, 0, QSE_SIZEOF(data));
data.handle = handle;
data.version = dir->version;
data.keepalive = dir->keepalive;
data.chunked = dir->keepalive;
data.path = dir->path;
data.qpath = dir->qpath;
data.head = dir->head;
data.foot = dir->foot;
QSE_MEMSET (&task, 0, QSE_SIZEOF(task));
task.init = task_init_dseg;
task.main = task_main_dseg;
task.fini = task_fini_dseg;
task.ctx = &data;
return qse_httpd_entask (httpd, client, pred, &task, QSE_SIZEOF(data) + data.path.len + 1 + data.qpath.len + 1 + data.head.len + 1 + data.foot.len + 1);
}
/*------------------------------------------------------------------------*/
static int task_init_getdir (
qse_httpd_t* httpd, qse_httpd_client_t* client, qse_httpd_task_t* task)
{
task_dir_t* xtn = qse_httpd_gettaskxtn (httpd, task);
task_dir_t* arg = (task_dir_t*)task->ctx;
/* deep-copy the context data to the extension area */
QSE_MEMCPY (xtn, arg, QSE_SIZEOF(*xtn));
xtn->path.ptr = (qse_mchar_t*)(xtn + 1);
qse_mbscpy ((qse_mchar_t*)xtn->path.ptr, arg->path.ptr);
xtn->qpath.ptr = xtn->path.ptr + xtn->path.len + 1;
qse_mbscpy ((qse_mchar_t*)xtn->qpath.ptr, arg->qpath.ptr);
xtn->head.ptr = xtn->qpath.ptr + xtn->qpath.len + 1;
qse_mbscpy ((qse_mchar_t*)xtn->head.ptr, arg->head.ptr);
xtn->foot.ptr = xtn->head.ptr + xtn->head.len + 1;
qse_mbscpy ((qse_mchar_t*)xtn->foot.ptr, arg->foot.ptr);
/* switch the context to the extension area */
task->ctx = xtn;
return 0;
}
static QSE_INLINE int task_main_getdir (
qse_httpd_t* httpd, qse_httpd_client_t* client, qse_httpd_task_t* task)
{
task_dir_t* dir;
qse_httpd_task_t* x;
dir = (task_dir_t*)task->ctx;
x = task;
/* arrange to return the header part first */
if (dir->method == QSE_HTTP_HEAD)
{
x = qse_httpd_entaskformat (
httpd, client, x,
QSE_MT("HTTP/%d.%d 200 OK\r\nServer: %s\r\nDate: %s\r\nConnection: %s\r\nContent-Type: text/html\r\nContent-Length: 0\r\n\r\n"),
dir->version.major, dir->version.minor,
qse_httpd_getname (httpd),
qse_httpd_fmtgmtimetobb (httpd, QSE_NULL, 0),
(dir->keepalive? QSE_MT("keep-alive"): QSE_MT("close"))
);
httpd->opt.scb.dir.close (httpd, dir->handle);
return (x == QSE_NULL)? -1: 0;
}
else
{
int keepalive_ignored = 0;
if (dir->keepalive && qse_comparehttpversions (&dir->version, &qse_http_v11) < 0)
{
/* this task does chunking when keepalive is set.
* chunking is not supported for http 1.0 or earlier.
* force switch to the close mode */
dir->keepalive = 0;
keepalive_ignored = 1;
}
x = qse_httpd_entaskformat (
httpd, client, x,
QSE_MT("HTTP/%d.%d 200 OK\r\nServer: %s\r\nDate: %s\r\nConnection: %s\r\nContent-Type: text/html\r\n%s\r\n"),
dir->version.major, dir->version.minor,
qse_httpd_getname (httpd),
qse_httpd_fmtgmtimetobb (httpd, QSE_NULL, 0),
(dir->keepalive? QSE_MT("keep-alive"): QSE_MT("close")),
(dir->keepalive? QSE_MT("Transfer-Encoding: chunked\r\n"): QSE_MT(""))
);
if (x)
{
#if 0
if (httpd->opt.trait & QSE_HTTPD_LOGACC)
{
qse_httpd_reqsum_t reqsum;
acc.remote = remote;
acc.qpath = qpath;
acc.status = 200;
acc.version = ...;
acc.method = ...;
httpd->opt.rcb.logacc (httpd, &reqsum);
}
#endif
/* arrange to send the actual directory contents */
x = entask_directory_segment (httpd, client, x, dir->handle, dir);
if (keepalive_ignored && x)
x = qse_httpd_entaskdisconnect (httpd, client, x);
if (x) return 0;
}
httpd->opt.scb.dir.close (httpd, dir->handle);
return -1;
}
}
qse_httpd_task_t* qse_httpd_entaskdir (
qse_httpd_t* httpd,
qse_httpd_client_t* client,
qse_httpd_task_t* pred,
qse_httpd_rsrc_dir_t* dir,
qse_htre_t* req)
{
int method;
method = qse_htre_getqmethodtype(req);
/* i don't need contents for directories */
qse_htre_discardcontent (req);
switch (method)
{
case QSE_HTTP_OPTIONS:
return qse_httpd_entaskallow (httpd, client, pred,
QSE_MT("OPTIONS,GET,HEAD,POST,PUT,DELETE"), req);
case QSE_HTTP_HEAD:
case QSE_HTTP_GET:
case QSE_HTTP_POST:
{
task_dir_t data;
QSE_MEMSET (&data, 0, QSE_SIZEOF(data));
/* check if the directory stream can be opened before
* creating an actual task. */
if (httpd->opt.scb.dir.open (httpd, dir->path, &data.handle) <= -1)
{
/* arrange a status code to return */
int status;
status = (httpd->errnum == QSE_HTTPD_ENOENT)? 404:
(httpd->errnum == QSE_HTTPD_EACCES)? 403: 500;
return qse_httpd_entaskerror (httpd, client, pred, status, req);
}
else
{
/* create a directory listing task */
qse_httpd_task_t task, * x;
data.path.ptr = (qse_mchar_t*)dir->path;
data.path.len = qse_mbslen(data.path.ptr);
data.qpath.ptr = qse_htre_getqpath(req);
data.qpath.len = qse_mbslen(data.qpath.ptr);
data.head.ptr = dir->head? (qse_mchar_t*)dir->head: QSE_MT("<style type='text/css'>body { background-color:#d0e4fe; font-size: 0.9em; } div.header { font-weight: bold; margin-bottom: 5px; } div.footer { border-top: 1px solid #99AABB; text-align: right; } table { font-size: 0.9em; } td { white-space: nowrap; } td.size { text-align: right; }</style>");
data.head.len = qse_mbslen(data.head.ptr);
data.foot.ptr = dir->foot? dir->foot: qse_httpd_getname(httpd);
data.foot.len = qse_mbslen(data.foot.ptr);
data.version = *qse_htre_getversion(req);
data.keepalive = req->flags & QSE_HTRE_ATTR_KEEPALIVE;
data.method = method;
QSE_MEMSET (&task, 0, QSE_SIZEOF(task));
task.init = task_init_getdir;
task.main = task_main_getdir;
task.ctx = &data;
x = qse_httpd_entask (httpd, client, pred, &task,
QSE_SIZEOF(task_dir_t) + data.path.len + 1 + data.qpath.len + 1 +
data.head.len + 1 + data.foot.len + 1);
if (x == QSE_NULL)
{
httpd->opt.scb.dir.close (httpd, data.handle);
}
return x;
}
}
case QSE_HTTP_PUT:
{
int status = 201; /* 201 Created */
if (httpd->opt.scb.dir.make (httpd, dir->path) <= -1)
{
if (httpd->errnum == QSE_HTTPD_EEXIST)
{
/* an entry with the same name exists.
* if it is a directory, it's considered ok.
* if not, send '403 forbidden' indicating you can't
* change a file to a directory */
qse_httpd_stat_t st;
status = (httpd->opt.scb.dir.stat (httpd, dir->path, &st) <= -1)? 403: 204;
}
else
{
status = (httpd->errnum == QSE_HTTPD_ENOENT)? 404:
(httpd->errnum == QSE_HTTPD_EACCES)? 403: 500;
}
}
return qse_httpd_entaskerror (httpd, client, pred, status, req);
}
case QSE_HTTP_DELETE:
{
int status = 200;
if (httpd->opt.scb.dir.purge (httpd, dir->path) <= -1)
{
status = (httpd->errnum == QSE_HTTPD_ENOENT)? 404:
(httpd->errnum == QSE_HTTPD_EACCES)? 403: 500;
}
return qse_httpd_entaskerror (httpd, client, pred, status, req);
}
default:
/* Method not allowed */
return qse_httpd_entaskerror (httpd, client, pred, 405, req);
}
}