mirror of
https://gitee.com/jiuyilian/embedded-framework.git
synced 2025-01-06 10:16:51 -05:00
3547 lines
96 KiB
C
3547 lines
96 KiB
C
/*
|
|
http.c -- GoAhead HTTP engine
|
|
|
|
This module implements an embedded HTTP/1.1 web server. It supports
|
|
loadable URL handlers that define the nature of URL processing performed.
|
|
|
|
Copyright (c) All Rights Reserved. See details at the end of the file.
|
|
*/
|
|
|
|
/********************************* Includes ***********************************/
|
|
|
|
#include "goahead.h"
|
|
|
|
/********************************* Defines ************************************/
|
|
|
|
#define WEBS_TIMEOUT (ME_GOAHEAD_LIMIT_TIMEOUT * 1000)
|
|
#define PARSE_TIMEOUT (ME_GOAHEAD_LIMIT_PARSE_TIMEOUT * 1000)
|
|
#define CHUNK_LOW 128 /* Low water mark for chunking */
|
|
|
|
#define TOKEN_HEADER_KEY 0x1 /* Validate token as a header key */
|
|
#define TOKEN_HEADER_VALUE 0x2 /* Validate token as a header value */
|
|
#define TOKEN_URI_VALUE 0x4 /* Validate token as a URI value */
|
|
#define TOKEN_NUMBER 0x8 /* Validate token as a number */
|
|
#define TOKEN_WORD 0x10 /* Validate token as single word with no spaces */
|
|
#define TOKEN_LINE 0x20 /* Validate token as line with no newlines */
|
|
|
|
/************************************ Locals **********************************/
|
|
|
|
static int websBackground; /* Run as a daemon */
|
|
static int websDebug; /* Run in debug mode and defeat timeouts */
|
|
static int defaultHttpPort; /* Default port number for http */
|
|
static int defaultSslPort; /* Default port number for https */
|
|
static int listens[WEBS_MAX_LISTEN]; /* Listen endpoints */;
|
|
static int listenMax; /* Max entry in listens */
|
|
static Webs **webs; /* Open connection list head */
|
|
static WebsHash websMime; /* Set of mime types */
|
|
static int websMax; /* List size */
|
|
static char websHost[ME_MAX_IP]; /* Host name for the server */
|
|
static char websIpAddr[ME_MAX_IP]; /* IP address for the server */
|
|
static char *websHostUrl = NULL; /* URL to access server */
|
|
static char *websIpAddrUrl = NULL; /* URL to access server */
|
|
|
|
#define WEBS_ENCODE_HTML 0x1 /* Bit setting in charMatch[] */
|
|
#define WEBS_ENCODE_URI 0x4 /* Encode URI characters */
|
|
|
|
/*
|
|
Character escape/descape matching codes. Generated by mprEncodeGenerate in appweb.
|
|
*/
|
|
static uchar charMatch[256] = {
|
|
0x00,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x7e,0x3c,0x3c,0x7c,0x3c,0x3c,
|
|
0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x7c,0x3c,0x3c,0x3c,0x3c,0x3c,
|
|
0x3c,0x00,0x7f,0x28,0x2a,0x3c,0x2b,0x43,0x02,0x02,0x02,0x28,0x28,0x00,0x00,0x28,
|
|
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x28,0x2a,0x3f,0x28,0x3f,0x2a,
|
|
0x28,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
|
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3a,0x7e,0x3a,0x3e,0x00,
|
|
0x3e,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
|
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3e,0x3e,0x3e,0x02,0x3c,
|
|
0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,
|
|
0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,
|
|
0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,
|
|
0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,
|
|
0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,
|
|
0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,
|
|
0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,
|
|
0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c,0x3c
|
|
};
|
|
|
|
/*
|
|
Add entries to the MimeList as required for your content
|
|
*/
|
|
static WebsMime websMimeList[] = {
|
|
{ "application/java", ".class" },
|
|
{ "application/java", ".jar" },
|
|
{ "text/html", ".asp" },
|
|
{ "text/html", ".htm" },
|
|
{ "text/html", ".html" },
|
|
{ "text/xml", ".xml" },
|
|
{ "image/gif", ".gif" },
|
|
{ "image/jpeg", ".jpg" },
|
|
{ "image/png", ".png" },
|
|
{ "image/svg+xml", ".svg" },
|
|
{ "image/vnd.microsoft.icon", ".ico" },
|
|
{ "text/css", ".css" },
|
|
{ "text/plain", ".txt" },
|
|
{ "application/x-javascript", ".js" },
|
|
{ "application/x-shockwave-flash", ".swf" },
|
|
{ "application/json", ".json" },
|
|
|
|
{ "application/binary", ".exe" },
|
|
{ "application/compress", ".z" },
|
|
{ "application/gzip", ".gz" },
|
|
{ "application/octet-stream", ".bin" },
|
|
{ "application/oda", ".oda" },
|
|
{ "application/pdf", ".pdf" },
|
|
{ "application/postscript", ".ai" },
|
|
{ "application/postscript", ".eps" },
|
|
{ "application/postscript", ".ps" },
|
|
{ "application/rtf", ".rtf" },
|
|
{ "application/x-bcpio", ".bcpio" },
|
|
{ "application/x-cpio", ".cpio" },
|
|
{ "application/x-csh", ".csh" },
|
|
{ "application/x-dvi", ".dvi" },
|
|
{ "application/x-gtar", ".gtar" },
|
|
{ "application/x-hdf", ".hdf" },
|
|
{ "application/x-latex", ".latex" },
|
|
{ "application/x-mif", ".mif" },
|
|
{ "application/x-netcdf", ".nc" },
|
|
{ "application/x-netcdf", ".cdf" },
|
|
{ "application/x-ns-proxy-autoconfig", ".pac" },
|
|
{ "application/x-patch", ".patch" },
|
|
{ "application/x-sh", ".sh" },
|
|
{ "application/x-shar", ".shar" },
|
|
{ "application/x-sv4cpio", ".sv4cpio" },
|
|
{ "application/x-sv4crc", ".sv4crc" },
|
|
{ "application/x-tar", ".tar" },
|
|
{ "application/x-tgz", ".tgz" },
|
|
{ "application/x-tcl", ".tcl" },
|
|
{ "application/x-tex", ".tex" },
|
|
{ "application/x-texinfo", ".texinfo" },
|
|
{ "application/x-texinfo", ".texi" },
|
|
{ "application/x-troff", ".t" },
|
|
{ "application/x-troff", ".tr" },
|
|
{ "application/x-troff", ".roff" },
|
|
{ "application/x-troff-man", ".man" },
|
|
{ "application/x-troff-me", ".me" },
|
|
{ "application/x-troff-ms", ".ms" },
|
|
{ "application/x-ustar", ".ustar" },
|
|
{ "application/x-wais-source", ".src" },
|
|
{ "application/zip", ".zip" },
|
|
{ "audio/basic", ".au snd" },
|
|
{ "audio/x-aiff", ".aif" },
|
|
{ "audio/x-aiff", ".aiff" },
|
|
{ "audio/x-aiff", ".aifc" },
|
|
{ "audio/x-wav", ".wav" },
|
|
{ "audio/x-wav", ".ram" },
|
|
{ "image/ief", ".ief" },
|
|
{ "image/jpeg", ".jpeg" },
|
|
{ "image/jpeg", ".jpe" },
|
|
{ "image/tiff", ".tiff" },
|
|
{ "image/tiff", ".tif" },
|
|
{ "image/x-cmu-raster", ".ras" },
|
|
{ "image/x-portable-anymap", ".pnm" },
|
|
{ "image/x-portable-bitmap", ".pbm" },
|
|
{ "image/x-portable-graymap", ".pgm" },
|
|
{ "image/x-portable-pixmap", ".ppm" },
|
|
{ "image/x-rgb", ".rgb" },
|
|
{ "image/x-xbitmap", ".xbm" },
|
|
{ "image/x-xpixmap", ".xpm" },
|
|
{ "image/x-xwindowdump", ".xwd" },
|
|
{ "text/html", ".cfm" },
|
|
{ "text/html", ".shtm" },
|
|
{ "text/html", ".shtml" },
|
|
{ "text/richtext", ".rtx" },
|
|
{ "text/tab-separated-values", ".tsv" },
|
|
{ "text/x-setext", ".etx" },
|
|
{ "video/mpeg", ".mpeg" },
|
|
{ "video/mpeg", ".mpg" },
|
|
{ "video/mpeg", ".mpe" },
|
|
{ "video/quicktime", ".qt" },
|
|
{ "video/quicktime", ".mov" },
|
|
{ "video/mp4", ".mp4" },
|
|
{ "video/x-msvideo", ".avi" },
|
|
{ "video/x-sgi-movie", ".movie" },
|
|
{ NULL, NULL},
|
|
};
|
|
|
|
/*
|
|
Standard HTTP error codes
|
|
*/
|
|
static WebsError websErrors[] = {
|
|
{ 200, "OK" },
|
|
{ 201, "Created" },
|
|
{ 204, "No Content" },
|
|
{ 205, "Reset Content" },
|
|
{ 206, "Partial Content" },
|
|
{ 301, "Redirect" },
|
|
{ 302, "Redirect" },
|
|
{ 304, "Not Modified" },
|
|
{ 400, "Bad Request" },
|
|
{ 401, "Unauthorized" },
|
|
{ 402, "Payment required" },
|
|
{ 403, "Forbidden" },
|
|
{ 404, "Not Found" },
|
|
{ 405, "Access Denied" },
|
|
{ 406, "Not Acceptable" },
|
|
{ 408, "Request Timeout" },
|
|
{ 413, "Request too large" },
|
|
{ 500, "Internal Server Error" },
|
|
{ 501, "Not Implemented" },
|
|
{ 503, "Service Unavailable" },
|
|
{ 0, NULL }
|
|
};
|
|
|
|
#if ME_GOAHEAD_ACCESS_LOG && !ME_ROM
|
|
static char accessLog[64] = "access.log"; /* Log filename */
|
|
static int accessFd; /* Log file handle */
|
|
#endif
|
|
|
|
static WebsHash sessions = -1;
|
|
static int sessionCount = 0;
|
|
static int pruneId; /* Callback ID */
|
|
|
|
/**************************** Forward Declarations ****************************/
|
|
|
|
static void checkTimeout(void *arg, int id);
|
|
static bool filterChunkData(Webs *wp);
|
|
static int getTimeSinceMark(Webs *wp);
|
|
static char *getToken(Webs *wp, char *delim, int validation);
|
|
static void parseFirstLine(Webs *wp);
|
|
static void parseHeaders(Webs *wp);
|
|
static bool processContent(Webs *wp);
|
|
static bool parseIncoming(Webs *wp);
|
|
static void pruneSessions(void);
|
|
static void freeSession(WebsSession *sp);
|
|
static void freeSessions(void);
|
|
static void readEvent(Webs *wp);
|
|
static void reuseConn(Webs *wp);
|
|
static void setFileLimits(void);
|
|
static int setLocalHost(void);
|
|
static void socketEvent(int sid, int mask, void *data);
|
|
static void writeEvent(Webs *wp);
|
|
static char *validateToken(char *token, char *endToken, int validation);
|
|
|
|
#if ME_GOAHEAD_ACCESS_LOG
|
|
static void logRequest(Webs *wp, int code);
|
|
#endif
|
|
|
|
/*********************************** Code *************************************/
|
|
|
|
PUBLIC int websOpen(cchar *documents, cchar *routeFile)
|
|
{
|
|
WebsMime *mt;
|
|
|
|
webs = NULL;
|
|
websMax = 0;
|
|
|
|
websOsOpen();
|
|
websRuntimeOpen();
|
|
websTimeOpen();
|
|
websFsOpen();
|
|
logOpen();
|
|
setFileLimits();
|
|
socketOpen();
|
|
if (setLocalHost() < 0) {
|
|
return -1;
|
|
}
|
|
#if ME_COM_SSL
|
|
if (sslOpen() < 0) {
|
|
return -1;
|
|
}
|
|
#endif
|
|
if ((sessions = hashCreate(-1)) < 0) {
|
|
return -1;
|
|
}
|
|
if (!websDebug) {
|
|
pruneId = websStartEvent(WEBS_SESSION_PRUNE, (WebsEventProc) pruneSessions, 0);
|
|
}
|
|
if (documents) {
|
|
websSetDocuments(documents);
|
|
}
|
|
if (websOpenRoute() < 0) {
|
|
return -1;
|
|
}
|
|
#if ME_GOAHEAD_CGI
|
|
websCgiOpen();
|
|
#endif
|
|
websOptionsOpen();
|
|
websActionOpen();
|
|
websFileOpen();
|
|
#if ME_GOAHEAD_UPLOAD
|
|
websUploadOpen();
|
|
#endif
|
|
#if ME_GOAHEAD_JAVASCRIPT
|
|
websJstOpen();
|
|
#endif
|
|
#if ME_GOAHEAD_AUTH
|
|
if (websOpenAuth(0) < 0) {
|
|
return -1;
|
|
}
|
|
#endif
|
|
if (routeFile && websLoad(routeFile) < 0) {
|
|
return -1;
|
|
}
|
|
/*
|
|
Create a mime type lookup table for quickly determining the content type
|
|
*/
|
|
websMime = hashCreate(WEBS_HASH_INIT * 4);
|
|
assert(websMime >= 0);
|
|
for (mt = websMimeList; mt->type; mt++) {
|
|
hashEnter(websMime, mt->ext, valueString(mt->type, 0), 0);
|
|
}
|
|
|
|
#if ME_GOAHEAD_ACCESS_LOG && !ME_ROM
|
|
if ((accessFd = open(accessLog, O_CREAT | O_TRUNC | O_APPEND | O_WRONLY, 0666)) < 0) {
|
|
error("Cannot open access log %s", accessLog);
|
|
return -1;
|
|
}
|
|
/* Some platforms don't implement O_APPEND (VXWORKS) */
|
|
lseek(accessFd, 0, SEEK_END);
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
|
|
PUBLIC void websClose(void)
|
|
{
|
|
Webs *wp;
|
|
int i;
|
|
|
|
websCloseRoute();
|
|
#if ME_GOAHEAD_AUTH
|
|
websCloseAuth();
|
|
#endif
|
|
if (pruneId >= 0) {
|
|
websStopEvent(pruneId);
|
|
pruneId = -1;
|
|
}
|
|
if (sessions >= 0) {
|
|
freeSessions();
|
|
}
|
|
for (i = 0; i < listenMax; i++) {
|
|
if (listens[i] >= 0) {
|
|
socketCloseConnection(listens[i]);
|
|
listens[i] = -1;
|
|
}
|
|
}
|
|
listenMax = 0;
|
|
for (i = websMax; webs && i >= 0; i--) {
|
|
if ((wp = webs[i]) == NULL) {
|
|
continue;
|
|
}
|
|
if (wp->sid >= 0) {
|
|
socketCloseConnection(wp->sid);
|
|
wp->sid = -1;
|
|
}
|
|
websFree(wp);
|
|
}
|
|
wfree(websHostUrl);
|
|
wfree(websIpAddrUrl);
|
|
websIpAddrUrl = websHostUrl = NULL;
|
|
|
|
#if ME_COM_SSL
|
|
sslClose();
|
|
#endif
|
|
#if ME_GOAHEAD_ACCESS_LOG
|
|
if (accessFd >= 0) {
|
|
close(accessFd);
|
|
accessFd = -1;
|
|
}
|
|
#endif
|
|
websFsClose();
|
|
hashFree(websMime);
|
|
socketClose();
|
|
logClose();
|
|
websTimeClose();
|
|
websRuntimeClose();
|
|
websOsClose();
|
|
}
|
|
|
|
|
|
static void initWebs(Webs *wp, int flags, int reuse)
|
|
{
|
|
WebsBuf rxbuf;
|
|
WebsTime timestamp;
|
|
void *ssl;
|
|
char ipaddr[ME_MAX_IP], ifaddr[ME_MAX_IP];
|
|
int wid, sid, timeout, listenSid;
|
|
|
|
assert(wp);
|
|
|
|
if (reuse) {
|
|
rxbuf = wp->rxbuf;
|
|
wid = wp->wid;
|
|
sid = wp->sid;
|
|
timeout = wp->timeout;
|
|
ssl = wp->ssl;
|
|
listenSid = wp->listenSid;
|
|
scopy(ipaddr, sizeof(ipaddr), wp->ipaddr);
|
|
scopy(ifaddr, sizeof(ifaddr), wp->ifaddr);
|
|
timestamp = wp->timestamp;
|
|
} else {
|
|
wid = sid = -1;
|
|
timeout = -1;
|
|
ssl = 0;
|
|
listenSid = -1;
|
|
timestamp = 0;
|
|
}
|
|
memset(wp, 0, sizeof(Webs));
|
|
wp->flags = flags;
|
|
wp->state = WEBS_BEGIN;
|
|
wp->wid = wid;
|
|
wp->sid = sid;
|
|
wp->timeout = timeout;
|
|
wp->docfd = -1;
|
|
wp->txLen = -1;
|
|
wp->rxLen = -1;
|
|
wp->responseCookies = hashCreate(7);
|
|
wp->code = HTTP_CODE_OK;
|
|
wp->ssl = ssl;
|
|
wp->listenSid = listenSid;
|
|
wp->timestamp = timestamp;
|
|
#if !ME_ROM
|
|
wp->putfd = -1;
|
|
#endif
|
|
#if ME_GOAHEAD_CGI
|
|
wp->cgifd = -1;
|
|
#endif
|
|
#if ME_GOAHEAD_UPLOAD
|
|
wp->files = -1;
|
|
wp->upfd = -1;
|
|
#endif
|
|
if (reuse) {
|
|
scopy(wp->ipaddr, sizeof(wp->ipaddr), ipaddr);
|
|
scopy(wp->ifaddr, sizeof(wp->ifaddr), ifaddr);
|
|
} else {
|
|
wp->timeout = -1;
|
|
}
|
|
wp->vars = hashCreate(WEBS_HASH_INIT);
|
|
/*
|
|
Ring queues can never be totally full and are short one byte. Better to do even I/O and allocate
|
|
a little more memory than required. The chunkbuf has extra room to fit chunk headers and trailers.
|
|
*/
|
|
assert(ME_GOAHEAD_LIMIT_BUFFER >= 1024);
|
|
bufCreate(&wp->output, ME_GOAHEAD_LIMIT_BUFFER + 1, ME_GOAHEAD_LIMIT_BUFFER + 1);
|
|
bufCreate(&wp->chunkbuf, ME_GOAHEAD_LIMIT_BUFFER + 1, ME_GOAHEAD_LIMIT_BUFFER * 2);
|
|
bufCreate(&wp->input, ME_GOAHEAD_LIMIT_BUFFER + 1, ME_GOAHEAD_LIMIT_PUT + 1);
|
|
if (reuse) {
|
|
wp->rxbuf = rxbuf;
|
|
} else {
|
|
bufCreate(&wp->rxbuf, ME_GOAHEAD_LIMIT_HEADERS, ME_GOAHEAD_LIMIT_HEADERS + ME_GOAHEAD_LIMIT_PUT);
|
|
}
|
|
}
|
|
|
|
|
|
static void termWebs(Webs *wp, int reuse)
|
|
{
|
|
assert(wp);
|
|
|
|
/*
|
|
Some of this is done elsewhere, but keep this here for when a shutdown is done and there are open connections.
|
|
*/
|
|
bufFree(&wp->input);
|
|
bufFree(&wp->output);
|
|
bufFree(&wp->chunkbuf);
|
|
if (!reuse) {
|
|
bufFree(&wp->rxbuf);
|
|
if (wp->sid >= 0) {
|
|
#if ME_COM_SSL
|
|
sslFree(wp);
|
|
#endif
|
|
socketDeleteHandler(wp->sid);
|
|
socketCloseConnection(wp->sid);
|
|
wp->sid = -1;
|
|
}
|
|
}
|
|
#if !ME_ROM
|
|
if (wp->putfd >= 0) {
|
|
close(wp->putfd);
|
|
wp->putfd = -1;
|
|
assert(wp->putname && wp->filename);
|
|
if (rename(wp->putname, wp->filename) < 0) {
|
|
error("Cannot rename PUT file from %s to %s", wp->putname, wp->filename);
|
|
}
|
|
}
|
|
#endif
|
|
#if ME_GOAHEAD_CGI
|
|
if (wp->cgifd >= 0) {
|
|
close(wp->cgifd);
|
|
wp->cgifd = -1;
|
|
}
|
|
if (wp->cgiStdin) {
|
|
unlink(wp->cgiStdin);
|
|
wfree(wp->cgiStdin);
|
|
}
|
|
#endif
|
|
#if ME_GOAHEAD_UPLOAD
|
|
wfree(wp->clientFilename);
|
|
#endif
|
|
websPageClose(wp);
|
|
if (wp->timeout >= 0 && !reuse) {
|
|
websCancelTimeout(wp);
|
|
}
|
|
wfree(wp->authDetails);
|
|
wfree(wp->authResponse);
|
|
wfree(wp->authType);
|
|
wfree(wp->contentType);
|
|
wfree(wp->cookie);
|
|
wfree(wp->decodedQuery);
|
|
wfree(wp->digest);
|
|
wfree(wp->ext);
|
|
wfree(wp->filename);
|
|
wfree(wp->host);
|
|
wfree(wp->method);
|
|
wfree(wp->password);
|
|
wfree(wp->path);
|
|
wfree(wp->protoVersion);
|
|
wfree(wp->putname);
|
|
wfree(wp->query);
|
|
wfree(wp->realm);
|
|
wfree(wp->referrer);
|
|
wfree(wp->url);
|
|
wfree(wp->userAgent);
|
|
wfree(wp->username);
|
|
#if ME_GOAHEAD_UPLOAD
|
|
wfree(wp->boundary);
|
|
wfree(wp->uploadTmp);
|
|
wfree(wp->uploadVar);
|
|
#endif
|
|
#if ME_GOAHEAD_DIGEST
|
|
wfree(wp->cnonce);
|
|
wfree(wp->digestUri);
|
|
wfree(wp->opaque);
|
|
wfree(wp->nc);
|
|
wfree(wp->nonce);
|
|
wfree(wp->qop);
|
|
#endif
|
|
hashFree(wp->vars);
|
|
hashFree(wp->responseCookies);
|
|
|
|
#if ME_GOAHEAD_UPLOAD
|
|
if (wp->files >= 0) {
|
|
websFreeUpload(wp);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
|
|
PUBLIC int websAlloc(int sid)
|
|
{
|
|
Webs *wp;
|
|
int wid;
|
|
|
|
if ((wid = wallocObject(&webs, &websMax, sizeof(Webs))) < 0) {
|
|
return -1;
|
|
}
|
|
wp = webs[wid];
|
|
assert(wp);
|
|
initWebs(wp, 0, 0);
|
|
wp->wid = wid;
|
|
wp->sid = sid;
|
|
wp->timestamp = time(0);
|
|
return wid;
|
|
}
|
|
|
|
|
|
static void reuseConn(Webs *wp)
|
|
{
|
|
assert(wp);
|
|
assert(websValid(wp));
|
|
|
|
bufCompact(&wp->rxbuf);
|
|
if (bufLen(&wp->rxbuf)) {
|
|
socketReservice(wp->sid);
|
|
}
|
|
termWebs(wp, 1);
|
|
initWebs(wp, wp->flags & (WEBS_KEEP_ALIVE | WEBS_SECURE | WEBS_HTTP11), 1);
|
|
}
|
|
|
|
|
|
PUBLIC void websFree(Webs *wp)
|
|
{
|
|
assert(wp);
|
|
assert(websValid(wp));
|
|
|
|
termWebs(wp, 0);
|
|
websMax = wfreeHandle(&webs, wp->wid);
|
|
wfree(wp);
|
|
assert(websMax >= 0);
|
|
}
|
|
|
|
|
|
/*
|
|
Called when the request is complete. Note: it may not have fully drained from the tx buffer.
|
|
*/
|
|
PUBLIC void websDone(Webs *wp)
|
|
{
|
|
WebsSocket *sp;
|
|
|
|
assert(wp);
|
|
assert(websValid(wp));
|
|
|
|
if (wp->finalized) {
|
|
return;
|
|
}
|
|
assert(WEBS_BEGIN <= wp->state && wp->state <= WEBS_COMPLETE);
|
|
#if DEPRECATED || 1
|
|
wp->flags |= WEBS_FINALIZED;
|
|
#endif
|
|
wp->finalized = 1;
|
|
|
|
/*
|
|
Once running, it is the handlers responsibility to conclude the request.
|
|
*/
|
|
if (wp->connError && wp->state < WEBS_RUNNING) {
|
|
wp->state = WEBS_COMPLETE;
|
|
}
|
|
if (wp->state < WEBS_COMPLETE) {
|
|
/*
|
|
Initiate flush. If not all flushed, wait for output to drain via a socket event.
|
|
*/
|
|
if (websFlush(wp, 0) == 0) {
|
|
sp = socketPtr(wp->sid);
|
|
socketCreateHandler(wp->sid, sp->handlerMask | SOCKET_WRITABLE, socketEvent, wp);
|
|
}
|
|
}
|
|
#if ME_GOAHEAD_ACCESS_LOG
|
|
logRequest(wp, wp->code);
|
|
#endif
|
|
if (!(wp->flags & WEBS_RESPONSE_TRACED)) {
|
|
trace(3 | WEBS_RAW_MSG, "Request complete: code %d", wp->code);
|
|
}
|
|
}
|
|
|
|
|
|
static int complete(Webs *wp, int reuse)
|
|
{
|
|
assert(wp);
|
|
assert(websValid(wp));
|
|
assert(wp->state == WEBS_BEGIN || wp->state == WEBS_COMPLETE);
|
|
|
|
if (reuse && wp->flags & WEBS_KEEP_ALIVE && wp->rxRemaining == 0) {
|
|
reuseConn(wp);
|
|
socketCreateHandler(wp->sid, SOCKET_READABLE, socketEvent, wp);
|
|
trace(5, "Keep connection alive");
|
|
return 1;
|
|
}
|
|
trace(5, "Close connection");
|
|
wp->state = WEBS_BEGIN;
|
|
wp->flags |= WEBS_CLOSED;
|
|
return 0;
|
|
}
|
|
|
|
|
|
PUBLIC int websListen(cchar *endpoint)
|
|
{
|
|
WebsSocket *sp;
|
|
char *ip, *ipaddr;
|
|
int port, secure, sid;
|
|
|
|
assert(endpoint && *endpoint);
|
|
|
|
if (listenMax >= WEBS_MAX_LISTEN) {
|
|
error("Too many listen endpoints");
|
|
return -1;
|
|
}
|
|
socketParseAddress(endpoint, &ip, &port, &secure, 80);
|
|
if ((sid = socketListen(ip, port, websAccept, 0)) < 0) {
|
|
error("Unable to open socket on port %d.", port);
|
|
return -1;
|
|
}
|
|
sp = socketPtr(sid);
|
|
sp->secure = secure;
|
|
if (sp->secure) {
|
|
if (!defaultSslPort) {
|
|
defaultSslPort = port;
|
|
}
|
|
} else if (!defaultHttpPort) {
|
|
defaultHttpPort = port;
|
|
}
|
|
listens[listenMax++] = sid;
|
|
if (ip) {
|
|
ipaddr = smatch(ip, "::") ? "[::]" : ip;
|
|
} else {
|
|
ipaddr = "*";
|
|
}
|
|
logmsg(2, "Started %s://%s:%d", secure ? "https" : "http", ipaddr, port);
|
|
|
|
if (!websHostUrl) {
|
|
if (port == 80) {
|
|
websHostUrl = sclone(ip ? ip : websIpAddr);
|
|
} else {
|
|
websHostUrl = sfmt("%s:%d", ip ? ip : websIpAddr, port);
|
|
}
|
|
}
|
|
if (!websIpAddrUrl) {
|
|
if (port == 80) {
|
|
websIpAddrUrl = sclone(websIpAddr);
|
|
} else {
|
|
websIpAddrUrl = sfmt("%s:%d", websIpAddr, port);
|
|
}
|
|
}
|
|
wfree(ip);
|
|
return sid;
|
|
}
|
|
|
|
|
|
/*
|
|
Accept a new connection from ipaddr:port
|
|
*/
|
|
PUBLIC int websAccept(int sid, cchar *ipaddr, int port, int listenSid)
|
|
{
|
|
Webs *wp;
|
|
WebsSocket *lp;
|
|
struct sockaddr_storage ifAddr;
|
|
int wid, len;
|
|
|
|
assert(sid >= 0);
|
|
assert(ipaddr && *ipaddr);
|
|
assert(listenSid >= 0);
|
|
assert(port >= 0);
|
|
|
|
/*
|
|
Allocate a new handle for this accepted connection. This will allocate a Webs structure in the webs[] list
|
|
*/
|
|
if ((wid = websAlloc(sid)) < 0) {
|
|
return -1;
|
|
}
|
|
wp = webs[wid];
|
|
assert(wp);
|
|
wp->listenSid = listenSid;
|
|
strncpy(wp->ipaddr, ipaddr, min(sizeof(wp->ipaddr) - 1, strlen(ipaddr)));
|
|
|
|
/*
|
|
Get the ip address of the interface that accept the connection.
|
|
*/
|
|
len = sizeof(ifAddr);
|
|
if (getsockname(socketPtr(sid)->sock, (struct sockaddr*) &ifAddr, (Socklen*) &len) < 0) {
|
|
error("Cannot get sockname");
|
|
websFree(wp);
|
|
return -1;
|
|
}
|
|
socketAddress((struct sockaddr*) &ifAddr, (int) len, wp->ifaddr, sizeof(wp->ifaddr), NULL);
|
|
|
|
#if ME_GOAHEAD_LEGACY
|
|
/*
|
|
Check if this is a request from a browser on this system. This is useful to know for permitting administrative
|
|
operations only for local access
|
|
*/
|
|
if (strcmp(wp->ipaddr, "127.0.0.1") == 0 || strcmp(wp->ipaddr, websIpAddr) == 0 ||
|
|
strcmp(wp->ipaddr, websHost) == 0) {
|
|
wp->flags |= WEBS_LOCAL;
|
|
}
|
|
#endif
|
|
|
|
/*
|
|
Arrange for socketEvent to be called when read data is available
|
|
*/
|
|
lp = socketPtr(listenSid);
|
|
trace(4, "New connection from %s:%d to %s:%d", ipaddr, port, wp->ifaddr, lp->port);
|
|
|
|
#if ME_COM_SSL
|
|
if (lp->secure) {
|
|
wp->flags |= WEBS_SECURE;
|
|
trace(4, "Upgrade connection to TLS");
|
|
if (sslUpgrade(wp) < 0) {
|
|
error("Cannot upgrade to TLS");
|
|
websFree(wp);
|
|
return -1;
|
|
}
|
|
}
|
|
#endif
|
|
assert(wp->timeout == -1);
|
|
wp->timeout = websStartEvent(PARSE_TIMEOUT, checkTimeout, (void*) wp);
|
|
socketEvent(sid, SOCKET_READABLE, wp);
|
|
return 0;
|
|
}
|
|
|
|
|
|
/*
|
|
The webs socket handler. Called in response to I/O. We just pass control to the relevant read or write handler. A
|
|
pointer to the webs structure is passed as a (void*) in wptr.
|
|
*/
|
|
static void socketEvent(int sid, int mask, void *wptr)
|
|
{
|
|
Webs *wp;
|
|
|
|
wp = (Webs*) wptr;
|
|
assert(wp);
|
|
|
|
assert(websValid(wp));
|
|
if (! websValid(wp)) {
|
|
return;
|
|
}
|
|
if (mask & SOCKET_READABLE) {
|
|
readEvent(wp);
|
|
}
|
|
if (mask & SOCKET_WRITABLE) {
|
|
writeEvent(wp);
|
|
}
|
|
if (wp->flags & WEBS_CLOSED) {
|
|
websFree(wp);
|
|
/* WARNING: wp not valid here */
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Read from a connection. Return the number of bytes read if successful. This may be less than the requested "len" and
|
|
may be zero. Return -1 for errors or EOF. Distinguish between error and EOF via socketEof().
|
|
*/
|
|
static ssize websRead(Webs *wp, char *buf, ssize len)
|
|
{
|
|
assert(wp);
|
|
assert(buf);
|
|
assert(len > 0);
|
|
#if ME_COM_SSL
|
|
if (wp->flags & WEBS_SECURE) {
|
|
return sslRead(wp, buf, len);
|
|
}
|
|
#endif
|
|
return socketRead(wp->sid, buf, len);
|
|
}
|
|
|
|
|
|
/*
|
|
The webs read handler. This is the primary read event loop. It uses a state machine to track progress while parsing
|
|
the HTTP request. Note: we never block as the socket is always in non-blocking mode.
|
|
*/
|
|
static void readEvent(Webs *wp)
|
|
{
|
|
WebsBuf *rxbuf;
|
|
WebsSocket *sp;
|
|
ssize nbytes;
|
|
|
|
assert(wp);
|
|
assert(websValid(wp));
|
|
|
|
if (!websValid(wp)) {
|
|
return;
|
|
}
|
|
websNoteRequestActivity(wp);
|
|
rxbuf = &wp->rxbuf;
|
|
|
|
if (bufRoom(rxbuf) < (ME_GOAHEAD_LIMIT_BUFFER + 1)) {
|
|
if (!bufGrow(rxbuf, ME_GOAHEAD_LIMIT_BUFFER + 1)) {
|
|
websError(wp, HTTP_CODE_INTERNAL_SERVER_ERROR, "Cannot grow rxbuf");
|
|
websPump(wp);
|
|
return;
|
|
}
|
|
}
|
|
if ((nbytes = websRead(wp, (char*) rxbuf->endp, ME_GOAHEAD_LIMIT_BUFFER)) > 0) {
|
|
wp->lastRead = nbytes;
|
|
bufAdjustEnd(rxbuf, nbytes);
|
|
bufAddNull(rxbuf);
|
|
}
|
|
if (nbytes > 0 || wp->state > WEBS_BEGIN) {
|
|
websPump(wp);
|
|
}
|
|
if (wp->flags & WEBS_CLOSED) {
|
|
return;
|
|
} else if (nbytes < 0 && socketEof(wp->sid)) {
|
|
/* EOF or error. Allow running requests to continue. */
|
|
if (wp->state < WEBS_READY) {
|
|
if (wp->state > WEBS_BEGIN) {
|
|
websError(wp, HTTP_CODE_COMMS_ERROR | WEBS_CLOSE, "Read error: connection lost");
|
|
websPump(wp);
|
|
} else {
|
|
complete(wp, 0);
|
|
}
|
|
} else {
|
|
socketDeleteHandler(wp->sid);
|
|
}
|
|
} else if (wp->state < WEBS_READY) {
|
|
sp = socketPtr(wp->sid);
|
|
socketCreateHandler(wp->sid, sp->handlerMask | SOCKET_READABLE, socketEvent, wp);
|
|
}
|
|
}
|
|
|
|
|
|
PUBLIC void websPump(Webs *wp)
|
|
{
|
|
bool canProceed;
|
|
|
|
for (canProceed = 1; canProceed; ) {
|
|
switch (wp->state) {
|
|
case WEBS_BEGIN:
|
|
canProceed = parseIncoming(wp);
|
|
break;
|
|
case WEBS_CONTENT:
|
|
canProceed = processContent(wp);
|
|
break;
|
|
case WEBS_READY:
|
|
if (!websRunRequest(wp)) {
|
|
/* Reroute if the handler re-wrote the request */
|
|
websRouteRequest(wp);
|
|
wp->state = WEBS_READY;
|
|
canProceed = 1;
|
|
continue;
|
|
}
|
|
canProceed = (wp->state != WEBS_RUNNING);
|
|
break;
|
|
case WEBS_RUNNING:
|
|
/* Nothing to do until websDone is called */
|
|
return;
|
|
case WEBS_COMPLETE:
|
|
canProceed = complete(wp, 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static bool parseIncoming(Webs *wp)
|
|
{
|
|
WebsBuf *rxbuf;
|
|
char *end, c;
|
|
|
|
rxbuf = &wp->rxbuf;
|
|
while (*rxbuf->servp == '\r' || *rxbuf->servp == '\n') {
|
|
if (bufGetc(rxbuf) < 0) {
|
|
break;
|
|
}
|
|
}
|
|
if ((end = strstr((char*) wp->rxbuf.servp, "\r\n\r\n")) == 0) {
|
|
if (bufLen(&wp->rxbuf) >= ME_GOAHEAD_LIMIT_HEADER) {
|
|
websError(wp, HTTP_CODE_REQUEST_TOO_LARGE | WEBS_CLOSE, "Header too large");
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
trace(3 | WEBS_RAW_MSG, "\n<<< Request\n");
|
|
c = *end;
|
|
*end = '\0';
|
|
trace(3 | WEBS_RAW_MSG, "%s\n", wp->rxbuf.servp);
|
|
*end = c;
|
|
|
|
/*
|
|
Parse the first line of the Http header
|
|
*/
|
|
parseFirstLine(wp);
|
|
if (wp->state == WEBS_COMPLETE) {
|
|
return 1;
|
|
}
|
|
parseHeaders(wp);
|
|
if (wp->state == WEBS_COMPLETE) {
|
|
return 1;
|
|
}
|
|
wp->state = (wp->rxChunkState || wp->rxLen > 0) ? WEBS_CONTENT : WEBS_READY;
|
|
|
|
websRouteRequest(wp);
|
|
|
|
if (wp->state == WEBS_COMPLETE) {
|
|
return 1;
|
|
}
|
|
#if ME_GOAHEAD_CGI
|
|
if (wp->route && wp->route->handler && wp->route->handler->service == cgiHandler) {
|
|
if (smatch(wp->method, "POST")) {
|
|
wp->cgiStdin = websGetCgiCommName();
|
|
if ((wp->cgifd = open(wp->cgiStdin, O_CREAT | O_WRONLY | O_BINARY | O_TRUNC, 0666)) < 0) {
|
|
websError(wp, HTTP_CODE_NOT_FOUND | WEBS_CLOSE, "Cannot open CGI file");
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
#if !ME_ROM
|
|
if (smatch(wp->method, "PUT")) {
|
|
WebsStat sbuf;
|
|
wp->code = (stat(wp->filename, &sbuf) == 0 && sbuf.st_mode & S_IFDIR) ? HTTP_CODE_NO_CONTENT : HTTP_CODE_CREATED;
|
|
wfree(wp->putname);
|
|
wp->putname = websTempFile(ME_GOAHEAD_PUT_DIR, "put");
|
|
if ((wp->putfd = open(wp->putname, O_BINARY | O_WRONLY | O_CREAT | O_BINARY, 0644)) < 0) {
|
|
error("Cannot create PUT filename %s", wp->putname);
|
|
websError(wp, HTTP_CODE_INTERNAL_SERVER_ERROR, "Cannot create the put URI");
|
|
wfree(wp->putname);
|
|
return 1;
|
|
}
|
|
}
|
|
#endif
|
|
return 1;
|
|
}
|
|
|
|
|
|
/*
|
|
Parse the first line of a HTTP request
|
|
*/
|
|
static void parseFirstLine(Webs *wp)
|
|
{
|
|
char *op, *protoVer, *url, *host, *query, *path, *port, *ext, *buf;
|
|
int listenPort;
|
|
|
|
assert(wp);
|
|
assert(websValid(wp));
|
|
|
|
/*
|
|
Determine the request type: GET, HEAD or POST
|
|
*/
|
|
op = getToken(wp, NULL, TOKEN_WORD);
|
|
if (op == NULL || *op == '\0') {
|
|
websError(wp, HTTP_CODE_NOT_FOUND | WEBS_CLOSE, "Bad HTTP request");
|
|
return;
|
|
}
|
|
wp->method = supper(sclone(op));
|
|
|
|
url = getToken(wp, NULL, TOKEN_URI_VALUE);
|
|
if (url == NULL || *url == '\0') {
|
|
websError(wp, HTTP_CODE_BAD_REQUEST | WEBS_CLOSE, "Bad HTTP request");
|
|
return;
|
|
}
|
|
if (strlen(url) > ME_GOAHEAD_LIMIT_URI) {
|
|
websError(wp, HTTP_CODE_REQUEST_URL_TOO_LARGE | WEBS_CLOSE, "URI too big");
|
|
return;
|
|
}
|
|
protoVer = getToken(wp, "\r\n", TOKEN_WORD);
|
|
if (protoVer == NULL || *protoVer == '\0') {
|
|
websError(wp, HTTP_CODE_BAD_REQUEST | WEBS_CLOSE, "Bad HTTP request");
|
|
return;
|
|
}
|
|
if (websGetLogLevel() == 2) {
|
|
trace(2, "%s %s %s", wp->method, url, protoVer);
|
|
}
|
|
|
|
/*
|
|
Parse the URL and store all the various URL components. websUrlParse returns an allocated buffer in buf which we
|
|
must free. We support both proxied and non-proxied requests. Proxied requests will have http://host/ at the
|
|
start of the URL. Non-proxied will just be local path names.
|
|
*/
|
|
host = path = port = query = ext = NULL;
|
|
if (websUrlParse(url, &buf, NULL, &host, &port, &path, NULL, NULL, &query) < 0) {
|
|
error("Cannot parse URL: %s", url);
|
|
websError(wp, HTTP_CODE_BAD_REQUEST | WEBS_CLOSE | WEBS_NOLOG, "Bad URL");
|
|
return;
|
|
}
|
|
|
|
// Decode the path and save. This includes the extension.
|
|
if ((wp->path = websValidateUriPath(path)) == 0) {
|
|
websError(wp, HTTP_CODE_BAD_REQUEST | WEBS_CLOSE | WEBS_NOLOG, "Bad URL");
|
|
wfree(buf);
|
|
return;
|
|
}
|
|
wp->url = sclone(url);
|
|
if ((ext = strrchr(wp->path, '.')) != NULL) {
|
|
wp->ext = sclone(slower(ext));
|
|
}
|
|
wp->filename = sfmt("%s%s", websGetDocuments(), wp->path);
|
|
wp->query = sclone(query);
|
|
wp->host = sclone(host);
|
|
wp->protocol = wp->flags & WEBS_SECURE ? "https" : "http";
|
|
if (smatch(protoVer, "HTTP/1.1")) {
|
|
wp->flags |= WEBS_KEEP_ALIVE | WEBS_HTTP11;
|
|
} else if (smatch(protoVer, "HTTP/1.0")) {
|
|
wp->flags &= ~(WEBS_HTTP11);
|
|
} else {
|
|
protoVer = "HTTP/1.1";
|
|
websError(wp, WEBS_CLOSE | HTTP_CODE_NOT_ACCEPTABLE, "Unsupported HTTP protocol");
|
|
}
|
|
wp->protoVersion = sclone(protoVer);
|
|
if ((listenPort = socketGetPort(wp->listenSid)) >= 0) {
|
|
wp->port = listenPort;
|
|
} else {
|
|
wp->port = atoi(port);
|
|
}
|
|
wfree(buf);
|
|
}
|
|
|
|
|
|
/*
|
|
Parse a full request
|
|
*/
|
|
static void parseHeaders(Webs *wp)
|
|
{
|
|
cchar *prior;
|
|
char *combined, *upperKey, *cp, *key, *value, *tok;
|
|
int count;
|
|
|
|
assert(websValid(wp));
|
|
|
|
/*
|
|
Parse the header and create the Http header keyword variables
|
|
We rewrite the header as we go for non-local requests. NOTE: this
|
|
modifies the header string directly and tokenizes each line with '\0'.
|
|
*/
|
|
for (count = 0; wp->rxbuf.servp[0] != '\r'; count++) {
|
|
if (count >= ME_GOAHEAD_LIMIT_NUM_HEADERS) {
|
|
websError(wp, HTTP_CODE_REQUEST_TOO_LARGE | WEBS_CLOSE, "Too many headers");
|
|
return;
|
|
}
|
|
key = getToken(wp, ":", TOKEN_HEADER_KEY);
|
|
if (key == NULL || *key == '\0' || bufLen(&wp->rxbuf) == 0) {
|
|
websError(wp, HTTP_CODE_BAD_REQUEST | WEBS_CLOSE, "Bad header format");
|
|
return;
|
|
}
|
|
value = getToken(wp, "\r\n", TOKEN_HEADER_VALUE);
|
|
if (value == NULL || bufLen(&wp->rxbuf) == 0 || wp->rxbuf.servp[0] == '\0') {
|
|
websError(wp, HTTP_CODE_BAD_REQUEST | WEBS_CLOSE, "Bad header format");
|
|
return;
|
|
}
|
|
slower(key);
|
|
|
|
/*
|
|
Create a header variable for each line in the header
|
|
*/
|
|
upperKey = sfmt("HTTP_%s", key);
|
|
for (cp = upperKey; *cp; cp++) {
|
|
if (*cp == '-') {
|
|
*cp = '_';
|
|
}
|
|
}
|
|
supper(upperKey);
|
|
if ((prior = websGetVar(wp, upperKey, 0)) != 0) {
|
|
combined = sfmt("%s, %s", prior, value);
|
|
websSetVar(wp, upperKey, combined);
|
|
wfree(combined);
|
|
} else {
|
|
websSetVar(wp, upperKey, value);
|
|
}
|
|
wfree(upperKey);
|
|
|
|
/*
|
|
Track the requesting agent (browser) type
|
|
*/
|
|
if (strcmp(key, "user-agent") == 0) {
|
|
wfree(wp->userAgent);
|
|
wp->userAgent = sclone(value);
|
|
|
|
} else if (scaselesscmp(key, "authorization") == 0) {
|
|
wfree(wp->authType);
|
|
wp->authType = sclone(value);
|
|
ssplit(wp->authType, " \t", &tok);
|
|
wfree(wp->authDetails);
|
|
wp->authDetails = sclone(tok);
|
|
slower(wp->authType);
|
|
|
|
} else if (strcmp(key, "connection") == 0) {
|
|
slower(value);
|
|
if (strcmp(value, "keep-alive") == 0) {
|
|
wp->flags |= WEBS_KEEP_ALIVE;
|
|
} else if (strcmp(value, "close") == 0) {
|
|
wp->flags &= ~WEBS_KEEP_ALIVE;
|
|
}
|
|
|
|
} else if (strcmp(key, "content-length") == 0) {
|
|
if ((wp->rxLen = atoi(value)) < 0) {
|
|
websError(wp, HTTP_CODE_REQUEST_TOO_LARGE | WEBS_CLOSE, "Invalid content length");
|
|
return;
|
|
}
|
|
if (smatch(wp->method, "PUT")) {
|
|
if (wp->rxLen > ME_GOAHEAD_LIMIT_PUT) {
|
|
websError(wp, HTTP_CODE_REQUEST_TOO_LARGE | WEBS_CLOSE, "Too big");
|
|
return;
|
|
}
|
|
} else {
|
|
if (wp->rxLen > ME_GOAHEAD_LIMIT_POST) { // open source code.
|
|
websError(wp, HTTP_CODE_REQUEST_TOO_LARGE | WEBS_CLOSE, "Too big");
|
|
return;
|
|
}
|
|
}
|
|
if (!smatch(wp->method, "HEAD")) {
|
|
wp->rxRemaining = wp->rxLen;
|
|
}
|
|
|
|
} else if (strcmp(key, "content-type") == 0) {
|
|
wfree(wp->contentType);
|
|
wp->contentType = sclone(value);
|
|
if (strstr(value, "application/x-www-form-urlencoded")) {
|
|
wp->flags |= WEBS_FORM;
|
|
} else if (strstr(value, "application/json")) {
|
|
wp->flags |= WEBS_JSON;
|
|
} else if (strstr(value, "multipart/form-data")) {
|
|
wp->flags |= WEBS_UPLOAD;
|
|
}
|
|
|
|
} else if (strcmp(key, "cookie") == 0) {
|
|
/* Should be only one cookie header really with semicolon delimmited key/value pairs */
|
|
wp->flags |= WEBS_COOKIE;
|
|
if (wp->cookie) {
|
|
char *prior = wp->cookie;
|
|
wp->cookie = sfmt("%s; %s", prior, value);
|
|
wfree(prior);
|
|
} else {
|
|
wp->cookie = sclone(value);
|
|
}
|
|
|
|
} else if (strcmp(key, "host") == 0) {
|
|
if ((int) strspn(value, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-.[]:")
|
|
< (int) slen(value)) {
|
|
websError(wp, WEBS_CLOSE | HTTP_CODE_BAD_REQUEST, "Bad host header");
|
|
return;
|
|
}
|
|
wfree(wp->host);
|
|
wp->host = sclone(value);
|
|
|
|
} else if (strcmp(key, "if-modified-since") == 0) {
|
|
if ((cp = strchr(value, ';')) != NULL) {
|
|
*cp = '\0';
|
|
}
|
|
websParseDateTime(&wp->since, value, 0);
|
|
|
|
/*
|
|
Yes Veronica, the HTTP spec does misspell Referrer
|
|
*/
|
|
} else if (strcmp(key, "referer") == 0) {
|
|
wfree(wp->referrer);
|
|
wp->referrer = sclone(value);
|
|
|
|
} else if (strcmp(key, "transfer-encoding") == 0) {
|
|
if (scaselesscmp(value, "chunked") == 0) {
|
|
wp->rxChunkState = WEBS_CHUNK_START;
|
|
wp->rxRemaining = MAXINT;
|
|
}
|
|
}
|
|
}
|
|
if (!wp->rxChunkState) {
|
|
/*
|
|
Step over "\r\n" after headers.
|
|
Don't do this if chunked so that chunking can parse a single chunk delimiter of "\r\nSIZE ...\r\n"
|
|
*/
|
|
if (bufLen(&wp->rxbuf) < 2 || wp->rxbuf.servp[0] != '\r' || wp->rxbuf.servp[1] != '\n') {
|
|
websError(wp, HTTP_CODE_BAD_REQUEST | WEBS_CLOSE, "Bad header termination");
|
|
return;
|
|
}
|
|
wp->rxbuf.servp += 2;
|
|
}
|
|
wp->eof = (wp->rxRemaining == 0);
|
|
}
|
|
|
|
|
|
static bool processContent(Webs *wp)
|
|
{
|
|
bool canProceed;
|
|
|
|
if (!wp->eof) {
|
|
canProceed = filterChunkData(wp);
|
|
if (!canProceed || wp->finalized) {
|
|
return canProceed;
|
|
}
|
|
#if ME_GOAHEAD_UPLOAD
|
|
if (wp->flags & WEBS_UPLOAD) {
|
|
canProceed = websProcessUploadData(wp);
|
|
if (!canProceed || wp->finalized) {
|
|
return canProceed;
|
|
}
|
|
}
|
|
#endif
|
|
#if !ME_ROM
|
|
if (wp->putfd >= 0) {
|
|
canProceed = websProcessPutData(wp);
|
|
if (!canProceed || wp->finalized) {
|
|
return canProceed;
|
|
}
|
|
}
|
|
#endif
|
|
#if ME_GOAHEAD_CGI
|
|
if (wp->cgifd >= 0) {
|
|
canProceed = websProcessCgiData(wp);
|
|
if (!canProceed || wp->finalized) {
|
|
return canProceed;
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
if (wp->eof) {
|
|
wp->state = WEBS_READY;
|
|
/*
|
|
Prevent reading content from the next request
|
|
The handler may not have been created if all the content was read in the initial read. No matter.
|
|
*/
|
|
socketDeleteHandler(wp->sid);
|
|
canProceed = 1;
|
|
}
|
|
return canProceed;
|
|
}
|
|
|
|
|
|
/*
|
|
Always called after data is consumed from the input buffer
|
|
*/
|
|
PUBLIC void websConsumeInput(Webs *wp, ssize nbytes)
|
|
{
|
|
assert(wp);
|
|
assert(nbytes >= 0);
|
|
|
|
assert(bufLen(&wp->input) >= nbytes);
|
|
if (nbytes <= 0) {
|
|
return;
|
|
}
|
|
bufAdjustStart(&wp->input, nbytes);
|
|
if (bufLen(&wp->input) == 0) {
|
|
bufReset(&wp->input);
|
|
}
|
|
}
|
|
|
|
|
|
static bool filterChunkData(Webs *wp)
|
|
{
|
|
WebsBuf *rxbuf;
|
|
ssize chunkSize;
|
|
char *start, *cp;
|
|
ssize len, nbytes;
|
|
int bad;
|
|
|
|
assert(wp);
|
|
assert(wp->rxbuf.buf);
|
|
rxbuf = &wp->rxbuf;
|
|
|
|
while (bufLen(rxbuf) > 0) {
|
|
switch (wp->rxChunkState) {
|
|
case WEBS_CHUNK_UNCHUNKED:
|
|
len = min(wp->rxRemaining, bufLen(rxbuf));
|
|
bufPutBlk(&wp->input, rxbuf->servp, len);
|
|
bufAddNull(&wp->input);
|
|
bufAdjustStart(rxbuf, len);
|
|
bufCompact(rxbuf);
|
|
wp->rxRemaining -= len;
|
|
if (wp->rxRemaining <= 0) {
|
|
wp->eof = 1;
|
|
}
|
|
assert(wp->rxRemaining >= 0);
|
|
return 1;
|
|
|
|
case WEBS_CHUNK_START:
|
|
/*
|
|
Expect: "\r\nSIZE.*\r\n"
|
|
*/
|
|
if (bufLen(rxbuf) < 5) {
|
|
return 0;
|
|
}
|
|
start = rxbuf->servp;
|
|
bad = (start[0] != '\r' || start[1] != '\n');
|
|
for (cp = &start[2]; cp < rxbuf->endp && *cp != '\n'; cp++) {}
|
|
if (*cp != '\n' && (cp - start) < 80) {
|
|
/* Insufficient data */
|
|
return 0;
|
|
}
|
|
bad += (cp[-1] != '\r' || cp[0] != '\n');
|
|
if (bad) {
|
|
websError(wp, WEBS_CLOSE | HTTP_CODE_BAD_REQUEST, "Bad chunk specification");
|
|
return 1;
|
|
}
|
|
chunkSize = hextoi(&start[2]);
|
|
if (!isxdigit((uchar) start[2]) || chunkSize < 0) {
|
|
websError(wp, WEBS_CLOSE | HTTP_CODE_BAD_REQUEST, "Bad chunk specification");
|
|
return 1;
|
|
}
|
|
if (chunkSize == 0) {
|
|
/* On the last chunk, consume the final "\r\n" */
|
|
if ((cp + 2) >= rxbuf->endp) {
|
|
/* Insufficient data */
|
|
return 0;
|
|
}
|
|
cp += 2;
|
|
bad += (cp[-1] != '\r' || cp[0] != '\n');
|
|
if (bad) {
|
|
websError(wp, WEBS_CLOSE | HTTP_CODE_BAD_REQUEST, "Bad final chunk specification");
|
|
return 1;
|
|
}
|
|
}
|
|
bufAdjustStart(rxbuf, cp - start + 1);
|
|
wp->rxChunkSize = chunkSize;
|
|
wp->rxRemaining = chunkSize;
|
|
if (chunkSize == 0) {
|
|
#if ME_GOAHEAD_LEGACY
|
|
wfree(wp->query);
|
|
wp->query = sclone(bufStart(&wp->input));
|
|
#endif
|
|
wp->eof = 1;
|
|
return 1;
|
|
}
|
|
trace(7, "chunkFilter: start incoming chunk of %d bytes", chunkSize);
|
|
wp->rxChunkState = WEBS_CHUNK_DATA;
|
|
break;
|
|
|
|
case WEBS_CHUNK_DATA:
|
|
len = min(bufLen(rxbuf), wp->rxRemaining);
|
|
nbytes = min(bufRoom(&wp->input), len);
|
|
if (len > 0 && (nbytes = bufPutBlk(&wp->input, rxbuf->servp, nbytes)) == 0) {
|
|
websError(wp, HTTP_CODE_REQUEST_TOO_LARGE | WEBS_CLOSE, "Too big");
|
|
return 1;
|
|
}
|
|
bufAddNull(&wp->input);
|
|
bufAdjustStart(rxbuf, nbytes);
|
|
wp->rxRemaining -= nbytes;
|
|
if (wp->rxRemaining <= 0) {
|
|
wp->rxChunkState = WEBS_CHUNK_START;
|
|
bufCompact(rxbuf);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
/*
|
|
Basic event loop. SocketReady returns true when a socket is ready for service. SocketSelect will block until an
|
|
event occurs. SocketProcess will actually do the servicing.
|
|
*/
|
|
PUBLIC void websServiceEvents(int *finished)
|
|
{
|
|
int delay, nextEvent;
|
|
|
|
if (finished) {
|
|
*finished = 0;
|
|
}
|
|
// ===================== added by xiao ===================== //
|
|
// delay = 0; // open source code
|
|
#define TIME_OUT_MS 1000
|
|
delay = TIME_OUT_MS; // modifed by xiao, time out 1000 ms
|
|
// ===================== added by xiao end ===================== //
|
|
while (!finished || !*finished) {
|
|
if (socketSelect(-1, delay)) {
|
|
socketProcess();
|
|
}
|
|
#if ME_GOAHEAD_CGI
|
|
delay = websCgiPoll();
|
|
#else
|
|
delay = MAXINT;
|
|
#endif
|
|
nextEvent = websRunEvents();
|
|
delay = min(delay, nextEvent);
|
|
// ===================== added by xiao ===================== //
|
|
delay = (delay <= 0 || delay >= TIME_OUT_MS) ? TIME_OUT_MS : delay;
|
|
// ===================== added by xiao end ===================== //
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
NOTE: the vars variable is modified
|
|
*/
|
|
static void addFormVars(Webs *wp, char *vars)
|
|
{
|
|
WebsKey *sp;
|
|
cchar *prior;
|
|
char *keyword, *value, *tok;
|
|
|
|
assert(wp);
|
|
assert(vars);
|
|
|
|
keyword = stok(vars, "&", &tok);
|
|
while (keyword != NULL) {
|
|
if ((value = strchr(keyword, '=')) != NULL) {
|
|
*value++ = '\0';
|
|
websDecodeUrl(keyword, keyword, strlen(keyword));
|
|
websDecodeUrl(value, value, strlen(value));
|
|
} else {
|
|
value = "";
|
|
}
|
|
if (*keyword) {
|
|
/*
|
|
If keyword has already been set, append the new value to what has been stored.
|
|
*/
|
|
if ((prior = websGetVar(wp, keyword, NULL)) != 0) {
|
|
sp = websSetVarFmt(wp, keyword, "%s %s", prior, value);
|
|
} else {
|
|
sp = websSetVar(wp, keyword, value);
|
|
}
|
|
/* Flag as untrusted keyword by setting arg to 1. This is used by CGI to prefix this keyword */
|
|
sp->arg = 1;
|
|
}
|
|
keyword = stok(NULL, "&", &tok);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Set the variable (CGI) environment for this request. Create variables for all standard CGI variables. Also decode
|
|
the query string and create a variable for each name=value pair.
|
|
*/
|
|
PUBLIC void websSetEnv(Webs *wp)
|
|
{
|
|
assert(wp);
|
|
assert(websValid(wp));
|
|
|
|
websSetVar(wp, "AUTH_TYPE", wp->authType);
|
|
websSetVarFmt(wp, "CONTENT_LENGTH", "%d", wp->rxLen);
|
|
websSetVar(wp, "CONTENT_TYPE", wp->contentType);
|
|
if (wp->route && wp->route->dir) {
|
|
websSetVar(wp, "DOCUMENT_ROOT", wp->route->dir);
|
|
}
|
|
websSetVar(wp, "GATEWAY_INTERFACE", "CGI/1.1");
|
|
websSetVar(wp, "PATH_INFO", wp->path);
|
|
websSetVar(wp, "PATH_TRANSLATED", wp->filename);
|
|
websSetVar(wp, "QUERY_STRING", wp->query);
|
|
websSetVar(wp, "REMOTE_ADDR", wp->ipaddr);
|
|
websSetVar(wp, "REMOTE_USER", wp->username);
|
|
websSetVar(wp, "REMOTE_HOST", wp->ipaddr);
|
|
websSetVar(wp, "REQUEST_METHOD", wp->method);
|
|
websSetVar(wp, "REQUEST_TRANSPORT", wp->protocol);
|
|
websSetVar(wp, "REQUEST_URI", wp->path);
|
|
websSetVar(wp, "SERVER_ADDR", wp->ifaddr);
|
|
websSetVar(wp, "SERVER_HOST", websHost);
|
|
websSetVar(wp, "SERVER_NAME", websHost);
|
|
websSetVarFmt(wp, "SERVER_PORT", "%d", wp->port);
|
|
websSetVar(wp, "SERVER_PROTOCOL", wp->protoVersion);
|
|
websSetVar(wp, "SERVER_URL", websHostUrl);
|
|
websSetVarFmt(wp, "SERVER_SOFTWARE", "GoAhead/%s", ME_VERSION);
|
|
}
|
|
|
|
|
|
PUBLIC void websSetFormVars(Webs *wp)
|
|
{
|
|
char *data;
|
|
|
|
if (wp->rxLen > 0 && bufLen(&wp->input) > 0) {
|
|
if (wp->flags & WEBS_FORM) {
|
|
data = sclone(wp->input.servp);
|
|
addFormVars(wp, data);
|
|
wfree(data);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
PUBLIC void websSetQueryVars(Webs *wp)
|
|
{
|
|
/*
|
|
Decode and create an environment query variable for each query keyword. We split into pairs at each '&', then
|
|
split pairs at the '='. Note: we rely on wp->decodedQuery preserving the decoded values in the symbol table.
|
|
*/
|
|
if (wp->query && *wp->query) {
|
|
wfree(wp->decodedQuery);
|
|
wp->decodedQuery = sclone(wp->query);
|
|
addFormVars(wp, wp->decodedQuery);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Define a webs (CGI) variable for this connection. Also create in relevant scripting engines. Note: the incoming
|
|
value may be volatile.
|
|
*/
|
|
PUBLIC WebsKey *websSetVarFmt(Webs *wp, cchar *var, cchar *fmt, ...)
|
|
{
|
|
WebsValue v;
|
|
va_list args;
|
|
|
|
assert(websValid(wp));
|
|
assert(var && *var);
|
|
|
|
if (fmt) {
|
|
va_start(args, fmt);
|
|
v = valueString(sfmtv(fmt, args), 0);
|
|
v.allocated = 1;
|
|
va_end(args);
|
|
} else {
|
|
v = valueString("", 0);
|
|
}
|
|
return hashEnter(wp->vars, var, v, 0);
|
|
}
|
|
|
|
|
|
PUBLIC WebsKey *websSetVar(Webs *wp, cchar *var, cchar *value)
|
|
{
|
|
WebsValue v;
|
|
|
|
assert(websValid(wp));
|
|
assert(var && *var);
|
|
|
|
if (value) {
|
|
v = valueString(value, VALUE_ALLOCATE);
|
|
} else {
|
|
v = valueString("", 0);
|
|
}
|
|
return hashEnter(wp->vars, var, v, 0);
|
|
}
|
|
|
|
|
|
/*
|
|
Return TRUE if a webs variable exists for this connection.
|
|
*/
|
|
PUBLIC bool websTestVar(Webs *wp, cchar *var)
|
|
{
|
|
WebsKey *sp;
|
|
|
|
assert(websValid(wp));
|
|
assert(var && *var);
|
|
|
|
if (var == NULL || *var == '\0') {
|
|
return 0;
|
|
}
|
|
if ((sp = hashLookup(wp->vars, var)) == NULL) {
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
|
|
/*
|
|
Get a webs variable but return a default value if string not found. Note, defaultGetValue can be NULL to permit
|
|
testing existence.
|
|
*/
|
|
PUBLIC cchar *websGetVar(Webs *wp, cchar *var, cchar *defaultGetValue)
|
|
{
|
|
WebsKey *sp;
|
|
|
|
assert(websValid(wp));
|
|
assert(var && *var);
|
|
|
|
if ((sp = hashLookup(wp->vars, var)) != NULL) {
|
|
assert(sp->content.type == string);
|
|
if (sp->content.value.string) {
|
|
return sp->content.value.string;
|
|
} else {
|
|
return "";
|
|
}
|
|
}
|
|
return defaultGetValue;
|
|
}
|
|
|
|
|
|
/*
|
|
Return TRUE if a webs variable is set to a given value
|
|
*/
|
|
PUBLIC int websCompareVar(Webs *wp, cchar *var, cchar *value)
|
|
{
|
|
assert(websValid(wp));
|
|
assert(var && *var);
|
|
|
|
if (strcmp(value, websGetVar(wp, var, " __UNDEF__ ")) == 0) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
/*
|
|
Cancel the request timeout. Note may be called multiple times.
|
|
*/
|
|
PUBLIC void websCancelTimeout(Webs *wp)
|
|
{
|
|
assert(websValid(wp));
|
|
|
|
if (wp->timeout >= 0) {
|
|
websStopEvent(wp->timeout);
|
|
wp->timeout = -1;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Output a HTTP response back to the browser. If redirect is set to a URL, the browser will be sent to this location.
|
|
*/
|
|
PUBLIC void websResponse(Webs *wp, int code, cchar *message)
|
|
{
|
|
ssize len;
|
|
|
|
assert(websValid(wp));
|
|
websSetStatus(wp, code);
|
|
|
|
if (!smatch(wp->method, "HEAD") && message && *message) {
|
|
len = slen(message);
|
|
websWriteHeaders(wp, len + 2, 0);
|
|
websWriteEndHeaders(wp);
|
|
websWriteBlock(wp, message, len);
|
|
websWriteBlock(wp, "\r\n", 2);
|
|
} else {
|
|
websWriteHeaders(wp, 0, 0);
|
|
websWriteEndHeaders(wp);
|
|
}
|
|
websDone(wp);
|
|
}
|
|
|
|
|
|
static char *makeUri(cchar *scheme, cchar *host, int port, cchar *path)
|
|
{
|
|
if (port <= 0) {
|
|
port = smatch(scheme, "https") ? defaultSslPort : defaultHttpPort;
|
|
}
|
|
if (port == 80 || port == 443) {
|
|
return sfmt("%s://%s%s", scheme, host, path);
|
|
}
|
|
return sfmt("%s://%s:%d%s", scheme, host, port, path);
|
|
}
|
|
|
|
|
|
/*
|
|
Redirect the user to another webs page
|
|
*/
|
|
PUBLIC void websRedirect(Webs *wp, cchar *uri)
|
|
{
|
|
char *encoded, *message, *location, *scheme, *host, *pstr;
|
|
bool secure, fullyQualified;
|
|
ssize len;
|
|
int originalPort, port;
|
|
|
|
assert(websValid(wp));
|
|
assert(uri);
|
|
message = location = NULL;
|
|
originalPort = port = 0;
|
|
|
|
// This has risk of host header injection.
|
|
// If enabled, must also free host and MUST sanitize wp->host which comes from the host header.
|
|
// host = sclone(wp->host ? wp->host : websHostUrl);
|
|
|
|
host = websHostUrl;
|
|
pstr = strchr(host, ']');
|
|
pstr = pstr ? pstr : host;
|
|
if ((pstr = strchr(pstr, ':')) != 0) {
|
|
*pstr++ = '\0';
|
|
originalPort = atoi(pstr);
|
|
}
|
|
if (smatch(uri, "http://") || smatch(uri, "https://")) {
|
|
/* Protocol switch with existing Uri */
|
|
scheme = sncmp(uri, "https", 5) == 0 ? "https" : "http";
|
|
uri = location = makeUri(scheme, host, 0, wp->url);
|
|
}
|
|
secure = strstr(uri, "https://") != 0;
|
|
fullyQualified = strstr(uri, "http://") || strstr(uri, "https://");
|
|
if (!fullyQualified) {
|
|
port = originalPort;
|
|
if (wp->flags & WEBS_SECURE) {
|
|
secure = 1;
|
|
}
|
|
}
|
|
scheme = secure ? "https" : "http";
|
|
if (port <= 0) {
|
|
port = secure ? defaultSslPort : defaultHttpPort;
|
|
}
|
|
if (strstr(uri, "https:///")) {
|
|
/* Short-hand for redirect to https */
|
|
uri = location = makeUri(scheme, host, port, &uri[8]);
|
|
|
|
} else if (strstr(uri, "http:///")) {
|
|
uri = location = makeUri(scheme, host, port, &uri[7]);
|
|
|
|
} else if (!fullyQualified) {
|
|
uri = location = makeUri(scheme, host, port, uri);
|
|
}
|
|
encoded = websEscapeUri(uri);
|
|
message = sfmt("<html><head></head><body>\r\n\
|
|
This document has moved to a new <a href=\"%s\">location</a>.\r\n\
|
|
Please update your documents to reflect the new location.\r\n\
|
|
</body></html>\r\n", encoded);
|
|
len = slen(message);
|
|
websSetStatus(wp, HTTP_CODE_MOVED_TEMPORARILY);
|
|
websWriteHeaders(wp, len + 2, uri);
|
|
websWriteEndHeaders(wp);
|
|
websWriteBlock(wp, message, len);
|
|
websWriteBlock(wp, "\r\n", 2);
|
|
websDone(wp);
|
|
wfree(message);
|
|
wfree(location);
|
|
wfree(encoded);
|
|
}
|
|
|
|
|
|
PUBLIC int websRedirectByStatus(Webs *wp, int status)
|
|
{
|
|
WebsKey *key;
|
|
char code[16], *uri;
|
|
|
|
assert(wp);
|
|
assert(status >= 0);
|
|
|
|
if (wp->route && wp->route->redirects >= 0) {
|
|
itosbuf(code, sizeof(code), status, 10);
|
|
if ((key = hashLookup(wp->route->redirects, code)) != 0) {
|
|
uri = key->content.value.string;
|
|
} else {
|
|
return -1;
|
|
}
|
|
websRedirect(wp, uri);
|
|
} else {
|
|
if (status == HTTP_CODE_UNAUTHORIZED) {
|
|
websError(wp, status, "Access Denied. User not logged in.");
|
|
} else {
|
|
websError(wp, status, 0);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
/*
|
|
Escape HTML to escape defined characters (prevent cross-site scripting)
|
|
Returns an allocated string.
|
|
*/
|
|
PUBLIC char *websEscapeHtml(cchar *html)
|
|
{
|
|
cchar *ip;
|
|
char *result, *op;
|
|
int len;
|
|
|
|
if (!html) {
|
|
return sclone("");
|
|
}
|
|
for (len = 1, ip = html; *ip; ip++, len++) {
|
|
if (charMatch[(int) (uchar) *ip] & WEBS_ENCODE_HTML) {
|
|
len += 5;
|
|
}
|
|
}
|
|
if ((result = walloc(len)) == 0) {
|
|
return 0;
|
|
}
|
|
/*
|
|
Leave room for the biggest expansion
|
|
*/
|
|
op = result;
|
|
while (*html != '\0') {
|
|
if (charMatch[(uchar) *html] & WEBS_ENCODE_HTML) {
|
|
if (*html == '&') {
|
|
strcpy(op, "&");
|
|
op += 5;
|
|
} else if (*html == '<') {
|
|
strcpy(op, "<");
|
|
op += 4;
|
|
} else if (*html == '>') {
|
|
strcpy(op, ">");
|
|
op += 4;
|
|
} else if (*html == '#') {
|
|
strcpy(op, "#");
|
|
op += 5;
|
|
} else if (*html == '(') {
|
|
strcpy(op, "(");
|
|
op += 5;
|
|
} else if (*html == ')') {
|
|
strcpy(op, ")");
|
|
op += 5;
|
|
} else if (*html == '"') {
|
|
strcpy(op, """);
|
|
op += 6;
|
|
} else if (*html == '\'') {
|
|
strcpy(op, "'");
|
|
op += 5;
|
|
} else {
|
|
assert(0);
|
|
}
|
|
html++;
|
|
} else {
|
|
*op++ = *html++;
|
|
}
|
|
}
|
|
assert(op < &result[len]);
|
|
*op = '\0';
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
Uri encode by encoding special characters with hex equivalents. Return an allocated string.
|
|
*/
|
|
PUBLIC char *websEscapeUri(cchar *uri)
|
|
{
|
|
static cchar hexTable[] = "0123456789ABCDEF";
|
|
uchar c;
|
|
cchar *ip;
|
|
char *result, *op;
|
|
int len;
|
|
|
|
assert(uri);
|
|
|
|
if (!uri) {
|
|
return sclone("");
|
|
}
|
|
for (len = 1, ip = uri; *ip; ip++, len++) {
|
|
if (charMatch[(uchar) *ip] & WEBS_ENCODE_URI) {
|
|
len += 2;
|
|
}
|
|
}
|
|
if ((result = walloc(len)) == 0) {
|
|
return 0;
|
|
}
|
|
op = result;
|
|
|
|
while ((c = (uchar) (*uri++)) != 0) {
|
|
if (charMatch[c] & WEBS_ENCODE_URI) {
|
|
*op++ = '%';
|
|
*op++ = hexTable[c >> 4];
|
|
*op++ = hexTable[c & 0xf];
|
|
} else {
|
|
*op++ = c;
|
|
}
|
|
}
|
|
assert(op < &result[len]);
|
|
*op = '\0';
|
|
return result;
|
|
}
|
|
|
|
|
|
PUBLIC int websWriteHeader(Webs *wp, cchar *key, cchar *fmt, ...)
|
|
{
|
|
va_list vargs;
|
|
char *buf;
|
|
|
|
assert(websValid(wp));
|
|
|
|
if (!(wp->flags & WEBS_RESPONSE_TRACED)) {
|
|
wp->flags |= WEBS_RESPONSE_TRACED;
|
|
trace(3 | WEBS_RAW_MSG, "\n>>> Response\n");
|
|
}
|
|
if (key) {
|
|
if (websWriteBlock(wp, key, strlen(key)) < 0) {
|
|
return -1;
|
|
}
|
|
if (websWriteBlock(wp, ": ", 2) < 0) {
|
|
return -1;
|
|
}
|
|
trace(3 | WEBS_RAW_MSG, "%s: ", key);
|
|
}
|
|
if (fmt) {
|
|
va_start(vargs, fmt);
|
|
if ((buf = sfmtv(fmt, vargs)) == 0) {
|
|
error("websWrite lost data, buffer overflow");
|
|
return -1;
|
|
}
|
|
va_end(vargs);
|
|
trace(3 | WEBS_RAW_MSG, "%s", buf);
|
|
if (websWriteBlock(wp, buf, strlen(buf)) < 0) {
|
|
return -1;
|
|
}
|
|
wfree(buf);
|
|
if (websWriteBlock(wp, "\r\n", 2) != 2) {
|
|
return -1;
|
|
}
|
|
}
|
|
trace(3 | WEBS_RAW_MSG, "\r\n");
|
|
return 0;
|
|
}
|
|
|
|
|
|
PUBLIC void websSetStatus(Webs *wp, int code)
|
|
{
|
|
wp->code = (code & WEBS_CODE_MASK);
|
|
if (code & WEBS_CLOSE) {
|
|
wp->flags &= ~WEBS_KEEP_ALIVE;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Write a set of headers. Does not write the trailing blank line so callers can add more headers.
|
|
Set length to -1 if unknown and transfer-chunk-encoding will be employed.
|
|
*/
|
|
PUBLIC void websWriteHeaders(Webs *wp, ssize length, cchar *location)
|
|
{
|
|
WebsKey *cookie, *key, *next;
|
|
char *date, *protoVersion;
|
|
|
|
assert(websValid(wp));
|
|
|
|
if (!(wp->flags & WEBS_HEADERS_CREATED)) {
|
|
protoVersion = wp->protoVersion;
|
|
if (!protoVersion) {
|
|
protoVersion = "HTTP/1.0";
|
|
wp->flags &= ~WEBS_KEEP_ALIVE;
|
|
}
|
|
websWriteHeader(wp, NULL, "%s %d %s", protoVersion, wp->code, websErrorMsg(wp->code));
|
|
#if !ME_GOAHEAD_STEALTH
|
|
websWriteHeader(wp, "Server", "GoAhead-http");
|
|
#endif
|
|
if ((date = websGetDateString(NULL)) != NULL) {
|
|
websWriteHeader(wp, "Date", "%s", date);
|
|
wfree(date);
|
|
}
|
|
if (wp->authResponse) {
|
|
websWriteHeader(wp, "WWW-Authenticate", "%s", wp->authResponse);
|
|
}
|
|
if (length >= 0) {
|
|
if (smatch(wp->method, "HEAD")) {
|
|
websWriteHeader(wp, "Content-Length", "%d", (int) length);
|
|
} else if (!((100 <= wp->code && wp->code <= 199) || wp->code == 204 || wp->code == 304)) {
|
|
/* Server must not emit a content length header for 1XX, 204 and 304 status */
|
|
websWriteHeader(wp, "Content-Length", "%d", (int) length);
|
|
}
|
|
}
|
|
wp->txLen = length;
|
|
if (wp->txLen < 0) {
|
|
websWriteHeader(wp, "Transfer-Encoding", "chunked");
|
|
}
|
|
if (wp->flags & WEBS_KEEP_ALIVE) {
|
|
websWriteHeader(wp, "Connection", "keep-alive");
|
|
} else {
|
|
websWriteHeader(wp, "Connection", "close");
|
|
}
|
|
if (location) {
|
|
websWriteHeader(wp, "Location", "%s", location);
|
|
} else if ((key = hashLookup(websMime, wp->ext)) != 0) {
|
|
websWriteHeader(wp, "Content-Type", "%s", key->content.value.string);
|
|
}
|
|
for (cookie = hashFirst(wp->responseCookies); cookie; cookie = next) {
|
|
websWriteHeader(wp, "Set-Cookie", "%s", cookie->content.value.string);
|
|
websWriteHeader(wp, "Cache-Control", "%s", "no-cache=\"set-cookie\"");
|
|
next = hashNext(wp->responseCookies, cookie);
|
|
}
|
|
#if defined(ME_GOAHEAD_CLIENT_CACHE)
|
|
if (wp->ext) {
|
|
char *etok = sfmt("%s,", &wp->ext[1]);
|
|
if (strstr(ME_GOAHEAD_CLIENT_CACHE ",", etok)) {
|
|
websWriteHeader(wp, "Cache-Control", "public, max-age=%d", ME_GOAHEAD_CLIENT_CACHE_LIFESPAN);
|
|
}
|
|
wfree(etok);
|
|
}
|
|
#endif
|
|
#ifdef ME_GOAHEAD_XFRAME_HEADER
|
|
if (*ME_GOAHEAD_XFRAME_HEADER) {
|
|
websWriteHeader(wp, "X-Frame-Options", "%s", ME_GOAHEAD_XFRAME_HEADER);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
|
|
PUBLIC void websWriteEndHeaders(Webs *wp)
|
|
{
|
|
assert(wp);
|
|
/*
|
|
By omitting the "\r\n" delimiter after the headers, chunks can emit "\r\nSize\r\n" as a single chunk delimiter
|
|
*/
|
|
if (wp->txLen >= 0) {
|
|
websWriteBlock(wp, "\r\n", 2);
|
|
}
|
|
wp->flags |= WEBS_HEADERS_CREATED;
|
|
if (wp->txLen < 0) {
|
|
wp->flags |= WEBS_CHUNKING;
|
|
}
|
|
}
|
|
|
|
|
|
PUBLIC void websSetTxLength(Webs *wp, ssize length)
|
|
{
|
|
assert(wp);
|
|
wp->txLen = length;
|
|
}
|
|
|
|
|
|
/*
|
|
Do formatted output to the browser. This is the public Javascript and form write procedure.
|
|
*/
|
|
PUBLIC ssize websWrite(Webs *wp, cchar *fmt, ...)
|
|
{
|
|
va_list vargs;
|
|
char *buf;
|
|
ssize rc;
|
|
|
|
assert(websValid(wp));
|
|
assert(fmt && *fmt);
|
|
|
|
va_start(vargs, fmt);
|
|
|
|
buf = NULL;
|
|
rc = 0;
|
|
if ((buf = sfmtv(fmt, vargs)) == 0) {
|
|
error("websWrite lost data, buffer overflow");
|
|
}
|
|
va_end(vargs);
|
|
assert(buf);
|
|
if (buf) {
|
|
rc = websWriteBlock(wp, buf, strlen(buf));
|
|
wfree(buf);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
|
|
/*
|
|
Non-blocking write to socket.
|
|
Returns number of bytes written. Returns -1 on errors. May return short.
|
|
*/
|
|
PUBLIC ssize websWriteSocket(Webs *wp, cchar *buf, ssize size)
|
|
{
|
|
ssize written;
|
|
|
|
assert(wp);
|
|
assert(buf);
|
|
assert(size >= 0);
|
|
|
|
if (wp->flags & WEBS_CLOSED) {
|
|
return -1;
|
|
}
|
|
#if ME_COM_SSL
|
|
if (wp->flags & WEBS_SECURE) {
|
|
if ((written = sslWrite(wp, (void*) buf, size)) < 0) {
|
|
return written;
|
|
}
|
|
} else
|
|
#endif
|
|
if ((written = socketWrite(wp->sid, (void*) buf, size)) < 0) {
|
|
return written;
|
|
}
|
|
wp->written += written;
|
|
websNoteRequestActivity(wp);
|
|
return written;
|
|
}
|
|
|
|
|
|
/*
|
|
Write some output using transfer chunk encoding if required.
|
|
Returns true if all the data was written. Otherwise return zero.
|
|
*/
|
|
static bool flushChunkData(Webs *wp)
|
|
{
|
|
ssize len, written, room;
|
|
|
|
assert(wp);
|
|
|
|
while (bufLen(&wp->chunkbuf) > 0) {
|
|
/*
|
|
Stop if there is not room for a reasonable size chunk.
|
|
Subtract 16 to allow for the final trailer.
|
|
*/
|
|
if ((room = bufRoom(&wp->output) - 16) <= CHUNK_LOW) {
|
|
bufGrow(&wp->output, CHUNK_LOW - room + 1);
|
|
if ((room = bufRoom(&wp->output) - 16) <= CHUNK_LOW) {
|
|
return 0;
|
|
}
|
|
}
|
|
switch (wp->txChunkState) {
|
|
default:
|
|
case WEBS_CHUNK_START:
|
|
/* Select the chunk size so that both the prefix and data will fit */
|
|
wp->txChunkLen = min(bufLen(&wp->chunkbuf), room - 16);
|
|
fmt(wp->txChunkPrefix, sizeof(wp->txChunkPrefix), "\r\n%x\r\n", wp->txChunkLen);
|
|
wp->txChunkPrefixLen = slen(wp->txChunkPrefix);
|
|
wp->txChunkPrefixNext = wp->txChunkPrefix;
|
|
wp->txChunkState = WEBS_CHUNK_HEADER;
|
|
break;
|
|
|
|
case WEBS_CHUNK_HEADER:
|
|
if ((written = bufPutBlk(&wp->output, wp->txChunkPrefixNext, wp->txChunkPrefixLen)) < 0) {
|
|
return 0;
|
|
} else {
|
|
wp->txChunkPrefixNext += written;
|
|
wp->txChunkPrefixLen -= written;
|
|
if (wp->txChunkPrefixLen <= 0) {
|
|
wp->txChunkState = WEBS_CHUNK_DATA;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case WEBS_CHUNK_DATA:
|
|
if (wp->txChunkLen > 0) {
|
|
len = min(room, wp->txChunkLen);
|
|
if ((written = bufPutBlk(&wp->output, wp->chunkbuf.servp, len)) != len) {
|
|
assert(0);
|
|
return -1;
|
|
}
|
|
bufAdjustStart(&wp->chunkbuf, written);
|
|
wp->txChunkLen -= written;
|
|
if (wp->txChunkLen <= 0) {
|
|
wp->txChunkState = WEBS_CHUNK_START;
|
|
bufCompact(&wp->chunkbuf);
|
|
}
|
|
bufAddNull(&wp->output);
|
|
}
|
|
}
|
|
}
|
|
return bufLen(&wp->chunkbuf) == 0;
|
|
}
|
|
|
|
|
|
/*
|
|
Initiate flushing output buffer. Returns true if all data is written to the socket and the buffer is empty.
|
|
Returns < 0 for errors
|
|
== 0 if there is output remaining to be flushed
|
|
== 1 if the output was fully written to the socket
|
|
*/
|
|
PUBLIC int websFlush(Webs *wp, bool block)
|
|
{
|
|
WebsBuf *op;
|
|
ssize nbytes, written;
|
|
int errCode, wasBlocking;
|
|
|
|
if (block) {
|
|
wasBlocking = socketSetBlock(wp->sid, 1);
|
|
}
|
|
op = &wp->output;
|
|
if (wp->flags & WEBS_CHUNKING) {
|
|
trace(6, "websFlush chunking finalized %d", wp->finalized);
|
|
if (flushChunkData(wp) && wp->finalized) {
|
|
trace(6, "websFlush: write chunk trailer");
|
|
bufPutStr(op, "\r\n0\r\n\r\n");
|
|
bufAddNull(op);
|
|
wp->flags &= ~WEBS_CHUNKING;
|
|
}
|
|
}
|
|
trace(6, "websFlush: buflen %d", bufLen(op));
|
|
written = 0;
|
|
while ((nbytes = bufLen(op)) > 0) {
|
|
if ((written = websWriteSocket(wp, op->servp, nbytes)) < 0) {
|
|
errCode = socketGetError(wp->sid);
|
|
if (errCode == EWOULDBLOCK || errCode == EAGAIN) {
|
|
/* Not an error */
|
|
written = 0;
|
|
break;
|
|
}
|
|
/*
|
|
Connection Error
|
|
*/
|
|
wp->flags &= ~WEBS_KEEP_ALIVE;
|
|
bufFlush(op);
|
|
wp->state = WEBS_COMPLETE;
|
|
break;
|
|
} else if (written == 0) {
|
|
break;
|
|
}
|
|
trace(6, "websFlush: wrote %d to socket", written);
|
|
bufAdjustStart(op, written);
|
|
bufCompact(op);
|
|
nbytes = bufLen(op);
|
|
}
|
|
assert(websValid(wp));
|
|
|
|
if (bufLen(op) == 0 && wp->finalized) {
|
|
wp->state = WEBS_COMPLETE;
|
|
}
|
|
if (block) {
|
|
socketSetBlock(wp->sid, wasBlocking);
|
|
}
|
|
if (written < 0) {
|
|
/* I/O Error */
|
|
return -1;
|
|
}
|
|
return bufLen(op) == 0;
|
|
}
|
|
|
|
|
|
/*
|
|
Respond to a writable event. First write any tx buffer by calling websFlush.
|
|
Then write body data if writeProc is defined. If all written, ensure transition to complete state.
|
|
Calls websPump() to advance state.
|
|
*/
|
|
static void writeEvent(Webs *wp)
|
|
{
|
|
WebsBuf *op;
|
|
|
|
op = &wp->output;
|
|
if (bufLen(op) > 0) {
|
|
websFlush(wp, 0);
|
|
}
|
|
if (bufLen(op) == 0 && wp->writeData) {
|
|
(wp->writeData)(wp);
|
|
}
|
|
if (wp->state != WEBS_RUNNING) {
|
|
websPump(wp);
|
|
}
|
|
}
|
|
|
|
|
|
PUBLIC void websSetBackgroundWriter(Webs *wp, WebsWriteProc proc)
|
|
{
|
|
WebsSocket *sp;
|
|
WebsBuf *op;
|
|
|
|
assert(proc);
|
|
|
|
wp->writeData = proc;
|
|
op = &wp->output;
|
|
|
|
if (bufLen(op) > 0) {
|
|
websFlush(wp, 0);
|
|
}
|
|
if (bufLen(op) == 0) {
|
|
(wp->writeData)(wp);
|
|
}
|
|
if (wp->sid >= 0 && wp->state < WEBS_COMPLETE) {
|
|
sp = socketPtr(wp->sid);
|
|
socketCreateHandler(wp->sid, sp->handlerMask | SOCKET_WRITABLE, socketEvent, wp);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Write a block of data of length to the user's browser. Output is buffered and flushed via websFlush.
|
|
This routine will never return "short". i.e. it will return the requested size to write or -1.
|
|
Buffer data. Will flush as required. May return -1 on write errors.
|
|
*/
|
|
PUBLIC ssize websWriteBlock(Webs *wp, cchar *buf, ssize size)
|
|
{
|
|
WebsBuf *op;
|
|
ssize written, thisWrite, len, room;
|
|
|
|
assert(wp);
|
|
assert(websValid(wp));
|
|
assert(buf);
|
|
assert(size >= 0);
|
|
|
|
if (wp->state >= WEBS_COMPLETE) {
|
|
return -1;
|
|
}
|
|
op = (wp->flags & WEBS_CHUNKING) ? &wp->chunkbuf : &wp->output;
|
|
written = len = 0;
|
|
|
|
while (size > 0 && wp->state < WEBS_COMPLETE) {
|
|
if (bufRoom(op) < size) {
|
|
/*
|
|
This will do a blocking I/O write. Will only ever fail for I/O errors.
|
|
*/
|
|
if (websFlush(wp, 1) < 0) {
|
|
return -1;
|
|
}
|
|
}
|
|
if ((room = bufRoom(op)) == 0) {
|
|
break;
|
|
}
|
|
thisWrite = min(room, size);
|
|
bufPutBlk(op, buf, thisWrite);
|
|
size -= thisWrite;
|
|
buf += thisWrite;
|
|
written += thisWrite;
|
|
}
|
|
bufAddNull(op);
|
|
if (wp->state >= WEBS_COMPLETE && written == 0) {
|
|
return -1;
|
|
}
|
|
return written;
|
|
}
|
|
|
|
|
|
/*
|
|
Decode a URL (or part thereof). Allows insitu decoding.
|
|
*/
|
|
PUBLIC void websDecodeUrl(char *decoded, char *input, ssize len)
|
|
{
|
|
char *ip, *op;
|
|
int num, i, c;
|
|
|
|
assert(decoded);
|
|
assert(input);
|
|
|
|
if (!decoded) {
|
|
return;
|
|
}
|
|
if (!input) {
|
|
*decoded = '\0';
|
|
return;
|
|
}
|
|
if (len < 0) {
|
|
len = strlen(input);
|
|
}
|
|
op = decoded;
|
|
for (ip = input; *ip && len > 0; ip++, op++) {
|
|
if (*ip == '+') {
|
|
*op = ' ';
|
|
} else if (*ip == '%' && isxdigit((uchar) ip[1]) && isxdigit((uchar) ip[2]) &&
|
|
!(ip[1] == '0' && ip[2] == '0')) {
|
|
/*
|
|
Convert %nn to a single character
|
|
*/
|
|
ip++;
|
|
for (i = 0, num = 0; i < 2; i++, ip++) {
|
|
c = tolower((uchar) *ip);
|
|
if (c >= 'a' && c <= 'f') {
|
|
num = (num * 16) + 10 + c - 'a';
|
|
} else {
|
|
num = (num * 16) + c - '0';
|
|
}
|
|
}
|
|
*op = (char) num;
|
|
ip--;
|
|
|
|
} else {
|
|
*op = *ip;
|
|
}
|
|
len--;
|
|
}
|
|
*op = '\0';
|
|
}
|
|
|
|
|
|
#if ME_GOAHEAD_ACCESS_LOG && !ME_ROM
|
|
/*
|
|
Output a log message in Common Log Format: See http://httpd.apache.org/docs/1.3/logs.html#common
|
|
*/
|
|
static void logRequest(Webs *wp, int code)
|
|
{
|
|
char *buf, timeStr[28], zoneStr[6], dataStr[16];
|
|
ssize len;
|
|
WebsTime timer;
|
|
struct tm localt;
|
|
#if WINDOWS
|
|
DWORD dwRet;
|
|
TIME_ZONE_INFORMATION tzi;
|
|
#endif
|
|
|
|
assert(wp);
|
|
time(&timer);
|
|
#if WINDOWS
|
|
localtime_s(&localt, &timer);
|
|
#else
|
|
localtime_r(&timer, &localt);
|
|
#endif
|
|
strftime(timeStr, sizeof(timeStr), "%d/%b/%Y:%H:%M:%S", &localt);
|
|
timeStr[sizeof(timeStr) - 1] = '\0';
|
|
#if WINDOWS
|
|
dwRet = GetTimeZoneInformation(&tzi);
|
|
fmt(zoneStr, sizeof(zoneStr), "%+03d00", -(int) (tzi.Bias/60));
|
|
#elif !VXWORKS
|
|
fmt(zoneStr, sizeof(zoneStr), "%+03d00", (int) (localt.tm_gmtoff/3600));
|
|
#else
|
|
zoneStr[0] = '\0';
|
|
#endif
|
|
zoneStr[sizeof(zoneStr) - 1] = '\0';
|
|
if (wp->written != 0) {
|
|
fmt(dataStr, sizeof(dataStr), "%Ld", wp->written);
|
|
dataStr[sizeof(dataStr) - 1] = '\0';
|
|
} else {
|
|
dataStr[0] = '-'; dataStr[1] = '\0';
|
|
}
|
|
buf = NULL;
|
|
buf = sfmt("%s - %s [%s %s] \"%s %s %s\" %d %s\n",
|
|
wp->ipaddr, wp->username == NULL ? "-" : wp->username,
|
|
timeStr, zoneStr, wp->method, wp->path, wp->protoVersion, code, dataStr);
|
|
len = strlen(buf);
|
|
write(accessFd, buf, len);
|
|
wfree(buf);
|
|
}
|
|
#endif
|
|
|
|
|
|
/*
|
|
Request and connection timeout. The timeout triggers if we have not read any data from the
|
|
users browser in the last WEBS_TIMEOUT period. If we have heard from the browser, simply
|
|
re-issue the timeout.
|
|
*/
|
|
static void checkTimeout(void *arg, int id)
|
|
{
|
|
Webs *wp;
|
|
int elapsed, delay;
|
|
|
|
wp = (Webs*) arg;
|
|
assert(websValid(wp));
|
|
|
|
elapsed = getTimeSinceMark(wp) * 1000;
|
|
if (websDebug) {
|
|
websRestartEvent(id, (int) WEBS_TIMEOUT);
|
|
return;
|
|
}
|
|
if (elapsed >= WEBS_TIMEOUT) {
|
|
if (!(wp->flags & WEBS_HEADERS_CREATED)) {
|
|
if (wp->state > WEBS_BEGIN) {
|
|
websError(wp, HTTP_CODE_REQUEST_TIMEOUT, "Request exceeded timeout");
|
|
} else {
|
|
websError(wp, HTTP_CODE_REQUEST_TIMEOUT, "Idle connection closed");
|
|
}
|
|
}
|
|
wp->state = WEBS_COMPLETE;
|
|
complete(wp, 0);
|
|
websFree(wp);
|
|
/* WARNING: wp not valid here */
|
|
return;
|
|
}
|
|
delay = WEBS_TIMEOUT - elapsed;
|
|
assert(delay > 0);
|
|
websRestartEvent(id, delay);
|
|
}
|
|
|
|
|
|
static int setLocalHost(void)
|
|
{
|
|
struct in_addr intaddr;
|
|
char host[128], *ipaddr;
|
|
|
|
if (gethostname(host, sizeof(host)) < 0) {
|
|
error("Cannot get hostname: errno %d", errno);
|
|
return -1;
|
|
}
|
|
#if VXWORKS
|
|
intaddr.s_addr = (ulong) hostGetByName(host);
|
|
ipaddr = inet_ntoa(intaddr);
|
|
websSetIpAddr(ipaddr);
|
|
websSetHost(ipaddr);
|
|
#if _WRS_VXWORKS_MAJOR < 6
|
|
free(ipaddr);
|
|
#endif
|
|
#elif ECOS
|
|
ipaddr = inet_ntoa(eth0_bootp_data.bp_yiaddr);
|
|
websSetIpAddr(ipaddr);
|
|
websSetHost(ipaddr);
|
|
#elif TIDSP
|
|
{
|
|
struct hostent *hp;
|
|
if ((hp = gethostbyname(host)) == NULL) {
|
|
error("Cannot get host address for host %s: errno %d", host, errno);
|
|
return -1;
|
|
}
|
|
memcpy((char*) &intaddr, (char *) hp->h_addr[0], (size_t) hp->h_length);
|
|
ipaddr = inet_ntoa(intaddr);
|
|
websSetIpAddr(ipaddr);
|
|
websSetHost(ipaddr);
|
|
}
|
|
#elif MACOSX
|
|
{
|
|
struct hostent *hp;
|
|
if ((hp = gethostbyname(host)) == NULL) {
|
|
if ((hp = gethostbyname(sfmt("%s.local", host))) == NULL) {
|
|
error("Cannot get host address for host %s: errno %d", host, errno);
|
|
return -1;
|
|
}
|
|
}
|
|
memcpy((char*) &intaddr, (char *) hp->h_addr_list[0], (size_t) hp->h_length);
|
|
ipaddr = inet_ntoa(intaddr);
|
|
websSetIpAddr(ipaddr);
|
|
websSetHost(ipaddr);
|
|
}
|
|
#else
|
|
{
|
|
struct hostent *hp;
|
|
if ((hp = gethostbyname(host)) == NULL) {
|
|
error("Cannot get host address for host %s: errno %d", host, errno);
|
|
return -1;
|
|
}
|
|
memcpy((char*) &intaddr, (char *) hp->h_addr_list[0], (size_t) hp->h_length);
|
|
ipaddr = inet_ntoa(intaddr);
|
|
websSetIpAddr(ipaddr);
|
|
websSetHost(ipaddr);
|
|
}
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
|
|
PUBLIC void websSetHost(cchar *host)
|
|
{
|
|
scopy(websHost, sizeof(websHost), host);
|
|
}
|
|
|
|
|
|
PUBLIC void websSetHostUrl(cchar *url)
|
|
{
|
|
assert(url && *url);
|
|
|
|
wfree(websHostUrl);
|
|
websHostUrl = sclone(url);
|
|
}
|
|
|
|
|
|
PUBLIC void websSetIpAddr(cchar *ipaddr)
|
|
{
|
|
assert(ipaddr && *ipaddr);
|
|
scopy(websIpAddr, sizeof(websIpAddr), ipaddr);
|
|
}
|
|
|
|
|
|
#if ME_GOAHEAD_LEGACY
|
|
PUBLIC void websSetRequestFilename(Webs *wp, cchar *filename)
|
|
{
|
|
assert(websValid(wp));
|
|
assert(filename && *filename);
|
|
|
|
wfree(wp->filename);
|
|
wp->filename = sclone(filename);
|
|
websSetVar(wp, "PATH_TRANSLATED", wp->filename);
|
|
}
|
|
#endif
|
|
|
|
|
|
PUBLIC int websRewriteRequest(Webs *wp, cchar *url)
|
|
{
|
|
char *buf, *path;
|
|
|
|
wfree(wp->url);
|
|
wp->url = sclone(url);
|
|
wfree(wp->path);
|
|
wp->path = 0;
|
|
|
|
if (websUrlParse(url, &buf, NULL, NULL, NULL, &path, NULL, NULL, NULL) < 0) {
|
|
return -1;
|
|
}
|
|
wp->path = sclone(path);
|
|
wfree(wp->filename);
|
|
wp->filename = 0;
|
|
wp->flags |= WEBS_REROUTE;
|
|
wfree(buf);
|
|
return 0;
|
|
}
|
|
|
|
|
|
PUBLIC bool websValid(Webs *wp)
|
|
{
|
|
int wid;
|
|
|
|
for (wid = 0; wid < websMax; wid++) {
|
|
if (wp == webs[wid]) {
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
/*
|
|
Build an ASCII time string. If sbuf is NULL we use the current time, else we use the last modified time of sbuf;
|
|
*/
|
|
PUBLIC char *websGetDateString(WebsFileInfo *sbuf)
|
|
{
|
|
WebsTime now;
|
|
struct tm tm;
|
|
char *cp;
|
|
|
|
if (sbuf == NULL) {
|
|
time(&now);
|
|
} else {
|
|
now = sbuf->mtime;
|
|
}
|
|
#if ME_UNIX_LIKE
|
|
gmtime_r(&now, &tm);
|
|
#else
|
|
{
|
|
struct tm *tp;
|
|
tp = gmtime(&now);
|
|
tm = *tp;
|
|
}
|
|
#endif
|
|
if ((cp = asctime(&tm)) != NULL) {
|
|
cp[strlen(cp) - 1] = '\0';
|
|
return sclone(cp);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
|
|
/*
|
|
Take not of the request activity and mark the time. Set a timestamp so that, later, we can return the number of seconds
|
|
since we made the mark.
|
|
*/
|
|
PUBLIC void websNoteRequestActivity(Webs *wp)
|
|
{
|
|
wp->timestamp = time(0);
|
|
}
|
|
|
|
|
|
/*
|
|
Get the number of seconds since the last mark.
|
|
*/
|
|
static int getTimeSinceMark(Webs *wp)
|
|
{
|
|
return (int) (time(0) - wp->timestamp);
|
|
}
|
|
|
|
|
|
PUBLIC bool websValidUriChars(cchar *uri)
|
|
{
|
|
ssize pos;
|
|
|
|
if (uri == 0 || *uri == 0) {
|
|
return 1;
|
|
}
|
|
pos = strspn(uri, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;=%");
|
|
if (pos < slen(uri)) {
|
|
error("Bad character in URI at \"%s\"", &uri[pos]);
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
|
|
/*
|
|
Parse the URL. A single buffer is allocated to store the parsed URL in *pbuf. This must be freed by the caller.
|
|
*/
|
|
PUBLIC int websUrlParse(cchar *url, char **pbuf, char **pscheme, char **phost, char **pport, char **ppath, char **pext,
|
|
char **preference, char **pquery)
|
|
{
|
|
char *tok, *delim, *host, *path, *port, *scheme, *reference, *query, *ext, *buf, *buf2;
|
|
ssize buflen, ulen, len;
|
|
int sep;
|
|
|
|
assert(pbuf);
|
|
if (url == 0) {
|
|
url = "";
|
|
}
|
|
/*
|
|
Allocate twice. Need to null terminate the host so have to copy the path.
|
|
*/
|
|
ulen = strlen(url);
|
|
len = ulen + 1;
|
|
buflen = len * 2;
|
|
if ((buf = walloc(buflen)) == NULL) {
|
|
return -1;
|
|
}
|
|
buf2 = &buf[ulen + 1];
|
|
sncopy(buf, len, url, ulen);
|
|
sncopy(buf2, len, url, ulen);
|
|
url = buf;
|
|
|
|
scheme = 0;
|
|
host = 0;
|
|
port = 0;
|
|
path = 0;
|
|
ext = 0;
|
|
query = 0;
|
|
reference = 0;
|
|
tok = buf;
|
|
sep = '/';
|
|
|
|
/*
|
|
[scheme://][hostname[:port]][/path[.ext]][#ref][?query]
|
|
First trim query and then reference from the end
|
|
*/
|
|
if ((query = strchr(tok, '?')) != NULL) {
|
|
*query++ = '\0';
|
|
}
|
|
if ((reference = strchr(tok, '#')) != NULL) {
|
|
*reference++ = '\0';
|
|
}
|
|
|
|
/*
|
|
[scheme://][hostname[:port]][/path]
|
|
*/
|
|
if ((delim = strstr(tok, "://")) != 0) {
|
|
scheme = tok;
|
|
*delim = '\0';
|
|
tok = &delim[3];
|
|
}
|
|
|
|
/*
|
|
[hostname[:port]][/path]
|
|
*/
|
|
if (*tok == '[' && ((delim = strchr(tok, ']')) != 0)) {
|
|
/* IPv6 [::] */
|
|
host = &tok[1];
|
|
*delim++ = '\0';
|
|
tok = delim;
|
|
|
|
} else if (*tok && *tok != '/' && *tok != ':' && (scheme || strchr(tok, ':'))) {
|
|
/*
|
|
Supported forms:
|
|
scheme://hostname
|
|
hostname[:port][/path]
|
|
*/
|
|
host = tok;
|
|
if ((tok = strpbrk(tok, ":/")) == 0) {
|
|
tok = "";
|
|
}
|
|
/* Don't terminate the hostname yet, need to see if tok is a ':' for a port. */
|
|
assert(tok);
|
|
}
|
|
|
|
/* [:port][/path] */
|
|
if (*tok == ':') {
|
|
/* Terminate hostname */
|
|
*tok++ = '\0';
|
|
port = tok;
|
|
if ((tok = strchr(tok, '/')) == 0) {
|
|
tok = "";
|
|
}
|
|
}
|
|
|
|
/* [/path] */
|
|
if (*tok) {
|
|
/*
|
|
Terminate hostname. This zeros the leading path slash.
|
|
This will be repaired before returning if ppath is set
|
|
*/
|
|
sep = *tok;
|
|
*tok++ = '\0';
|
|
path = tok;
|
|
/* path[.ext[/extra]] */
|
|
if ((tok = strrchr(path, '.')) != 0) {
|
|
if (tok[1]) {
|
|
if ((delim = strrchr(path, '/')) != 0) {
|
|
if (delim < tok) {
|
|
ext = tok;
|
|
}
|
|
} else {
|
|
ext = tok;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/*
|
|
Pass back the requested fields
|
|
*/
|
|
*pbuf = buf;
|
|
if (pscheme) {
|
|
if (scheme == 0) {
|
|
scheme = "http";
|
|
}
|
|
*pscheme = scheme;
|
|
}
|
|
if (phost) {
|
|
if (host == 0) {
|
|
host = "localhost";
|
|
}
|
|
*phost = host;
|
|
}
|
|
if (pport) {
|
|
*pport = port;
|
|
}
|
|
if (ppath) {
|
|
if (path == 0) {
|
|
scopy(buf2, 1, "/");
|
|
path = buf2;
|
|
} else {
|
|
/* Copy path to reinsert leading slash */
|
|
scopy(&buf2[1], len - 1, path);
|
|
path = buf2;
|
|
*path = sep;
|
|
}
|
|
// websDecodeUrl(path, path, -1);
|
|
*ppath = path;
|
|
}
|
|
if (pquery) {
|
|
*pquery = query;
|
|
}
|
|
if (preference) {
|
|
*preference = reference;
|
|
}
|
|
if (pext) {
|
|
if (ext) {
|
|
websDecodeUrl(ext, ext, -1);
|
|
#if ME_WIN_LIKE || MACOSX
|
|
slower(ext);
|
|
#endif
|
|
}
|
|
*pext = ext;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
/*
|
|
Normalize a URI path to remove "./", "../" and redundant separators.
|
|
Note: this does not make an abs path and does not map separators nor change case.
|
|
This validates the URI and expects it to begin with "/".
|
|
Returns an allocated path, caller must free.
|
|
*/
|
|
PUBLIC char *websNormalizeUriPath(cchar *pathArg)
|
|
{
|
|
char *dupPath, *path, *sp, *dp, *mark, **segments;
|
|
int firstc, j, i, nseg, len;
|
|
|
|
if (pathArg == 0 || *pathArg == '\0') {
|
|
return sclone("");
|
|
}
|
|
len = (int) slen(pathArg);
|
|
if ((dupPath = walloc(len + 2)) == 0) {
|
|
return NULL;
|
|
}
|
|
strcpy(dupPath, pathArg);
|
|
|
|
if ((segments = walloc(sizeof(char*) * (len + 1))) == 0) {
|
|
wfree(dupPath);
|
|
return NULL;
|
|
}
|
|
nseg = len = 0;
|
|
firstc = *dupPath;
|
|
for (mark = sp = dupPath; *sp; sp++) {
|
|
if (*sp == '/') {
|
|
*sp = '\0';
|
|
while (sp[1] == '/') {
|
|
sp++;
|
|
}
|
|
segments[nseg++] = mark;
|
|
len += (int) (sp - mark);
|
|
mark = sp + 1;
|
|
}
|
|
}
|
|
segments[nseg++] = mark;
|
|
len += (int) (sp - mark);
|
|
for (j = i = 0; i < nseg; i++, j++) {
|
|
sp = segments[i];
|
|
if (sp[0] == '.') {
|
|
if (sp[1] == '\0') {
|
|
if ((i+1) == nseg) {
|
|
/* Trim trailing "." */
|
|
segments[j] = "";
|
|
} else {
|
|
j--;
|
|
}
|
|
} else if (sp[1] == '.' && sp[2] == '\0') {
|
|
j = max(j - 2, -1);
|
|
if ((i+1) == nseg) {
|
|
nseg--;
|
|
}
|
|
} else {
|
|
/* .more-chars */
|
|
segments[j] = segments[i];
|
|
}
|
|
} else {
|
|
segments[j] = segments[i];
|
|
}
|
|
}
|
|
nseg = j;
|
|
assert(nseg >= 0);
|
|
if ((path = walloc(len + nseg + 1)) != 0) {
|
|
for (i = 0, dp = path; i < nseg; ) {
|
|
strcpy(dp, segments[i]);
|
|
len = (int) slen(segments[i]);
|
|
dp += len;
|
|
if (++i < nseg || (nseg == 1 && *segments[0] == '\0' && firstc == '/')) {
|
|
*dp++ = '/';
|
|
}
|
|
}
|
|
*dp = '\0';
|
|
}
|
|
wfree(dupPath);
|
|
wfree(segments);
|
|
return path;
|
|
}
|
|
|
|
|
|
/*
|
|
Validate a URI path for use in a HTTP request line
|
|
The URI must contain only valid characters and must being with "/" both before and after decoding.
|
|
A decoded, normalized URI path is returned.
|
|
The uri is modified. Returns an allocated path. Caller must free.
|
|
*/
|
|
PUBLIC char *websValidateUriPath(cchar *uri)
|
|
{
|
|
char *decoded, *normalized;
|
|
|
|
if (uri == 0 || *uri != '/') {
|
|
return 0;
|
|
}
|
|
if (!websValidUriChars(uri)) {
|
|
return 0;
|
|
}
|
|
decoded = walloc(slen(uri) + 1);
|
|
websDecodeUrl(decoded, (char*) uri, -1);
|
|
normalized = websNormalizeUriPath(decoded);
|
|
wfree(decoded);
|
|
|
|
if (normalized == 0) {
|
|
return 0;
|
|
}
|
|
if (*normalized != '/' || strchr(normalized, '\\')) {
|
|
wfree(normalized);
|
|
return 0;
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
|
|
/*
|
|
Open a web page. filename is the local filename. path is the URL path name.
|
|
*/
|
|
PUBLIC int websPageOpen(Webs *wp, int mode, int perm)
|
|
{
|
|
assert(websValid(wp));
|
|
return (wp->docfd = websOpenFile(wp->filename, mode, perm));
|
|
}
|
|
|
|
|
|
PUBLIC void websPageClose(Webs *wp)
|
|
{
|
|
assert(websValid(wp));
|
|
|
|
if (wp->docfd >= 0) {
|
|
websCloseFile(wp->docfd);
|
|
wp->docfd = -1;
|
|
}
|
|
}
|
|
|
|
|
|
PUBLIC int websPageStat(Webs *wp, WebsFileInfo *sbuf)
|
|
{
|
|
return websStatFile(wp->filename, sbuf);
|
|
}
|
|
|
|
|
|
PUBLIC int websPageIsDirectory(Webs *wp)
|
|
{
|
|
WebsFileInfo sbuf;
|
|
|
|
if (websStatFile(wp->filename, &sbuf) >= 0) {
|
|
return(sbuf.isDir);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
/*
|
|
Read a web page. Returns the number of _bytes_ read. len is the size of buf, in bytes.
|
|
*/
|
|
PUBLIC ssize websPageReadData(Webs *wp, char *buf, ssize nBytes)
|
|
{
|
|
|
|
assert(websValid(wp));
|
|
return websReadFile(wp->docfd, buf, nBytes);
|
|
}
|
|
|
|
|
|
/*
|
|
Move file pointer offset bytes.
|
|
*/
|
|
PUBLIC void websPageSeek(Webs *wp, Offset offset, int origin)
|
|
{
|
|
assert(websValid(wp));
|
|
|
|
websSeekFile(wp->docfd, offset, origin);
|
|
}
|
|
|
|
|
|
PUBLIC void websSetCookie(Webs *wp, cchar *name, cchar *value, cchar *path, cchar *cookieDomain, int lifespan, int flags)
|
|
{
|
|
WebsTime when;
|
|
char *cp, *expiresAtt, *expires, *domainAtt, *domain, *secure, *httponly, *cookie, *old, *sameSite;
|
|
|
|
assert(wp);
|
|
assert(name && *name);
|
|
|
|
if (path == 0) {
|
|
path = "/";
|
|
}
|
|
if (!cookieDomain) {
|
|
domain = sclone(wp->host);
|
|
if ((cp = strchr(domain, ':')) != 0) {
|
|
/* Strip port */
|
|
*cp = '\0';
|
|
}
|
|
if (*domain && domain[strlen(domain) - 1] == '.') {
|
|
/* Cleanup bonjour addresses with trailing dot */
|
|
domain[strlen(domain) - 1] = '\0';
|
|
}
|
|
} else {
|
|
domain = sclone(cookieDomain);
|
|
}
|
|
domainAtt = "";
|
|
if (smatch(domain, "localhost")) {
|
|
wfree(domain);
|
|
domain = sclone("");
|
|
} else {
|
|
domainAtt = "; domain=";
|
|
if (!strchr(domain, '.')) {
|
|
old = domain;
|
|
domain = sfmt(".%s", domain);
|
|
wfree(old);
|
|
}
|
|
}
|
|
if (lifespan > 0) {
|
|
expiresAtt = "; expires=";
|
|
when = time(0) + lifespan;
|
|
if ((expires = ctime(&when)) != NULL) {
|
|
expires[strlen(expires) - 1] = '\0';
|
|
}
|
|
|
|
} else {
|
|
expiresAtt = "";
|
|
expires = "";
|
|
}
|
|
/*
|
|
Allow multiple cookie headers. Even if the same name. Later definitions take precedence
|
|
*/
|
|
secure = (flags & WEBS_COOKIE_SECURE) ? "; secure" : "";
|
|
httponly = (flags & WEBS_COOKIE_HTTP) ? "; httponly" : "";
|
|
sameSite = "";
|
|
if (flags & WEBS_COOKIE_SAME_LAX) {
|
|
sameSite = "; SameSite=Lax";
|
|
} else if (flags & WEBS_COOKIE_SAME_STRICT) {
|
|
sameSite = "; SameSite=Strict";
|
|
}
|
|
cookie = sfmt("%s=%s; path=%s%s%s%s%s%s%s%s", name, value, path, domainAtt, domain, expiresAtt, expires, secure,
|
|
httponly, sameSite);
|
|
hashEnter(wp->responseCookies, name, valueString(cookie, 0), 0);
|
|
wfree(domain);
|
|
}
|
|
|
|
|
|
/*
|
|
Return the next token in the input stream. Does not allocate.
|
|
The content buffer is advanced to the next token.
|
|
The delimiter is a string to match and not a set of characters.
|
|
If the delimeter null, it means use white space (space or tab) as a delimiter.
|
|
*/
|
|
static char *getToken(Webs *wp, char *delim, int validation)
|
|
{
|
|
WebsBuf *buf;
|
|
char *token, *endToken;
|
|
|
|
assert(wp);
|
|
|
|
buf = &wp->rxbuf;
|
|
/* Already null terminated but for safety */
|
|
bufAddNull(buf);
|
|
token = (char*) buf->servp;
|
|
endToken = (char*) buf->endp;
|
|
|
|
/*
|
|
Eat white space before token
|
|
*/
|
|
for (; token < (char*) buf->endp && (*token == ' ' || *token == '\t'); token++) {}
|
|
|
|
if (delim) {
|
|
if ((endToken = strstr(token, delim)) == NULL) {
|
|
return NULL;
|
|
}
|
|
/* Only eat one occurrence of the delimiter */
|
|
buf->servp = endToken + strlen(delim);
|
|
*endToken = '\0';
|
|
|
|
} else {
|
|
delim = " \t";
|
|
if ((endToken = strpbrk(token, delim)) == NULL) {
|
|
return NULL;
|
|
}
|
|
buf->servp = endToken + strspn(endToken, delim);
|
|
*endToken = '\0';
|
|
}
|
|
token = validateToken(token, endToken, validation);
|
|
return token;
|
|
}
|
|
|
|
|
|
static char *validateToken(char *token, char *endToken, int validation)
|
|
{
|
|
char *t;
|
|
|
|
if (validation == TOKEN_HEADER_KEY) {
|
|
if (token == NULL || *token == '\0') {
|
|
return NULL;
|
|
}
|
|
if (strpbrk(token, "\"\\/ \t\r\n(),:;<=>?@[]{}")) {
|
|
return NULL;
|
|
}
|
|
for (t = token; *t; t++) {
|
|
if (!isprint(*t)) {
|
|
return NULL;
|
|
}
|
|
}
|
|
} else if (validation == TOKEN_HEADER_VALUE) {
|
|
if (token < endToken) {
|
|
/* Trim white space */
|
|
for (t = &token[slen(token) - 1]; t >= token; t--) {
|
|
if (isspace((uchar) *t)) {
|
|
*t = '\0';
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
while (isspace((uchar) *token)) {
|
|
token++;
|
|
}
|
|
for (t = token; *t; t++) {
|
|
if (!isprint(*t)) {
|
|
return NULL;
|
|
}
|
|
}
|
|
} else if (validation == TOKEN_URI_VALUE) {
|
|
if (!websValidUriChars(token)) {
|
|
return NULL;
|
|
}
|
|
} else if (validation == TOKEN_NUMBER) {
|
|
if (!snumber(token)) {
|
|
return NULL;
|
|
}
|
|
} else if (validation == TOKEN_WORD) {
|
|
if (strpbrk(token, " \t\r\n") != NULL) {
|
|
return NULL;
|
|
}
|
|
} else {
|
|
if (strpbrk(token, "\r\n") != NULL) {
|
|
return NULL;
|
|
}
|
|
}
|
|
return token;
|
|
}
|
|
|
|
|
|
PUBLIC int websGetBackground(void)
|
|
{
|
|
return websBackground;
|
|
}
|
|
|
|
|
|
PUBLIC void websSetBackground(int on)
|
|
{
|
|
websBackground = on;
|
|
}
|
|
|
|
|
|
PUBLIC int websGetDebug(void)
|
|
{
|
|
return websDebug;
|
|
}
|
|
|
|
|
|
PUBLIC void websSetDebug(int on)
|
|
{
|
|
websDebug = on;
|
|
}
|
|
|
|
|
|
static char *makeSessionID(Webs *wp)
|
|
{
|
|
char idBuf[64];
|
|
static int nextSession = 0;
|
|
|
|
assert(wp);
|
|
fmt(idBuf, sizeof(idBuf), "%08x%08x%d", PTOI(wp) + PTOI(wp->url), (int) time(0), nextSession++);
|
|
return websMD5Block(idBuf, slen(idBuf), "::webs.session::");
|
|
}
|
|
|
|
|
|
PUBLIC void websDestroySession(Webs *wp)
|
|
{
|
|
websGetSession(wp, 0);
|
|
if (wp->session) {
|
|
hashDelete(sessions, wp->session->id);
|
|
sessionCount--;
|
|
freeSession(wp->session);
|
|
wp->session = 0;
|
|
}
|
|
}
|
|
|
|
|
|
PUBLIC WebsSession *websCreateSession(Webs *wp)
|
|
{
|
|
websDestroySession(wp);
|
|
return websGetSession(wp, 1);
|
|
}
|
|
|
|
|
|
WebsSession *websAllocSession(Webs *wp, cchar *id, int lifespan)
|
|
{
|
|
WebsSession *sp;
|
|
|
|
assert(wp);
|
|
|
|
if ((sp = walloc(sizeof(WebsSession))) == 0) {
|
|
return 0;
|
|
}
|
|
sp->lifespan = lifespan;
|
|
sp->expires = time(0) + lifespan;
|
|
if (id == 0) {
|
|
sp->id = makeSessionID(wp);
|
|
} else {
|
|
sp->id = sclone(id);
|
|
}
|
|
if ((sp->cache = hashCreate(WEBS_SESSION_HASH)) == 0) {
|
|
wfree(sp->id);
|
|
wfree(sp);
|
|
return 0;
|
|
}
|
|
if (hashEnter(sessions, sp->id, valueSymbol(sp), 0) == 0) {
|
|
wfree(sp->id);
|
|
wfree(sp);
|
|
return 0;
|
|
}
|
|
return sp;
|
|
}
|
|
|
|
|
|
static void freeSession(WebsSession *sp)
|
|
{
|
|
assert(sp);
|
|
|
|
if (sp->cache >= 0) {
|
|
hashFree(sp->cache);
|
|
sp->cache = -1;
|
|
}
|
|
wfree(sp->id);
|
|
wfree(sp);
|
|
}
|
|
|
|
|
|
WebsSession *websGetSession(Webs *wp, int create)
|
|
{
|
|
WebsKey *sym;
|
|
char *id;
|
|
int flags;
|
|
|
|
assert(wp);
|
|
|
|
if (!wp->session) {
|
|
id = websGetSessionID(wp);
|
|
if ((sym = hashLookup(sessions, id)) == 0) {
|
|
if (!create) {
|
|
wfree(id);
|
|
return 0;
|
|
}
|
|
if (sessionCount >= ME_GOAHEAD_LIMIT_SESSION_COUNT) {
|
|
error("Too many sessions %d/%d", sessionCount, ME_GOAHEAD_LIMIT_SESSION_COUNT);
|
|
wfree(id);
|
|
return 0;
|
|
}
|
|
sessionCount++;
|
|
if ((wp->session = websAllocSession(wp, id, ME_GOAHEAD_LIMIT_SESSION_LIFE)) == 0) {
|
|
wfree(id);
|
|
return 0;
|
|
}
|
|
flags = WEBS_COOKIE_SAME_LAX | WEBS_COOKIE_HTTP;
|
|
if (wp->flags & WEBS_SECURE) {
|
|
flags |= WEBS_COOKIE_SECURE;
|
|
}
|
|
websSetCookie(wp, WEBS_SESSION, wp->session->id, "/", NULL, 0, flags);
|
|
} else {
|
|
wp->session = (WebsSession*) sym->content.value.symbol;
|
|
}
|
|
wfree(id);
|
|
}
|
|
if (wp->session) {
|
|
wp->session->expires = time(0) + wp->session->lifespan;
|
|
}
|
|
return wp->session;
|
|
}
|
|
|
|
|
|
PUBLIC char *websParseCookie(Webs *wp, char *name)
|
|
{
|
|
char *buf, *cookie, *end, *key, *tok, *value, *vtok;
|
|
|
|
assert(wp);
|
|
|
|
if (wp->cookie == 0 || name == 0 || *name == '\0') {
|
|
return 0;
|
|
}
|
|
buf = sclone(wp->cookie);
|
|
end = &buf[slen(buf)];
|
|
value = 0;
|
|
|
|
for (tok = buf; tok && tok < end; ) {
|
|
cookie = stok(tok, ";", &tok);
|
|
key = stok(cookie, "=", &vtok);
|
|
if (smatch(key, name)) {
|
|
// Remove leading spaces first, then double quotes. Spaces inside double quotes preserved.
|
|
value = sclone(strim(strim(vtok, " ", WEBS_TRIM_BOTH), "\"", WEBS_TRIM_BOTH));
|
|
break;
|
|
}
|
|
}
|
|
wfree(buf);
|
|
return value;
|
|
}
|
|
|
|
|
|
PUBLIC char *websGetSessionID(Webs *wp)
|
|
{
|
|
assert(wp);
|
|
|
|
if (wp->session) {
|
|
return wp->session->id;
|
|
}
|
|
return websParseCookie(wp, WEBS_SESSION);
|
|
}
|
|
|
|
|
|
PUBLIC cchar *websGetSessionVar(Webs *wp, cchar *key, cchar *defaultValue)
|
|
{
|
|
WebsSession *sp;
|
|
WebsKey *sym;
|
|
|
|
assert(wp);
|
|
assert(key && *key);
|
|
|
|
if ((sp = websGetSession(wp, 1)) != 0) {
|
|
if ((sym = hashLookup(sp->cache, key)) == 0) {
|
|
return defaultValue;
|
|
}
|
|
return (char*) sym->content.value.symbol;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
PUBLIC void websRemoveSessionVar(Webs *wp, cchar *key)
|
|
{
|
|
WebsSession *sp;
|
|
|
|
assert(wp);
|
|
assert(key && *key);
|
|
|
|
if ((sp = websGetSession(wp, 1)) != 0) {
|
|
hashDelete(sp->cache, key);
|
|
}
|
|
}
|
|
|
|
|
|
PUBLIC int websSetSessionVar(Webs *wp, cchar *key, cchar *value)
|
|
{
|
|
WebsSession *sp;
|
|
|
|
assert(wp);
|
|
assert(key && *key);
|
|
assert(value);
|
|
|
|
if ((sp = websGetSession(wp, 1)) == 0) {
|
|
return 0;
|
|
}
|
|
if (hashEnter(sp->cache, key, valueString(value, VALUE_ALLOCATE), 0) == 0) {
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
static void pruneSessions(void)
|
|
{
|
|
WebsSession *sp;
|
|
WebsTime when;
|
|
WebsKey *sym, *next;
|
|
int oldCount;
|
|
|
|
if (sessions >= 0) {
|
|
oldCount = sessionCount;
|
|
when = time(0);
|
|
for (sym = hashFirst(sessions); sym; sym = next) {
|
|
next = hashNext(sessions, sym);
|
|
sp = (WebsSession*) sym->content.value.symbol;
|
|
if (sp->expires <= when) {
|
|
hashDelete(sessions, sp->id);
|
|
sessionCount--;
|
|
freeSession(sp);
|
|
}
|
|
}
|
|
if (oldCount != sessionCount || sessionCount) {
|
|
trace(4, "Prune %d sessions. Remaining: %d", oldCount - sessionCount, sessionCount);
|
|
}
|
|
}
|
|
websRestartEvent(pruneId, WEBS_SESSION_PRUNE);
|
|
}
|
|
|
|
|
|
static void freeSessions(void)
|
|
{
|
|
WebsSession *sp;
|
|
WebsKey *sym, *next;
|
|
|
|
if (sessions >= 0) {
|
|
for (sym = hashFirst(sessions); sym; sym = next) {
|
|
next = hashNext(sessions, sym);
|
|
sp = (WebsSession*) sym->content.value.symbol;
|
|
hashDelete(sessions, sp->id);
|
|
freeSession(sp);
|
|
}
|
|
hashFree(sessions);
|
|
sessions = -1;
|
|
sessionCount = 0;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
One line embedding
|
|
*/
|
|
PUBLIC int websServer(cchar *endpoint, cchar *documents)
|
|
{
|
|
int finished = 0;
|
|
|
|
if (websOpen(documents, "route.txt") < 0) {
|
|
error("Cannot initialize server. Exiting.");
|
|
return -1;
|
|
}
|
|
if (websLoad("auth.txt") < 0) {
|
|
error("Cannot load auth.txt");
|
|
return -1;
|
|
}
|
|
if (websListen(endpoint) < 0) {
|
|
return -1;
|
|
}
|
|
websServiceEvents(&finished);
|
|
websClose();
|
|
return 0;
|
|
}
|
|
|
|
|
|
static void setFileLimits(void)
|
|
{
|
|
#if ME_UNIX_LIKE
|
|
struct rlimit r;
|
|
int limit;
|
|
|
|
limit = ME_GOAHEAD_LIMIT_FILES;
|
|
if (limit) {
|
|
r.rlim_cur = r.rlim_max = limit;
|
|
if (setrlimit(RLIMIT_NOFILE, &r) < 0) {
|
|
error("Cannot set file limit to %d", limit);
|
|
}
|
|
}
|
|
getrlimit(RLIMIT_NOFILE, &r);
|
|
trace(6, "Max files soft %d, max %d", r.rlim_cur, r.rlim_max);
|
|
#endif
|
|
}
|
|
|
|
/*
|
|
Output an error message and cleanup
|
|
*/
|
|
PUBLIC void websError(Webs *wp, int code, cchar *fmt, ...)
|
|
{
|
|
va_list args;
|
|
char *msg, *buf;
|
|
char *encoded;
|
|
int status;
|
|
|
|
assert(wp);
|
|
wp->error = 1;
|
|
if (code & WEBS_CLOSE) {
|
|
wp->flags &= ~WEBS_KEEP_ALIVE;
|
|
wp->connError++;
|
|
}
|
|
status = code & WEBS_CODE_MASK;
|
|
#if !ME_ROM
|
|
if (wp->putfd >= 0) {
|
|
close(wp->putfd);
|
|
wp->putfd = -1;
|
|
}
|
|
#endif
|
|
if (wp->rxRemaining && status != 200 && status != 301 && status != 302 && status != 401) {
|
|
/* Close connection so we don't have to consume remaining content */
|
|
wp->flags &= ~WEBS_KEEP_ALIVE;
|
|
}
|
|
encoded = websEscapeHtml(wp->url);
|
|
wfree(wp->url);
|
|
wp->url = encoded;
|
|
if (fmt) {
|
|
if (!(code & WEBS_NOLOG)) {
|
|
va_start(args, fmt);
|
|
msg = sfmtv(fmt, args);
|
|
va_end(args);
|
|
trace(2, "%s", msg);
|
|
wfree(msg);
|
|
}
|
|
buf = sfmt("\
|
|
<html>\r\n\
|
|
<head><title>Document Error: %s</title></head>\r\n\
|
|
<body>\r\n\
|
|
<h2>Access Error: %s</h2>\r\n\
|
|
</body>\r\n\
|
|
</html>\r\n", websErrorMsg(code), websErrorMsg(code));
|
|
} else {
|
|
buf = 0;
|
|
}
|
|
websResponse(wp, code, buf);
|
|
wfree(buf);
|
|
}
|
|
|
|
|
|
/*
|
|
Return the error message for a given code
|
|
*/
|
|
PUBLIC cchar *websErrorMsg(int code)
|
|
{
|
|
WebsError *ep;
|
|
|
|
assert(code >= 0);
|
|
code &= WEBS_CODE_MASK;
|
|
for (ep = websErrors; ep->code; ep++) {
|
|
if (code == ep->code) {
|
|
return ep->msg;
|
|
}
|
|
}
|
|
return websErrorMsg(HTTP_CODE_INTERNAL_SERVER_ERROR);
|
|
}
|
|
|
|
|
|
/*
|
|
Accessors
|
|
*/
|
|
PUBLIC cchar *websGetCookie(Webs *wp) { return wp->cookie; }
|
|
PUBLIC cchar *websGetDir(Webs *wp) { return wp->route && wp->route->dir ? wp->route->dir : websGetDocuments(); }
|
|
PUBLIC int websGetEof(Webs *wp) { return wp->eof; }
|
|
PUBLIC cchar *websGetExt(Webs *wp) { return wp->ext; }
|
|
PUBLIC cchar *websGetFilename(Webs *wp) { return wp->filename; }
|
|
PUBLIC cchar *websGetHost(Webs *wp) { return wp->host; }
|
|
PUBLIC cchar *websGetIfaddr(Webs *wp) { return wp->ifaddr; }
|
|
PUBLIC cchar *websGetIpaddr(Webs *wp) { return wp->ipaddr; }
|
|
PUBLIC cchar *websGetMethod(Webs *wp) { return wp->method; }
|
|
PUBLIC cchar *websGetPassword(Webs *wp) { return wp->password; }
|
|
PUBLIC cchar *websGetPath(Webs *wp) { return wp->path; }
|
|
PUBLIC int websGetPort(Webs *wp) { return wp->port; }
|
|
PUBLIC cchar *websGetProtocol(Webs *wp) { return wp->protocol; }
|
|
PUBLIC cchar *websGetQuery(Webs *wp) { return wp->query; }
|
|
PUBLIC cchar *websGetServer(void) { return websHost; }
|
|
PUBLIC cchar *websGetServerAddress(void) { return websIpAddr; }
|
|
PUBLIC cchar *websGetServerAddressUrl(void) { return websIpAddrUrl; }
|
|
PUBLIC cchar *websGetServerUrl(void) { return websHostUrl; }
|
|
PUBLIC cchar *websGetUrl(Webs *wp) { return wp->url; }
|
|
PUBLIC cchar *websGetUserAgent(Webs *wp) { return wp->userAgent; }
|
|
PUBLIC cchar *websGetUsername(Webs *wp) { return wp->username; }
|
|
|
|
/*
|
|
Copyright (c) Embedthis Software. All Rights Reserved.
|
|
This software is distributed under a commercial license. Consult the LICENSE.md
|
|
distributed with this software for full details and copyrights.
|
|
*/
|