From 08031ed4fa053f38a5d20a10686ffaaf42809c42 Mon Sep 17 00:00:00 2001 From: Fancy code <258828110.@qq.com> Date: Thu, 29 Feb 2024 23:29:35 -0800 Subject: [PATCH] Modify:Goahead open source code. --- external/goahead-5.2.0/modify/http.c | 3546 +++++++++++++++++ middleware/AppManager/CMakeLists.txt | 3 +- middleware/AppManager/src/AppManager.cpp | 15 +- .../AppManager/src/AppManagerMakePtr.cpp | 1 - test/middleware/AppManager/CMakeLists.txt | 2 +- .../AppManager/src/AppManager_Test.cpp | 1 + test/utils/WebServer/src/WebServer_Test.cpp | 3 +- utils/WebServer/CMakeLists.txt | 3 +- utils/WebServer/README.md | 36 +- utils/WebServer/include/WebServer.h | 11 +- utils/WebServer/src/WebServer.cpp | 48 +- 11 files changed, 3652 insertions(+), 17 deletions(-) create mode 100644 external/goahead-5.2.0/modify/http.c diff --git a/external/goahead-5.2.0/modify/http.c b/external/goahead-5.2.0/modify/http.c new file mode 100644 index 00000000..c0a89dfb --- /dev/null +++ b/external/goahead-5.2.0/modify/http.c @@ -0,0 +1,3546 @@ +/* + 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) { + 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("\r\n\ + This document has moved to a new location.\r\n\ + Please update your documents to reflect the new location.\r\n\ + \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("\ +\r\n\ + Document Error: %s\r\n\ + \r\n\ +

Access Error: %s

\r\n\ + \r\n\ +\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. + */ diff --git a/middleware/AppManager/CMakeLists.txt b/middleware/AppManager/CMakeLists.txt index b92e02be..bf80d52b 100644 --- a/middleware/AppManager/CMakeLists.txt +++ b/middleware/AppManager/CMakeLists.txt @@ -9,6 +9,7 @@ include_directories( ${UTILS_SOURCE_PATH}/StatusCode/include ${UTILS_SOURCE_PATH}/Log/include ${UTILS_SOURCE_PATH}/FxHttpServer/include + ${UTILS_SOURCE_PATH}/WebServer/include ${HAL_SOURCE_PATH}/include ) #do not rely on any other library @@ -21,7 +22,7 @@ aux_source_directory(./src/Protocol/SixFrame SRC_FILES) set(TARGET_NAME AppManager) add_library(${TARGET_NAME} STATIC ${SRC_FILES}) -target_link_libraries(${TARGET_NAME} FxHttpServer StatusCode Log) +target_link_libraries(${TARGET_NAME} WebServer StatusCode Log) if ("${CLANG_TIDY_SUPPORT}" MATCHES "true") add_custom_target( diff --git a/middleware/AppManager/src/AppManager.cpp b/middleware/AppManager/src/AppManager.cpp index 40f10128..fa942cfd 100644 --- a/middleware/AppManager/src/AppManager.cpp +++ b/middleware/AppManager/src/AppManager.cpp @@ -14,8 +14,9 @@ */ #include "AppManager.h" #include "AppManagerMakePtr.h" -#include "FxHttpServer.h" +// #include "FxHttpServer.h" #include "ILog.h" +#include "WebServer.h" AppManager::AppManager() { // @@ -49,7 +50,8 @@ void AppManager::HttpServerStart(void) } void AppManager::HttpServerStop(void) { - FxHttpServerExit(); + // FxHttpServerExit(); + WebServerExit(); if (mHttpSever.joinable()) { mHttpSever.join(); } @@ -66,6 +68,9 @@ void AppManager::HttpServerThread(void) appImpl->AppRequestHandle(url, urlLength, responseHandle, context); } }; - FxHttpServerInit(httpHandle, 8080); - FxHttpServerUnInit(); -} + // FxHttpServerInit(httpHandle, 8080); + // FxHttpServerUnInit(); + WebServerParam web = {.mIp = "192.168.1.29", .mPort = 8888, .mHttpRequestHandle = httpHandle}; + WebServerInit(web); + WebServerUnInit(); +} \ No newline at end of file diff --git a/middleware/AppManager/src/AppManagerMakePtr.cpp b/middleware/AppManager/src/AppManagerMakePtr.cpp index 76a77516..55570ecb 100644 --- a/middleware/AppManager/src/AppManagerMakePtr.cpp +++ b/middleware/AppManager/src/AppManagerMakePtr.cpp @@ -31,7 +31,6 @@ bool CreateAppManagerModule(void) bool DestroyAppManagerModule(void) { auto instance = std::make_shared(); - IAppManager::GetInstance()->UnInit(); IAppManager::GetInstance(&instance); return true; } diff --git a/test/middleware/AppManager/CMakeLists.txt b/test/middleware/AppManager/CMakeLists.txt index b69487ed..8dc716b1 100644 --- a/test/middleware/AppManager/CMakeLists.txt +++ b/test/middleware/AppManager/CMakeLists.txt @@ -8,7 +8,7 @@ include_directories( ./tool/include ${UTILS_SOURCE_PATH}/Log/include ${UTILS_SOURCE_PATH}/StatusCode/include - # ${UTILS_SOURCE_PATH}/UartDevice/include + # ${UTILS_SOURCE_PATH}/WebServer/include # ${UTILS_SOURCE_PATH}/McuProtocol/include ${UTILS_SOURCE_PATH}/KeyControl/include ${UTILS_SOURCE_PATH}/LedControl/include diff --git a/test/middleware/AppManager/src/AppManager_Test.cpp b/test/middleware/AppManager/src/AppManager_Test.cpp index 1750ece2..a2845195 100644 --- a/test/middleware/AppManager/src/AppManager_Test.cpp +++ b/test/middleware/AppManager/src/AppManager_Test.cpp @@ -14,6 +14,7 @@ */ #include "IAppManager.h" #include "ILog.h" +// #include "WebServer.h" #include #include #include diff --git a/test/utils/WebServer/src/WebServer_Test.cpp b/test/utils/WebServer/src/WebServer_Test.cpp index fc09250f..67901657 100644 --- a/test/utils/WebServer/src/WebServer_Test.cpp +++ b/test/utils/WebServer/src/WebServer_Test.cpp @@ -24,7 +24,8 @@ TEST(WebServerTest, Demo) { CreateLogModule(); ILogInit(LOG_INSTANCE_TYPE_END); - WebServerInit(); + WebServerParam web = {.mIp = "192.168.1.29", .mPort = 8080}; + WebServerInit(web); // std::this_thread::sleep_for(std::chrono::milliseconds(1000 * 10)); ILogUnInit(); } diff --git a/utils/WebServer/CMakeLists.txt b/utils/WebServer/CMakeLists.txt index 34998d09..899a2be5 100644 --- a/utils/WebServer/CMakeLists.txt +++ b/utils/WebServer/CMakeLists.txt @@ -15,8 +15,6 @@ include_directories( # ${EXTERNAL_SOURCE_PATH}/libconfig/libconfig-1.7.3/lib/.libs # ) - - if (NOT DEFINED GOAHEAD_DOCUMENTS_PATH) set(GOAHEAD_DOCUMENTS_PATH "web") endif() @@ -68,6 +66,7 @@ add_custom_command( OUTPUT ${EXTERNAL_SOURCE_PATH}/goahead-5.2.0/GoAhead/Makefile COMMAND echo "tar zxvf goahead-5.2.0.tar.gz" COMMAND tar zxvf goahead-5.2.0.tar.gz + COMMAND cp ${EXTERNAL_SOURCE_PATH}/goahead-5.2.0/modify/http.c ${EXTERNAL_SOURCE_PATH}/goahead-5.2.0/GoAhead/src WORKING_DIRECTORY ${EXTERNAL_SOURCE_PATH}/goahead-5.2.0/ ) add_custom_command( diff --git a/utils/WebServer/README.md b/utils/WebServer/README.md index 927a5d74..beb28735 100644 --- a/utils/WebServer/README.md +++ b/utils/WebServer/README.md @@ -36,4 +36,38 @@ add_custom_target( ## 1.3. 环境配置 1. 拷贝self.crt,self.key两个到运行目录; -2. 配置文件两个:route.txt,auth.txt; \ No newline at end of file +2. 配置文件两个:route.txt,auth.txt; + +## 1.4. 代码整改 + +   由于官方源码是不超时阻塞,为了方便自动化测试,把官方源码的不超时阻塞改成超时阻塞。 +``` +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 ===================== // + } +} +``` \ No newline at end of file diff --git a/utils/WebServer/include/WebServer.h b/utils/WebServer/include/WebServer.h index 49f9cec6..8e779a5f 100644 --- a/utils/WebServer/include/WebServer.h +++ b/utils/WebServer/include/WebServer.h @@ -18,7 +18,16 @@ #ifdef __cplusplus extern "C" { #endif -StatusCode WebServerInit(void); +typedef void (*ResponseHandle)(const char *, void *); +typedef void (*HttpHandleCallback)(const char *, const unsigned int, ResponseHandle, void *); +typedef struct web_server_param +{ + const char *mIp; + int mPort; + HttpHandleCallback mHttpRequestHandle; +} WebServerParam; +StatusCode WebServerInit(const WebServerParam webParam); +StatusCode WebServerExit(void); StatusCode WebServerUnInit(void); #ifdef __cplusplus } diff --git a/utils/WebServer/src/WebServer.cpp b/utils/WebServer/src/WebServer.cpp index 7c19fef6..529cf40d 100644 --- a/utils/WebServer/src/WebServer.cpp +++ b/utils/WebServer/src/WebServer.cpp @@ -27,7 +27,10 @@ static void logHeader(void) { char home[ME_GOAHEAD_LIMIT_STRING]; - getcwd(home, sizeof(home)); + char *result = getcwd(home, sizeof(home)); + if (nullptr == result) { + LogWarning("Can't get path.\n"); + } logmsg(2, "Configuration for %s", ME_TITLE); logmsg(2, "---------------------------------------------"); logmsg(2, "Version: %s", ME_VERSION); @@ -47,7 +50,30 @@ void initPlatform(void) signal(SIGKILL, sigHandler); signal(SIGPIPE, SIG_IGN); } -StatusCode WebServerInit(void) +static bool testHandler(Webs *wp) +{ + if (smatch(wp->path, "/")) { + // websRewriteRequest(wp, "/home.html"); + /* Fall through */ + } + LogInfo("sssssssssssssssssssssssssssss url = %s\n", wp->url); + // websSetStatus(wp, HTTP_CODE_OK); + // websWriteHeaders(wp, 0, 0); + // websWriteEndHeaders(wp); + // websWrite(wp, "

