diff --git a/src/ESPAsyncWebServer.h b/src/ESPAsyncWebServer.h index 0f9b646..bee5e07 100644 --- a/src/ESPAsyncWebServer.h +++ b/src/ESPAsyncWebServer.h @@ -81,10 +81,11 @@ class AsyncWebHeader { AsyncWebHeader(String name, String value): _name(name), _value(value), next(NULL){} AsyncWebHeader(String data): _name(), _value(), next(NULL){ - if(!data || !data.length() || data.indexOf(':') < 0) - return; - _name = data.substring(0, data.indexOf(':')); - _value = data.substring(data.indexOf(':') + 2); + if(!data) return; + int index = data.indexOf(':'); + if (index < 0) return; + _name = data.substring(0, index); + _value = data.substring(index + 2); } ~AsyncWebHeader(){} String name(){ return _name; } @@ -149,7 +150,6 @@ class AsyncWebServerRequest { bool _parseReqHead(); bool _parseReqHeader(); void _parseLine(); - void _parseByte(uint8_t data); void _parsePlainPostChar(uint8_t data); void _parseMultipartPostByte(uint8_t data, bool last); void _addGetParam(String param); @@ -160,6 +160,7 @@ class AsyncWebServerRequest { public: File _tempFile; + void *_tempObject; AsyncWebServerRequest *next; AsyncWebServerRequest(AsyncWebServer*, AsyncClient*); @@ -185,12 +186,14 @@ class AsyncWebServerRequest { void send(AsyncWebServerResponse *response); void send(int code, String contentType=String(), String content=String()); void send(FS &fs, String path, String contentType=String(), bool download=false); + void send(File content, String path, String contentType=String(), bool download=false); void send(Stream &stream, String contentType, size_t len); void send(String contentType, size_t len, AwsResponseFiller callback); void sendChunked(String contentType, AwsResponseFiller callback); AsyncWebServerResponse *beginResponse(int code, String contentType=String(), String content=String()); AsyncWebServerResponse *beginResponse(FS &fs, String path, String contentType=String(), bool download=false); + AsyncWebServerResponse *beginResponse(File content, String path, String contentType=String(), bool download=false); AsyncWebServerResponse *beginResponse(Stream &stream, String contentType, size_t len); AsyncWebServerResponse *beginResponse(String contentType, size_t len, AwsResponseFiller callback); AsyncWebServerResponse *beginChunkedResponse(String contentType, AwsResponseFiller callback); diff --git a/src/WebHandlerImpl.h b/src/WebHandlerImpl.h index 5a7dc36..158cb60 100644 --- a/src/WebHandlerImpl.h +++ b/src/WebHandlerImpl.h @@ -24,47 +24,24 @@ #include "stddef.h" -class RedirectWebHandler: public AsyncWebHandler { - protected: - String _url; - String _location; - uint32_t _exclude_ip; - public: - RedirectWebHandler(const char* url, const char* location, uint32_t exclude_ip) - : _url(url), _location(location) { _exclude_ip = exclude_ip; } - bool canHandle(AsyncWebServerRequest *request); - void handleRequest(AsyncWebServerRequest *request); -}; - class AsyncStaticWebHandler: public AsyncWebHandler { private: - String _getPath(AsyncWebServerRequest *request); + bool _getFile(AsyncWebServerRequest *request); + bool _fileExists(AsyncWebServerRequest *request, const String path); + uint8_t _countBits(const uint8_t value); protected: FS _fs; String _uri; String _path; String _cache_header; - String _modified_header; - bool _isFile; + bool _isDir; + bool _gzipFirst; + uint8_t _gzipStats; + uint8_t _fileStats; public: - AsyncStaticWebHandler(FS& fs, const char* path, const char* uri, const char* cache_header, const char* modified_header) - : _fs(fs), _uri(uri), _path(path), _cache_header(cache_header), _modified_header(modified_header) { - - _isFile = _fs.exists(path) || _fs.exists((String(path)+".gz").c_str()); - if (_uri != "/" && _uri.endsWith("/")) { - _uri = _uri.substring(0, _uri.length() - 1); - DEBUGF("[AsyncStaticWebHandler] _uri / removed\n"); - } - if (_path != "/" && _path.endsWith("/")) { - _path = _path.substring(0, _path.length() - 1); - DEBUGF("[AsyncStaticWebHandler] _path / removed\n"); - } - - - } + AsyncStaticWebHandler(FS& fs, const char* path, const char* uri, const char* cache_header); bool canHandle(AsyncWebServerRequest *request); void handleRequest(AsyncWebServerRequest *request); - }; class AsyncCallbackWebHandler: public AsyncWebHandler { diff --git a/src/WebHandlers.cpp b/src/WebHandlers.cpp index af5d7ff..94483ae 100644 --- a/src/WebHandlers.cpp +++ b/src/WebHandlers.cpp @@ -21,102 +21,121 @@ #include "ESPAsyncWebServer.h" #include "WebHandlerImpl.h" - -bool RedirectWebHandler::canHandle(AsyncWebServerRequest *request) +AsyncStaticWebHandler::AsyncStaticWebHandler(FS& fs, const char* path, const char* uri, const char* cache_header) + : _fs(fs), _uri(uri), _path(path), _cache_header(cache_header) { - // We can redirect when the request url match and ip doesn't match - if (request->url() == _url && _exclude_ip != request->client()->localIP()) { - DEBUGF("[RedirectWebHandler::canHandle] TRUE\n"); - return true; - } - return false; -} + // Ensure leading '/' + if (_uri.length() == 0 || _uri[0] != '/') _uri = "/" + _uri; + if (_path.length() == 0 || _path[0] != '/') _path = "/" + _path; -void RedirectWebHandler::handleRequest(AsyncWebServerRequest *request) -{ - AsyncWebServerResponse *response = request->beginResponse(302); - response->addHeader("Location", _location); - request->send(response); -} + // If uri or path ends with '/' we assume a hint that this is a directory to improve performance. + // However - if they both do not end '/' we, can't assume they are files, they can still be directory. + bool isUriDir = _uri[_uri.length()-1] == '/'; + bool isPathDir = _path[_path.length()-1] == '/'; + _isDir = isUriDir || isPathDir; + // If we serving directory - remove the trailing '/' so we can handle default file + // Notice that root will be "" not "/" + if (_isDir && isUriDir) _uri = _uri.substring(0, _uri.length()-1); + if (_isDir && isPathDir) _path = _path.substring(0, _path.length()-1); + + // Reset stats + _gzipFirst = false; + _gzipStats = 0; + _fileStats = 0; +} bool AsyncStaticWebHandler::canHandle(AsyncWebServerRequest *request) { - if (request->method() != HTTP_GET) { - return false; - } - if ((_isFile && request->url() != _uri) ) { - return false; - } - // if the root of the request matches the _uri then it checks to see if there is a file it can handle. - if (request->url().startsWith(_uri)) { - String path = _getPath(request); - if (_fs.exists(path) || _fs.exists(path + ".gz")) { - if (_modified_header.length() != 0) { - request->addInterestingHeader("If-Modified-Since"); - } - DEBUGF("[AsyncStaticWebHandler::canHandle] TRUE\n"); - return true; - } + if (request->method() == HTTP_GET && + request->url().startsWith(_uri) && + _getFile(request)) { + + DEBUGF("[AsyncStaticWebHandler::canHandle] TRUE\n"); + return true; } return false; } -String AsyncStaticWebHandler::_getPath(AsyncWebServerRequest *request) +bool AsyncStaticWebHandler::_getFile(AsyncWebServerRequest *request) { + // Remove the found uri + String path = request->url().substring(_uri.length()); - String path = request->url(); - DEBUGF("[AsyncStaticWebHandler::_getPath]\n"); - DEBUGF(" [stored] _uri = %s, _path = %s\n" , _uri.c_str(), _path.c_str() ) ; - DEBUGF(" [request] url = %s\n", request->url().c_str() ); + // We can skip the file check if we serving a directory and (we have full match or we end with '/') + bool canSkipFileCheck = _isDir && (path.length() == 0 || path[path.length()-1] == '/'); - if (!_isFile) { - DEBUGF(" _isFile = false\n"); - String baserequestUrl = request->url().substring(_uri.length()); // this is the request - stored _uri... /espman/ - DEBUGF(" baserequestUrl = %s\n", baserequestUrl.c_str()); + path = _path + path; - if (!baserequestUrl.length()) { - baserequestUrl += "/"; - } + // Do we have a file or .gz file + if (!canSkipFileCheck && _fileExists(request, path)) + return true; - path = _path + baserequestUrl; - DEBUGF(" path = path + baserequestUrl, path = %s\n", path.c_str()); + // Try to add default page, ensure there is a trailing '/' ot the path. + if (path.length() == 0 || path[path.length()-1] != '/') + path += "/"; + path += "index.htm"; - if (path.endsWith("/")) { - DEBUGF(" 3 path ends with / : path = index.htm \n"); - path += "index.htm"; - } - } else { - path = _path; - } - - DEBUGF(" final path = %s\n", path.c_str()); - DEBUGF("[AsyncStaticWebHandler::_getPath] END\n\n"); - - return path; + return _fileExists(request, path); } +bool AsyncStaticWebHandler::_fileExists(AsyncWebServerRequest *request, const String path) +{ + bool fileFound = false; + bool gzipFound = false; + + String gzip = path + ".gz"; + + if (_gzipFirst) { + request->_tempFile = _fs.open(gzip, "r"); + gzipFound = request->_tempFile == true; + if (!gzipFound){ + request->_tempFile = _fs.open(path, "r"); + fileFound = request->_tempFile == true; + } + } else { + request->_tempFile = _fs.open(path, "r"); + fileFound = request->_tempFile == true; + if (!fileFound){ + request->_tempFile = _fs.open(gzip, "r"); + gzipFound = request->_tempFile == true; + } + } + + bool found = fileFound || gzipFound; + + if (found) { + size_t plen = path.length(); + char * _tempPath = (char*)malloc(plen+1); + snprintf(_tempPath, plen+1, "%s", path.c_str()); + request->_tempObject = (void*)_tempPath; + _gzipStats = (_gzipStats << 1) + gzipFound ? 1 : 0; + _fileStats = (_fileStats << 1) + fileFound ? 1 : 0; + _gzipFirst = _countBits(_gzipStats) > _countBits(_fileStats); + } + + return found; +} + +uint8_t AsyncStaticWebHandler::_countBits(const uint8_t value) +{ + uint8_t w = value; + uint8_t n; + for (n=0; w!=0; n++) w&=w-1; + return n; +} void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) { - - String path = _getPath(request); - - if (_fs.exists(path) || _fs.exists(path + ".gz")) { - if (_modified_header.length() != 0 && _modified_header == request->header("If-Modified-Since")) { - request->send(304); // Sed not modified - } else { - AsyncWebServerResponse * response = request->beginResponse(_fs, path); - if (_modified_header.length() !=0) - response->addHeader("Last-Modified", _modified_header); - if (_cache_header.length() != 0) - response->addHeader("Cache-Control", _cache_header); - request->send(response); - } + if (request->_tempFile == true) { + AsyncWebServerResponse * response = new AsyncFileResponse(request->_tempFile, String((char*)request->_tempObject)); + free(request->_tempObject); + request->_tempObject = NULL; + if (_cache_header.length() != 0) + response->addHeader("Cache-Control", _cache_header); + request->send(response); } else { request->send(404); } - path = String(); - } diff --git a/src/WebRequest.cpp b/src/WebRequest.cpp index f1e8ffa..617f8f1 100644 --- a/src/WebRequest.cpp +++ b/src/WebRequest.cpp @@ -63,6 +63,7 @@ AsyncWebServerRequest::AsyncWebServerRequest(AsyncWebServer* s, AsyncClient* c) , _itemBuffer(0) , _itemBufferIndex(0) , _itemIsFile(false) + , _tempObject(NULL) , next(NULL) { c->onError([](void *r, AsyncClient* c, int8_t error){ AsyncWebServerRequest *req = (AsyncWebServerRequest*)r; req->_onError(error); }, this); @@ -93,16 +94,30 @@ AsyncWebServerRequest::~AsyncWebServerRequest(){ delete _response; } + if(_tempObject != NULL){ + free(_tempObject); + } + } void AsyncWebServerRequest::_onData(void *buf, size_t len){ if(_parseState < PARSE_REQ_BODY){ - size_t i; - for(i=0; ihandleRequest(this); else send(501); - return; } } } @@ -196,16 +210,23 @@ void AsyncWebServerRequest::_addGetParam(String param){ param = urlDecode(param); String name = param; String value = ""; - if(param.indexOf('=') > 0){ - name = param.substring(0, param.indexOf('=')); - value = param.substring(param.indexOf('=') + 1); + int index = param.indexOf('='); + if(index > 0){ + name = param.substring(0, index); + value = param.substring(index + 1); } _addParam(new AsyncWebParameter(name, value)); } bool AsyncWebServerRequest::_parseReqHead(){ - String m = _temp.substring(0, _temp.indexOf(' ')); + // Split the head into method, url and version + int index = _temp.indexOf(' '); + String m = _temp.substring(0, index); + index = _temp.indexOf(' ', index+1); + String u = _temp.substring(m.length()+1, index); + _temp = _temp.substring(index+1); + if(m == "GET"){ _method = HTTP_GET; } else if(m == "POST"){ @@ -222,22 +243,22 @@ bool AsyncWebServerRequest::_parseReqHead(){ _method = HTTP_OPTIONS; } - _temp = _temp.substring(_temp.indexOf(' ')+1); - String u = _temp.substring(0, _temp.indexOf(' ')); u = urlDecode(u); String g = String(); - if(u.indexOf('?') > 0){ - g = u.substring(u.indexOf('?') + 1); - u = u.substring(0, u.indexOf('?')); + index = u.indexOf('?'); + if(index > 0){ + g = u.substring(index+1); + u = u.substring(0, index); } _url = u; if(g.length()){ while(true){ if(g.length() == 0) break; - if(g.indexOf('&') > 0){ - _addGetParam(g.substring(0, g.indexOf('&'))); - g = g.substring(g.indexOf('&') + 1); + index = g.indexOf('&'); + if(index > 0){ + _addGetParam(g.substring(0, index)); + g = g.substring(index+1); } else { _addGetParam(g); break; @@ -245,7 +266,6 @@ bool AsyncWebServerRequest::_parseReqHead(){ } } - _temp = _temp.substring(_temp.indexOf(' ')+1); if(_temp.startsWith("HTTP/1.1")) _version = 1; _temp = String(); @@ -253,36 +273,32 @@ bool AsyncWebServerRequest::_parseReqHead(){ } bool AsyncWebServerRequest::_parseReqHeader(){ - if(_temp.indexOf(':')){ - AsyncWebHeader *h = new AsyncWebHeader(_temp); - if(h == NULL) - return false; - if(h->name() == "Host"){ - _host = h->value(); - delete h; + int index = _temp.indexOf(':'); + if(index){ + String name = _temp.substring(0, index); + String value = _temp.substring(index + 2); + if(name == "Host"){ + _host = value; _server->_handleRequest(this); - } else if(h->name() == "Content-Type"){ - if (h->value().startsWith("multipart/")){ - _boundary = h->value().substring(h->value().indexOf('=')+1); - _contentType = h->value().substring(0, h->value().indexOf(';')); + } else if(name == "Content-Type"){ + if (value.startsWith("multipart/")){ + _boundary = value.substring(value.indexOf('=')+1); + _contentType = value.substring(0, value.indexOf(';')); _isMultipart = true; } else { - _contentType = h->value(); + _contentType = value; } - delete h; - } else if(h->name() == "Content-Length"){ - _contentLength = atoi(h->value().c_str()); - delete h; - } else if(h->name() == "Expect" && h->value() == "100-continue"){ + } else if(name == "Content-Length"){ + _contentLength = atoi(value.c_str()); + } else if(name == "Expect" && value == "100-continue"){ _expectingContinue = true; - delete h; - } else if(h->name() == "Authorization"){ - if(h->value().startsWith("Basic")){ - _authorization = h->value().substring(6); + } else if(name == "Authorization"){ + if(value.startsWith("Basic")){ + _authorization = value.substring(6); } - delete h; } else { - if(_interestingHeaders->contains(h->name()) || _interestingHeaders->contains("ANY")){ + if(_interestingHeaders->contains(name) || _interestingHeaders->contains("ANY")){ + AsyncWebHeader *h = new AsyncWebHeader(name, value); if(_headers == NULL) _headers = h; else { @@ -290,10 +306,8 @@ bool AsyncWebServerRequest::_parseReqHeader(){ while(hs->next != NULL) hs = hs->next; hs->next = h; } - } else - delete h; + } } - } _temp = String(); return true; @@ -527,14 +541,6 @@ void AsyncWebServerRequest::_parseLine(){ } } -void AsyncWebServerRequest::_parseByte(uint8_t data){ - if((char)data != '\r' && (char)data != '\n') - _temp += (char)data; - if((char)data == '\n') - _parseLine(); -} - - int AsyncWebServerRequest::headers(){ int i = 0; @@ -646,6 +652,12 @@ AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(FS &fs, String pat return NULL; } +AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(File content, String path, String contentType, bool download){ + if(content == true) + return new AsyncFileResponse(content, path, contentType, download); + return NULL; +} + AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(Stream &stream, String contentType, size_t len){ return new AsyncStreamResponse(stream, contentType, len); } @@ -674,6 +686,12 @@ void AsyncWebServerRequest::send(FS &fs, String path, String contentType, bool d } else send(404); } +void AsyncWebServerRequest::send(File content, String path, String contentType, bool download){ + if(content == true){ + send(beginResponse(content, path, contentType, download)); + } else send(404); +} + void AsyncWebServerRequest::send(Stream &stream, String contentType, size_t len){ send(beginResponse(stream, contentType, len)); } @@ -776,26 +794,24 @@ bool AsyncWebServerRequest::hasHeader(const char* name){ String AsyncWebServerRequest::urlDecode(const String& text){ - String decoded = ""; char temp[] = "0x00"; unsigned int len = text.length(); unsigned int i = 0; + String decoded = String(); + decoded.reserve(len); // Allocate the string internal buffer - never longer from source text while (i < len){ char decodedChar; char encodedChar = text.charAt(i++); if ((encodedChar == '%') && (i + 1 < len)){ temp[2] = text.charAt(i++); temp[3] = text.charAt(i++); - decodedChar = strtol(temp, NULL, 16); + } else if (encodedChar == '+') { + decodedChar = ' '; } else { - if (encodedChar == '+'){ - decodedChar = ' '; - } else { - decodedChar = encodedChar; // normal ascii char - } + decodedChar = encodedChar; // normal ascii char } - decoded += decodedChar; + decoded.concat(decodedChar); } return decoded; } diff --git a/src/WebResponseImpl.h b/src/WebResponseImpl.h index 59400f6..c7f539b 100644 --- a/src/WebResponseImpl.h +++ b/src/WebResponseImpl.h @@ -48,6 +48,7 @@ class AsyncFileResponse: public AsyncAbstractResponse { void _setContentType(String path); public: AsyncFileResponse(FS &fs, String path, String contentType=String(), bool download=false); + AsyncFileResponse(File content, String path, String contentType=String(), bool download=false); ~AsyncFileResponse(); bool _sourceValid(){ return !!(_content); } size_t _fillBuffer(uint8_t *buf, size_t maxLen); diff --git a/src/WebResponses.cpp b/src/WebResponses.cpp index e063c70..5eead49 100644 --- a/src/WebResponses.cpp +++ b/src/WebResponses.cpp @@ -119,21 +119,31 @@ String AsyncWebServerResponse::_assembleHead(uint8_t version){ if(_chunked) addHeader("Transfer-Encoding","chunked"); } - String out = "HTTP/1." + String(version) + " " + String(_code) + " " + _responseCodeToString(_code) + "\r\n"; - if(_sendContentLength) - out += "Content-Length: " + String(_contentLength) + "\r\n"; + String out = String(); + int bufSize = 300; + char buf[bufSize]; - if(_contentType.length()) - out += "Content-Type: " + _contentType + "\r\n"; + snprintf(buf, bufSize, "HTTP/1.%d %d %s\r\n", version, _code, _responseCodeToString(_code)); + out.concat(buf); + + if(_sendContentLength) { + snprintf(buf, bufSize, "Content-Length: %d\r\n", _contentLength); + out.concat(buf); + } + if(_contentType.length()) { + snprintf(buf, bufSize, "Content-Type: %s\r\n", _contentType.c_str()); + out.concat(buf); + } AsyncWebHeader *h; while(_headers != NULL){ h = _headers; _headers = _headers->next; - out += h->toString(); + snprintf(buf, bufSize, "%s: %s\r\n", h->name().c_str(), h->value().c_str()); + out.concat(buf); delete h; } - out += "\r\n"; + out.concat("\r\n"); _headLength = out.length(); return out; } @@ -158,7 +168,6 @@ AsyncBasicResponse::AsyncBasicResponse(int code, String contentType, String cont _contentType = "text/plain"; } addHeader("Connection","close"); - addHeader("Access-Control-Allow-Origin","*"); } void AsyncBasicResponse::_respond(AsyncWebServerRequest *request){ @@ -228,7 +237,6 @@ size_t AsyncBasicResponse::_ack(AsyncWebServerRequest *request, size_t len, uint void AsyncAbstractResponse::_respond(AsyncWebServerRequest *request){ addHeader("Connection","close"); - addHeader("Access-Control-Allow-Origin","*"); _head = _assembleHead(request->version()); _state = RESPONSE_HEADERS; _ack(request, 0, 0); @@ -333,7 +341,6 @@ void AsyncFileResponse::_setContentType(String path){ if (path.endsWith(".html")) _contentType = "text/html"; else if (path.endsWith(".htm")) _contentType = "text/html"; else if (path.endsWith(".css")) _contentType = "text/css"; - else if (path.endsWith(".txt")) _contentType = "text/plain"; else if (path.endsWith(".js")) _contentType = "application/javascript"; else if (path.endsWith(".png")) _contentType = "image/png"; else if (path.endsWith(".gif")) _contentType = "image/gif"; @@ -344,23 +351,64 @@ void AsyncFileResponse::_setContentType(String path){ else if (path.endsWith(".pdf")) _contentType = "application/pdf"; else if (path.endsWith(".zip")) _contentType = "application/zip"; else if(path.endsWith(".gz")) _contentType = "application/x-gzip"; - else _contentType = "application/octet-stream"; + else _contentType = "text/plain"; } AsyncFileResponse::AsyncFileResponse(FS &fs, String path, String contentType, bool download){ _code = 200; _path = path; + _content = fs.open(_path, "r"); + _contentLength = _content.size(); + if(!download && !fs.exists(_path) && fs.exists(_path+".gz")){ _path = _path+".gz"; addHeader("Content-Encoding", "gzip"); } - - if(download) - _contentType = "application/octet-stream"; - else + + if(contentType == "") _setContentType(path); - _content = fs.open(_path, "r"); + else + _contentType = contentType; + + int filenameStart = path.lastIndexOf('/') + 1; + char buf[26+path.length()-filenameStart]; + char* filename = (char*)path.c_str() + filenameStart; + + if(download) { + // set filename and force download + snprintf(buf, sizeof (buf), "attachment; filename=\"%s\"", filename); + } else { + // set filename and force rendering + snprintf(buf, sizeof (buf), "inline; filename=\"%s\"", filename); + } + addHeader("Content-Disposition", buf); + +} + +AsyncFileResponse::AsyncFileResponse(File content, String path, String contentType, bool download){ + _code = 200; + _path = path; + _content = content; _contentLength = _content.size(); + + if(!download && String(_content.name()).endsWith(".gz")) + addHeader("Content-Encoding", "gzip"); + + if(contentType == "") + _setContentType(path); + else + _contentType = contentType; + + int filenameStart = path.lastIndexOf('/') + 1; + char buf[26+path.length()-filenameStart]; + char* filename = (char*)path.c_str() + filenameStart; + + if(download) { + snprintf(buf, sizeof (buf), "attachment; filename=\"%s\"", filename); + } else { + snprintf(buf, sizeof (buf), "inline; filename=\"%s\"", filename); + } + addHeader("Content-Disposition", buf); } size_t AsyncFileResponse::_fillBuffer(uint8_t *data, size_t len){ @@ -458,4 +506,3 @@ size_t AsyncResponseStream::write(const uint8_t *data, size_t len){ size_t AsyncResponseStream::write(uint8_t data){ return write(&data, 1); } -