diff --git a/.travis.yml b/.travis.yml index 837dd8e..b4afa8e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,9 +35,9 @@ script: - arduino --board esp8266com:esp8266:generic --save-prefs - arduino --get-pref sketchbook.path - build_sketches arduino $HOME/Arduino/libraries/ESPAsyncWebServer esp8266 - - arduino --board espressif:ESP31B:esp31b --save-prefs - - arduino --get-pref sketchbook.path - - build_sketches arduino $HOME/Arduino/libraries/ESPAsyncWebServer esp31b +# - arduino --board espressif:ESP31B:esp31b --save-prefs +# - arduino --get-pref sketchbook.path +# - build_sketches arduino $HOME/Arduino/libraries/ESPAsyncWebServer esp31b notifications: email: diff --git a/README.md b/README.md index 5e52ec1..8eb1db6 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,11 @@ To use this library you need to have the latest git versions of either [ESP8266] - When you send the response, you are immediately ready to handle other connections while the server is taking care of sending the response in the background - Speed is OMG -- Easy to use API, HTTP Basic Authentication, ChunkedResponse +- Easy to use API, HTTP Basic and Digest MD5 Authentication (default), ChunkedResponse - Easily extendible to handle any type of content - Supports Continue 100 - Async WebSocket plugin offering different locations without extra servers or ports -- Async EventSource (ServerSideEvents) plugin to send events to the browser +- Async EventSource (Server-Sent Events) plugin to send events to the browser - URL Rewrite plugin for conditional and permanent url rewrites - ServeStatic plugin that supports cache, Last-Modified, default index and more @@ -668,7 +668,7 @@ client->binary(flash_binary, 4); ``` ## Async Event Source Plugin -The server includes EventSource (ServerSideEvents) plugin which can be used to send short text events to the browser. +The server includes EventSource (Server-Sent Events) plugin which can be used to send short text events to the browser. Difference between EventSource and WebSockets is that EventSource is single direction, text-only protocol. @@ -679,7 +679,7 @@ Difference between EventSource and WebSockets is that EventSource is single dire AsyncWebServer server(80); AsyncWebSocket ws("/ws"); // access at ws://[esp ip]/ws -AsyncEventSource events("/events"); // event source (server side events) +AsyncEventSource events("/events"); // event source (Server-Sent events) const char* ssid = "your-ssid"; const char* password = "your-pass"; diff --git a/src/ESPAsyncWebServer.h b/src/ESPAsyncWebServer.h index 6c57ead..596dbae 100644 --- a/src/ESPAsyncWebServer.h +++ b/src/ESPAsyncWebServer.h @@ -129,6 +129,7 @@ class AsyncWebServerRequest { String _contentType; String _boundary; String _authorization; + bool _isDigest; bool _isMultipart; bool _isPlainPost; bool _expectingContinue; @@ -188,9 +189,13 @@ class AsyncWebServerRequest { bool multipart(){ return _isMultipart; } const char * methodToString(); - bool authenticate(const char * username, const char * password); + + //hash is the string representation of: + // base64(user:pass) for basic or + // user:realm:md5(user:realm:pass) for digest bool authenticate(const char * hash); - void requestAuthentication(); + bool authenticate(const char * username, const char * password, const char * realm = NULL, bool passwordIsHash = false); + void requestAuthentication(const char * realm = NULL, bool isDigest = true); void setHandler(AsyncWebHandler *handler){ _handler = handler; } void addInterestingHeader(String name); diff --git a/src/WebAuthentication.cpp b/src/WebAuthentication.cpp new file mode 100644 index 0000000..cb261b2 --- /dev/null +++ b/src/WebAuthentication.cpp @@ -0,0 +1,214 @@ +/* + Asynchronous WebServer library for Espressif MCUs + + Copyright (c) 2016 Hristo Gochkov. All rights reserved. + This file is part of the esp8266 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#include "WebAuthentication.h" +#include +#include "md5.h" + +// Basic Auth hash = base64("username:password") + +bool checkBasicAuthentication(const char * hash, const char * username, const char * password){ + if(username == NULL || password == NULL || hash == NULL) + return false; + + size_t toencodeLen = os_strlen(username)+os_strlen(password)+1; + size_t encodedLen = base64_encode_expected_len(toencodeLen); + if(strlen(hash) != encodedLen) + return false; + + char *toencode = new char[toencodeLen+1]; + if(toencode == NULL){ + return false; + } + char *encoded = new char[base64_encode_expected_len(toencodeLen)+1]; + if(encoded == NULL){ + delete[] toencode; + return false; + } + sprintf(toencode, "%s:%s", username, password); + if(base64_encode_chars(toencode, toencodeLen, encoded) > 0 && memcmp(hash, encoded, encodedLen) == 0){ + delete[] toencode; + delete[] encoded; + return true; + } + delete[] toencode; + delete[] encoded; + return false; +} + +static bool getMD5(uint8_t * data, uint16_t len, char * output){//33 bytes or more + md5_context_t _ctx; + uint8_t i; + uint8_t * _buf = (uint8_t*)malloc(16); + if(_buf == NULL) + return false; + memset(_buf, 0x00, 16); + MD5Init(&_ctx); + MD5Update(&_ctx, data, len); + MD5Final(_buf, &_ctx); + for(i = 0; i < 16; i++) { + sprintf(output + (i * 2), "%02x", _buf[i]); + } + free(_buf); + return true; +} + +static String genRandomMD5(){ + uint32_t r = RANDOM_REG32; + char * out = (char*)malloc(33); + if(out == NULL || !getMD5((uint8_t*)(&r), 4, out)) + return ""; + String res = String(out); + free(out); + return res; +} + +static String stringMD5(String in){ + char * out = (char*)malloc(33); + if(out == NULL || !getMD5((uint8_t*)(in.c_str()), in.length(), out)) + return ""; + String res = String(out); + free(out); + return res; +} + +String generateDigestHash(const char * username, const char * password, const char * realm){ + if(username == NULL || password == NULL || realm == NULL){ + return ""; + } + char * out = (char*)malloc(33); + String res = String(username); + res.concat(":"); + res.concat(realm); + res.concat(":"); + String in = res; + in.concat(password); + if(out == NULL || !getMD5((uint8_t*)(in.c_str()), in.length(), out)) + return ""; + res.concat(out); + free(out); + return res; +} + +String requestDigestAuthentication(const char * realm){ + String header = "realm=\""; + if(realm == NULL) + header.concat("asyncesp"); + else + header.concat(realm); + header.concat( "\", qop=\"auth\", nonce=\""); + header.concat(genRandomMD5()); + header.concat("\", opaque=\""); + header.concat(genRandomMD5()); + header.concat("\""); + return header; +} + +bool checkDigestAuthentication(const char * header, const char * method, const char * username, const char * password, const char * realm, bool passwordIsHash, const char * nonce, const char * opaque, const char * uri){ + if(username == NULL || password == NULL || header == NULL || method == NULL){ + //os_printf("AUTH FAIL: missing requred fields\n"); + return false; + } + + String myHeader = String(header); + int nextBreak = myHeader.indexOf(", "); + if(nextBreak < 0){ + //os_printf("AUTH FAIL: no variables\n"); + return false; + } + + String myUsername = String(); + String myRealm = String(); + String myNonce = String(); + String myUri = String(); + String myResponse = String(); + String myQop = String(); + String myNc = String(); + String myCnonce = String(); + + myHeader += ", "; + do { + String avLine = myHeader.substring(0, nextBreak); + myHeader = myHeader.substring(nextBreak+2); + nextBreak = myHeader.indexOf(", "); + + int eqSign = avLine.indexOf("="); + if(eqSign < 0){ + //os_printf("AUTH FAIL: no = sign\n"); + return false; + } + String varName = avLine.substring(0, eqSign); + avLine = avLine.substring(eqSign + 1); + if(avLine.startsWith("\"")){ + avLine = avLine.substring(1, avLine.length() - 1); + } + + if(varName.equals("username")){ + if(!avLine.equals(username)){ + //os_printf("AUTH FAIL: username\n"); + return false; + } + myUsername = avLine; + } else if(varName.equals("realm")){ + if(realm != NULL && !avLine.equals(realm)){ + //os_printf("AUTH FAIL: realm\n"); + return false; + } + myRealm = avLine; + } else if(varName.equals("nonce")){ + if(nonce != NULL && !avLine.equals(nonce)){ + //os_printf("AUTH FAIL: nonce\n"); + return false; + } + myNonce = avLine; + } else if(varName.equals("opaque")){ + if(opaque != NULL && !avLine.equals(opaque)){ + //os_printf("AUTH FAIL: opaque\n"); + return false; + } + } else if(varName.equals("uri")){ + if(uri != NULL && !avLine.equals(uri)){ + //os_printf("AUTH FAIL: uri\n"); + return false; + } + myUri = avLine; + } else if(varName.equals("response")){ + myResponse = avLine; + } else if(varName.equals("qop")){ + myQop = avLine; + } else if(varName.equals("nc")){ + myNc = avLine; + } else if(varName.equals("cnonce")){ + myCnonce = avLine; + } + } while(nextBreak > 0); + + String ha1 = (passwordIsHash) ? String(password) : myUsername + ":" + myRealm + ":" + String(password); + String ha2 = String(method) + ":" + myUri; + String response = stringMD5(ha1) + ":" + myNonce + ":" + myNc + ":" + myCnonce + ":" + myQop + ":" + stringMD5(ha2); + + if(myResponse.equals(stringMD5(response))){ + //os_printf("AUTH SUCCESS\n"); + return true; + } + + //os_printf("AUTH FAIL: password\n"); + return false; +} diff --git a/src/WebAuthentication.h b/src/WebAuthentication.h new file mode 100644 index 0000000..ff68265 --- /dev/null +++ b/src/WebAuthentication.h @@ -0,0 +1,34 @@ +/* + Asynchronous WebServer library for Espressif MCUs + + Copyright (c) 2016 Hristo Gochkov. All rights reserved. + This file is part of the esp8266 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef WEB_AUTHENTICATION_H_ +#define WEB_AUTHENTICATION_H_ + +#include "Arduino.h" + +bool checkBasicAuthentication(const char * header, const char * username, const char * password); +String requestDigestAuthentication(const char * realm); +bool checkDigestAuthentication(const char * header, const char * method, const char * username, const char * password, const char * realm, bool passwordIsHash, const char * nonce, const char * opaque, const char * uri); + +//for storing hashed versions on the device that can be authenticated against +String generateDigestHash(const char * username, const char * password, const char * realm); + +#endif diff --git a/src/WebRequest.cpp b/src/WebRequest.cpp index a02aa7f..26b4fc4 100644 --- a/src/WebRequest.cpp +++ b/src/WebRequest.cpp @@ -19,8 +19,8 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "ESPAsyncWebServer.h" -#include #include "WebResponseImpl.h" +#include "WebAuthentication.h" #ifndef ESP8266 #define os_strlen strlen @@ -45,6 +45,7 @@ AsyncWebServerRequest::AsyncWebServerRequest(AsyncWebServer* s, AsyncClient* c) , _contentType() , _boundary() , _authorization() + , _isDigest(false) , _isMultipart(false) , _isPlainPost(false) , _expectingContinue(false) @@ -139,6 +140,7 @@ void AsyncWebServerRequest::_onData(void *buf, size_t len){ } } if(!_isPlainPost) { + //check if authenticated before calling the body if(_handler) _handler->handleBody(this, (uint8_t*)buf, len, _parsedLength, _contentLength); _parsedLength += len; } else { @@ -152,6 +154,7 @@ void AsyncWebServerRequest::_onData(void *buf, size_t len){ if(_parsedLength == _contentLength){ _parseState = PARSE_REQ_END; + //check if authenticated before calling handleRequest and request auth instead if(_handler) _handler->handleRequest(this); else send(501); } @@ -254,7 +257,7 @@ bool AsyncWebServerRequest::_parseReqHead(){ _url = u; _addGetParams(g); - if(_temp.startsWith("HTTP/1.1")) + if(!_temp.startsWith("HTTP/1.0")) _version = 1; _temp = String(); @@ -285,6 +288,9 @@ bool AsyncWebServerRequest::_parseReqHeader(){ } else if(name == "Authorization"){ if(value.startsWith("Basic")){ _authorization = value.substring(6); + } else if(value.startsWith("Digest")){ + _isDigest = true; + _authorization = value.substring(7); } } else { if(_interestingHeaders->contains(name) || _interestingHeaders->contains("ANY")){ @@ -323,6 +329,7 @@ void AsyncWebServerRequest::_handleUploadByte(uint8_t data, bool last){ _itemBuffer[_itemBufferIndex++] = data; if(last || _itemBufferIndex == 1460){ + //check if authenticated before calling the upload if(_handler) _handler->handleUpload(this, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, false); _itemBufferIndex = 0; @@ -463,6 +470,7 @@ void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last){ _addParam(new AsyncWebParameter(_itemName, _itemValue, true)); } else { if(_itemSize){ + //check if authenticated before calling the upload if(_handler) _handler->handleUpload(this, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, true); _itemBufferIndex = 0; _addParam(new AsyncWebParameter(_itemName, _itemFilename, true, true, _itemSize)); @@ -520,6 +528,7 @@ void AsyncWebServerRequest::_parseLine(){ const char * response = "HTTP/1.1 100 Continue\r\n\r\n"; _client->write(response, os_strlen(response)); } + //check handler for authentication if(_contentLength){ _parseState = PARSE_REQ_BODY; } else { @@ -700,38 +709,54 @@ void AsyncWebServerRequest::redirect(String url){ send(response); } - -bool AsyncWebServerRequest::authenticate(const char * username, const char * password){ +bool AsyncWebServerRequest::authenticate(const char * username, const char * password, const char * realm, bool passwordIsHash){ if(_authorization.length()){ - char toencodeLen = os_strlen(username)+os_strlen(password)+1; - char *toencode = new char[toencodeLen+1]; - if(toencode == NULL){ - return false; - } - char *encoded = new char[base64_encode_expected_len(toencodeLen)+1]; - if(encoded == NULL){ - delete[] toencode; - return false; - } - sprintf(toencode, "%s:%s", username, password); - if(base64_encode_chars(toencode, toencodeLen, encoded) > 0 && _authorization.equals(encoded)){ - delete[] toencode; - delete[] encoded; - return true; - } - delete[] toencode; - delete[] encoded; + if(_isDigest) + return checkDigestAuthentication(_authorization.c_str(), methodToString(), username, password, realm, passwordIsHash, NULL, NULL, NULL); + else if(!passwordIsHash) + return checkBasicAuthentication(_authorization.c_str(), username, password); + else + return _authorization.equals(password); } return false; } bool AsyncWebServerRequest::authenticate(const char * hash){ - return (_authorization.length() && (_authorization == String(hash))); + if(!_authorization.length() || hash == NULL) + return false; + + if(_isDigest){ + String hStr = String(hash); + int separator = hStr.indexOf(":"); + if(separator <= 0) + return false; + String username = hStr.substring(0, separator); + hStr = hStr.substring(separator + 1); + separator = hStr.indexOf(":"); + if(separator <= 0) + return false; + String realm = hStr.substring(0, separator); + hStr = hStr.substring(separator + 1); + return checkDigestAuthentication(_authorization.c_str(), methodToString(), username.c_str(), hStr.c_str(), realm.c_str(), true, NULL, NULL, NULL); + } + + return (_authorization.equals(hash)); } -void AsyncWebServerRequest::requestAuthentication(){ +void AsyncWebServerRequest::requestAuthentication(const char * realm, bool isDigest){ AsyncWebServerResponse * r = beginResponse(401); - r->addHeader("WWW-Authenticate", "Basic realm=\"Login Required\""); + if(!isDigest && realm == NULL){ + r->addHeader("WWW-Authenticate", "Basic realm=\"Login Required\""); + } else if(!isDigest){ + String header = "Basic realm=\""; + header.concat(realm); + header.concat("\""); + r->addHeader("WWW-Authenticate", header); + } else { + String header = "Digest "; + header.concat(requestDigestAuthentication(realm)); + r->addHeader("WWW-Authenticate", header); + } send(r); }