/* Esp8266 http server - core routines */ /* * ---------------------------------------------------------------------------- * "THE BEER-WARE LICENSE" (Revision 42): * Jeroen Domburg wrote this file. As long as you retain * this notice you can do whatever you want with this stuff. If we meet some day, * and you think this stuff is worth it, you can buy me a beer in return. * ---------------------------------------------------------------------------- */ #include "espmissingincludes.h" #include "c_types.h" #include "user_interface.h" #include "espconn.h" #include "mem.h" #include "osapi.h" #include "espconn.h" #include "httpd.h" #include "io.h" #include "espfs.h" //Max length of request head #define MAX_HEAD_LEN 1024 //Max amount of connections #define MAX_CONN 8 //Max post buffer len #define MAX_POST 1024 //Max send buffer len #define MAX_SENDBUFF_LEN 2048 //This gets set at init time. static HttpdBuiltInUrl *builtInUrls; //Private data for http connection struct HttpdPriv { char head[MAX_HEAD_LEN]; int headPos; char *sendBuff; int sendBuffLen; }; //Connection pool static HttpdPriv connPrivData[MAX_CONN]; static HttpdConnData connData[MAX_CONN]; static HttpdPostData connPostData[MAX_CONN]; //Listening connection data static struct espconn httpdConn; static esp_tcp httpdTcp; //Struct to keep extension->mime data in typedef struct { const char *ext; const char *mimetype; } MimeMap; //The mappings from file extensions to mime types. If you need an extra mime type, //add it here. static const MimeMap mimeTypes[]={ {"htm", "text/htm"}, {"html", "text/html"}, {"css", "text/css"}, {"js", "text/javascript"}, {"txt", "text/plain"}, {"jpg", "image/jpeg"}, {"jpeg", "image/jpeg"}, {"png", "image/png"}, {NULL, "text/html"}, //default value }; // The static files with the following extensions from the HTML folder will be compressed and served with GZIP compression // Add any other file types you want compressed here (and don't forget to modify the Makefile too) #ifdef GZIP_COMPRESSION static const char * gzippedFileTypes[] = { "js", "html", "css", NULL }; static const char gzipNonSupportedMessage[] = "Your browser does not accept gzip-compressed data."; #endif //Returns a static char* to a mime type for a given url to a file. const char ICACHE_FLASH_ATTR *httpdGetMimetype(char *url) { int i=0; //Go find the extension char *ext=url+(strlen(url)-1); while (ext!=url && *ext!='.') ext--; if (*ext=='.') ext++; //ToDo: os_strcmp is case sensitive; we may want to do case-intensive matching here... while (mimeTypes[i].ext!=NULL && os_strcmp(ext, mimeTypes[i].ext)!=0) i++; return mimeTypes[i].mimetype; } #ifdef GZIP_COMPRESSION //Sends Content-encoding header if the requested file was GZIP compressed //If the client does not sent the Accept-encoding, send out a static html message. const char* sendGZIPEncodingIfNeeded(HttpdConnData *connData) { int i=0; char acceptEncodingBuffer[64]; //Go find the extension char *ext=connData->url+(strlen(connData->url)-1); while (ext!=connData->url && *ext!='.') ext--; if (*ext=='.') ext++; //ToDo: os_strcmp is case sensitive; we may want to do case-intensive matching here... while (gzippedFileTypes[i]!=NULL) { if (os_strcmp(ext, gzippedFileTypes[i])==0) { //when serving gzipped files check the browser's "Accept-Encoding" header //if the client does not advertises that he accepts GZIP send a warning message (telnet users for e.g.) httpdGetHeader(connData, "Accept-Encoding", acceptEncodingBuffer, 64); if (os_strstr(acceptEncodingBuffer, "gzip") == NULL) { //No Accept-Encoding: gzip header present return gzipNonSupportedMessage; } else { httpdHeader(connData, "Content-Encoding", "gzip"); return NULL; } } i++; } return NULL; } #endif //Looks up the connData info for a specific esp connection static HttpdConnData ICACHE_FLASH_ATTR *httpdFindConnData(void *arg) { int i; for (i=0; ipost->buff!=NULL) os_free(conn->post->buff); conn->post->buff=NULL; conn->cgi=NULL; conn->conn=NULL; } //Stupid li'l helper function that returns the value of a hex char. static int httpdHexVal(char c) { if (c>='0' && c<='9') return c-'0'; if (c>='A' && c<='F') return c-'A'+10; if (c>='a' && c<='f') return c-'a'+10; return 0; } //Decode a percent-encoded value. //Takes the valLen bytes stored in val, and converts it into at most retLen bytes that //are stored in the ret buffer. Returns the actual amount of bytes used in ret. Also //zero-terminates the ret buffer. int httpdUrlDecode(char *val, int valLen, char *ret, int retLen) { int s=0, d=0; int esced=0, escVal=0; while (spriv->head; p=p+strlen(p)+1; //skip GET/POST part p=p+strlen(p)+1; //skip HTTP part while (p<(conn->priv->head+conn->priv->headPos)) { while(*p<=32 && *p!=0) p++; //skip crap at start //See if this is the header if (os_strncmp(p, header, strlen(header))==0 && p[strlen(header)]==':') { //Skip 'key:' bit of header line p=p+strlen(header)+1; //Skip past spaces after the colon while(*p==' ') p++; //Copy from p to end while (*p!=0 && *p!='\r' && *p!='\n' && retLen>1) { *ret++=*p++; retLen--; } //Zero-terminate string *ret=0; //All done :) return 1; } p+=strlen(p)+1; //Skip past end of string and \0 terminator } return 0; } //Start the response headers. void ICACHE_FLASH_ATTR httpdStartResponse(HttpdConnData *conn, int code) { char buff[128]; int l; l=os_sprintf(buff, "HTTP/1.0 %d OK\r\nServer: esp8266-httpd/"HTTPDVER"\r\n", code); httpdSend(conn, buff, l); } //Send a http header. void ICACHE_FLASH_ATTR httpdHeader(HttpdConnData *conn, const char *field, const char *val) { char buff[256]; int l; l=os_sprintf(buff, "%s: %s\r\n", field, val); httpdSend(conn, buff, l); } //Finish the headers. void ICACHE_FLASH_ATTR httpdEndHeaders(HttpdConnData *conn) { httpdSend(conn, "\r\n", -1); } //ToDo: sprintf->snprintf everywhere... esp doesn't have snprintf tho' :/ //Redirect to the given URL. void ICACHE_FLASH_ATTR httpdRedirect(HttpdConnData *conn, char *newUrl) { char buff[1024]; int l; l=os_sprintf(buff, "HTTP/1.1 302 Found\r\nLocation: %s\r\n\r\nMoved to %s\r\n", newUrl, newUrl); httpdSend(conn, buff, l); } //Use this as a cgi function to redirect one url to another. int ICACHE_FLASH_ATTR cgiRedirect(HttpdConnData *connData) { if (connData->conn==NULL) { //Connection aborted. Clean up. return HTTPD_CGI_DONE; } httpdRedirect(connData, (char*)connData->cgiArg); return HTTPD_CGI_DONE; } //Add data to the send buffer. len is the length of the data. If len is -1 //the data is seen as a C-string. //Returns 1 for success, 0 for out-of-memory. int ICACHE_FLASH_ATTR httpdSend(HttpdConnData *conn, const char *data, int len) { if (len<0) len=strlen(data); if (conn->priv->sendBuffLen+len>MAX_SENDBUFF_LEN) return 0; os_memcpy(conn->priv->sendBuff+conn->priv->sendBuffLen, data, len); conn->priv->sendBuffLen+=len; return 1; } //Helper function to send any data in conn->priv->sendBuff static void ICACHE_FLASH_ATTR xmitSendBuff(HttpdConnData *conn) { if (conn->priv->sendBuffLen!=0) { espconn_sent(conn->conn, (uint8_t*)conn->priv->sendBuff, conn->priv->sendBuffLen); conn->priv->sendBuffLen=0; } } //Callback called when the data on a socket has been successfully //sent. static void ICACHE_FLASH_ATTR httpdSentCb(void *arg) { int r; HttpdConnData *conn=httpdFindConnData(arg); char sendBuff[MAX_SENDBUFF_LEN]; if (conn==NULL) return; conn->priv->sendBuff=sendBuff; conn->priv->sendBuffLen=0; if (conn->cgi==NULL) { //Marked for destruction? os_printf("Conn %p is done. Closing.\n", conn->conn); espconn_disconnect(conn->conn); httpdRetireConn(conn); return; //No need to call xmitSendBuff. } r=conn->cgi(conn); //Execute cgi fn. if (r==HTTPD_CGI_DONE) { conn->cgi=NULL; //mark for destruction. } if (r==HTTPD_CGI_NOTFOUND || r==HTTPD_CGI_AUTHENTICATED) { os_printf("ERROR! CGI fn returns code %d after sending data! Bad CGI!\n", r); conn->cgi=NULL; //mark for destruction. } xmitSendBuff(conn); } static const char *httpNotFoundHeader="HTTP/1.0 404 Not Found\r\nServer: esp8266-httpd/0.1\r\nContent-Type: text/plain\r\nContent-Length: 12\r\n\r\nNot Found.\r\n"; //This is called when the headers have been received and the connection is ready to send //the result headers and data. //We need to find the CGI function to call, call it, and dependent on what it returns either //find the next cgi function, wait till the cgi data is sent or close up the connection. static void ICACHE_FLASH_ATTR httpdProcessRequest(HttpdConnData *conn) { int r; int i=0; if (conn->url==NULL) { os_printf("WtF? url = NULL\n"); return; //Shouldn't happen } //See if we can find a CGI that's happy to handle the request. while (1) { //Look up URL in the built-in URL table. while (builtInUrls[i].url!=NULL) { int match=0; //See if there's a literal match if (os_strcmp(builtInUrls[i].url, conn->url)==0) match=1; //See if there's a wildcard match if (builtInUrls[i].url[os_strlen(builtInUrls[i].url)-1]=='*' && os_strncmp(builtInUrls[i].url, conn->url, os_strlen(builtInUrls[i].url)-1)==0) match=1; if (match) { os_printf("Is url index %d\n", i); conn->cgiData=NULL; conn->cgi=builtInUrls[i].cgiCb; conn->cgiArg=builtInUrls[i].cgiArg; break; } i++; } if (builtInUrls[i].url==NULL) { //Drat, we're at the end of the URL table. This usually shouldn't happen. Well, just //generate a built-in 404 to handle this. os_printf("%s not found. 404!\n", conn->url); httpdSend(conn, httpNotFoundHeader, -1); xmitSendBuff(conn); conn->cgi=NULL; //mark for destruction return; } //Okay, we have a CGI function that matches the URL. See if it wants to handle the //particular URL we're supposed to handle. r=conn->cgi(conn); if (r==HTTPD_CGI_MORE) { //Yep, it's happy to do so and has more data to send. xmitSendBuff(conn); return; } else if (r==HTTPD_CGI_DONE) { //Yep, it's happy to do so and already is done sending data. xmitSendBuff(conn); conn->cgi=NULL; //mark conn for destruction return; } else if (r==HTTPD_CGI_NOTFOUND || r==HTTPD_CGI_AUTHENTICATED) { //URL doesn't want to handle the request: either the data isn't found or there's no //need to generate a login screen. i++; //look at next url the next iteration of the loop. } } } //Parse a line of header data and modify the connection data accordingly. static void ICACHE_FLASH_ATTR httpdParseHeader(char *h, HttpdConnData *conn) { int i; char first_line = false; if (os_strncmp(h, "GET ", 4)==0) { conn->requestType = HTTPD_METHOD_GET; first_line = true; } else if (os_strncmp(h, "POST ", 5)==0) { conn->requestType = HTTPD_METHOD_POST; first_line = true; } if (first_line) { char *e; //Skip past the space after POST/GET i=0; while (h[i]!=' ') i++; conn->url=h+i+1; //Figure out end of url. e=(char*)os_strstr(conn->url, " "); if (e==NULL) return; //wtf? *e=0; //terminate url part os_printf("URL = %s\n", conn->url); //Parse out the URL part before the GET parameters. conn->getArgs=(char*)os_strstr(conn->url, "?"); if (conn->getArgs!=0) { *conn->getArgs=0; conn->getArgs++; os_printf("GET args = %s\n", conn->getArgs); } else { conn->getArgs=NULL; } } else if (os_strncmp(h, "Content-Length:", 15)==0) { i=15; //Skip trailing spaces while (h[i]==' ') i++; //Get POST data length conn->post->len=atoi(h+i); // Allocate the buffer if (conn->post->len > MAX_POST) { // we'll stream this in in chunks conn->post->buffSize = MAX_POST; } else { conn->post->buffSize = conn->post->len; } os_printf("Mallocced buffer for %d + 1 bytes of post data.\n", conn->post->buffSize); conn->post->buff=(char*)os_malloc(conn->post->buffSize + 1); conn->post->buffLen=0; } else if (os_strncmp(h, "Content-Type: ", 14)==0) { if (os_strstr(h, "multipart/form-data")) { // It's multipart form data so let's pull out the boundary for future use char *b; if ((b = os_strstr(h, "boundary=")) != NULL) { conn->post->multipartBoundary = b + 7; // move the pointer 2 chars before boundary then fill them with dashes conn->post->multipartBoundary[0] = '-'; conn->post->multipartBoundary[1] = '-'; os_printf("boundary = %s\n", conn->post->multipartBoundary); } } } } //Callback called when there's data available on a socket. static void ICACHE_FLASH_ATTR httpdRecvCb(void *arg, char *data, unsigned short len) { int x; char *p, *e; char sendBuff[MAX_SENDBUFF_LEN]; HttpdConnData *conn=httpdFindConnData(arg); if (conn==NULL) return; conn->priv->sendBuff=sendBuff; conn->priv->sendBuffLen=0; //This is slightly evil/dirty: we abuse conn->post->len as a state variable for where in the http communications we are: //<0 (-1): Post len unknown because we're still receiving headers //==0: No post data //>0: Need to receive post data //ToDo: See if we can use something more elegant for this. for (x=0; xpost->len<0) { //This byte is a header byte. if (conn->priv->headPos!=MAX_HEAD_LEN) conn->priv->head[conn->priv->headPos++]=data[x]; conn->priv->head[conn->priv->headPos]=0; //Scan for /r/n/r/n. Receiving this indicate the headers end. if (data[x]=='\n' && (char *)os_strstr(conn->priv->head, "\r\n\r\n")!=NULL) { //Indicate we're done with the headers. conn->post->len=0; //Reset url data conn->url=NULL; //Iterate over all received headers and parse them. p=conn->priv->head; while(p<(&conn->priv->head[conn->priv->headPos-4])) { e=(char *)os_strstr(p, "\r\n"); //Find end of header line if (e==NULL) break; //Shouldn't happen. e[0]=0; //Zero-terminate header httpdParseHeader(p, conn); //and parse it. p=e+2; //Skip /r/n (now /0/n) } //If we don't need to receive post data, we can send the response now. if (conn->post->len==0) { httpdProcessRequest(conn); } } } else if (conn->post->len!=0) { //This byte is a POST byte. conn->post->buff[conn->post->buffLen++]=data[x]; conn->post->received++; if (conn->post->buffLen >= conn->post->buffSize || conn->post->received == conn->post->len) { //Received a chunk of post data conn->post->buff[conn->post->buffLen]=0; //zero-terminate, in case the cgi handler knows it can use strings //Send the response. httpdProcessRequest(conn); conn->post->buffLen = 0; } } } } static void ICACHE_FLASH_ATTR httpdReconCb(void *arg, sint8 err) { HttpdConnData *conn=httpdFindConnData(arg); os_printf("ReconCb\n"); if (conn==NULL) return; //Yeah... No idea what to do here. ToDo: figure something out. } static void ICACHE_FLASH_ATTR httpdDisconCb(void *arg) { //The esp sdk passes through wrong arg here, namely the one of the *listening* socket. //That is why we can't use (HttpdConnData)arg->sock here. //Just look at all the sockets and kill the slot if needed. int i; for (i=0; i=ESPCONN_CLOSE and not ==? Well, seems the stack sometimes de-allocates //espconns under our noses, especially when connections are interrupted. The memory //is then used for something else, and we can use that to capture *most* of the //disconnect cases. if (connData[i].conn->state==ESPCONN_NONE || connData[i].conn->state>=ESPCONN_CLOSE) { connData[i].conn=NULL; if (connData[i].cgi!=NULL) connData[i].cgi(&connData[i]); //flush cgi data httpdRetireConn(&connData[i]); } } } } static void ICACHE_FLASH_ATTR httpdConnectCb(void *arg) { struct espconn *conn=arg; int i; //Find empty conndata in pool for (i=0; iheadPos=0; connData[i].post=&connPostData[i]; connData[i].post->buff=NULL; connData[i].post->buffLen=0; connData[i].post->received=0; connData[i].post->len=-1; espconn_regist_recvcb(conn, httpdRecvCb); espconn_regist_reconcb(conn, httpdReconCb); espconn_regist_disconcb(conn, httpdDisconCb); espconn_regist_sentcb(conn, httpdSentCb); } //Httpd initialization routine. Call this to kick off webserver functionality. void ICACHE_FLASH_ATTR httpdInit(HttpdBuiltInUrl *fixedUrls, int port) { int i; for (i=0; i