"); + // websWrite(wp, "sssssssssssssssssssssss"); + // websWrite(wp, "

\n"); + // websDone(wp); + websSetStatus(wp, 200); + websWriteHeaders(wp, -1, 0); + websWriteHeader(wp, "Content-Type", "text/plain"); + websWriteEndHeaders(wp); + websWrite(wp, "Hello Legacy World\n"); + websDone(wp); + return 1; + return 1; +} +StatusCode WebServerInit(const WebServerParam webParam) { websSetDebug(1); logSetPath("stdout:2"); @@ -55,8 +81,10 @@ StatusCode WebServerInit(void) constexpr int BUF_LENGTH = 128; char routePath[BUF_LENGTH] = {0}; char authPath[BUF_LENGTH] = {0}; + char listen[BUF_LENGTH] = {0}; snprintf(routePath, BUF_LENGTH, "%s/route.txt", GOAHEAD_CONFIG_FILE_PATH); snprintf(authPath, BUF_LENGTH, "%s/auth.txt", GOAHEAD_CONFIG_FILE_PATH); + snprintf(listen, BUF_LENGTH, "%s:%d", webParam.mIp, webParam.mPort); initPlatform(); if (websOpen(documents, routePath) < 0) { LogError("Cannot initialize server. Exiting.\n"); @@ -67,12 +95,24 @@ StatusCode WebServerInit(void) LogError("Cannot load %s", authPath); return CreateStatusCode(STATUS_CODE_NOT_OK); } - if (websListen("192.168.1.189:8080") < 0) { + if (websListen(listen) < 0) { return CreateStatusCode(STATUS_CODE_NOT_OK); } + websDefineHandler("test", 0, testHandler, 0, 0); + websAddRoute("/test", "test", 0); websServiceEvents(&finished); logmsg(1, "Instructed to exit\n"); websClose(); return CreateStatusCode(STATUS_CODE_OK); } -StatusCode WebServerUnInit(void) { return CreateStatusCode(STATUS_CODE_OK); } \ No newline at end of file +StatusCode WebServerExit(void) +{ + LogInfo("Stop goahead web server.\n"); + finished = 1; + return CreateStatusCode(STATUS_CODE_OK); +} +StatusCode WebServerUnInit(void) +{ + LogInfo("WebServerUnInit.\n"); + return CreateStatusCode(STATUS_CODE_OK); +} \ No newline at end of file