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