Add Digest Web Authentication and make it default

accept htdigest formatted hashes as well
This commit is contained in:
Me No Dev 2016-06-29 16:20:03 +03:00
parent cdd1ced67a
commit 49b6046125
6 changed files with 312 additions and 34 deletions

View File

@ -35,9 +35,9 @@ script:
- arduino --board esp8266com:esp8266:generic --save-prefs - arduino --board esp8266com:esp8266:generic --save-prefs
- arduino --get-pref sketchbook.path - arduino --get-pref sketchbook.path
- build_sketches arduino $HOME/Arduino/libraries/ESPAsyncWebServer esp8266 - build_sketches arduino $HOME/Arduino/libraries/ESPAsyncWebServer esp8266
- arduino --board espressif:ESP31B:esp31b --save-prefs # - arduino --board espressif:ESP31B:esp31b --save-prefs
- arduino --get-pref sketchbook.path # - arduino --get-pref sketchbook.path
- build_sketches arduino $HOME/Arduino/libraries/ESPAsyncWebServer esp31b # - build_sketches arduino $HOME/Arduino/libraries/ESPAsyncWebServer esp31b
notifications: notifications:
email: email:

View File

@ -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 - 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 while the server is taking care of sending the response in the background
- Speed is OMG - 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 - Easily extendible to handle any type of content
- Supports Continue 100 - Supports Continue 100
- Async WebSocket plugin offering different locations without extra servers or ports - 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 - URL Rewrite plugin for conditional and permanent url rewrites
- ServeStatic plugin that supports cache, Last-Modified, default index and more - ServeStatic plugin that supports cache, Last-Modified, default index and more
@ -668,7 +668,7 @@ client->binary(flash_binary, 4);
``` ```
## Async Event Source Plugin ## 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. 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); AsyncWebServer server(80);
AsyncWebSocket ws("/ws"); // access at ws://[esp ip]/ws 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* ssid = "your-ssid";
const char* password = "your-pass"; const char* password = "your-pass";

View File

@ -129,6 +129,7 @@ class AsyncWebServerRequest {
String _contentType; String _contentType;
String _boundary; String _boundary;
String _authorization; String _authorization;
bool _isDigest;
bool _isMultipart; bool _isMultipart;
bool _isPlainPost; bool _isPlainPost;
bool _expectingContinue; bool _expectingContinue;
@ -188,9 +189,13 @@ class AsyncWebServerRequest {
bool multipart(){ return _isMultipart; } bool multipart(){ return _isMultipart; }
const char * methodToString(); 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); 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 setHandler(AsyncWebHandler *handler){ _handler = handler; }
void addInterestingHeader(String name); void addInterestingHeader(String name);

214
src/WebAuthentication.cpp Normal file
View File

@ -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 <libb64/cencode.h>
#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;
}

34
src/WebAuthentication.h Normal file
View File

@ -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

View File

@ -19,8 +19,8 @@
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/ */
#include "ESPAsyncWebServer.h" #include "ESPAsyncWebServer.h"
#include <libb64/cencode.h>
#include "WebResponseImpl.h" #include "WebResponseImpl.h"
#include "WebAuthentication.h"
#ifndef ESP8266 #ifndef ESP8266
#define os_strlen strlen #define os_strlen strlen
@ -45,6 +45,7 @@ AsyncWebServerRequest::AsyncWebServerRequest(AsyncWebServer* s, AsyncClient* c)
, _contentType() , _contentType()
, _boundary() , _boundary()
, _authorization() , _authorization()
, _isDigest(false)
, _isMultipart(false) , _isMultipart(false)
, _isPlainPost(false) , _isPlainPost(false)
, _expectingContinue(false) , _expectingContinue(false)
@ -139,6 +140,7 @@ void AsyncWebServerRequest::_onData(void *buf, size_t len){
} }
} }
if(!_isPlainPost) { if(!_isPlainPost) {
//check if authenticated before calling the body
if(_handler) _handler->handleBody(this, (uint8_t*)buf, len, _parsedLength, _contentLength); if(_handler) _handler->handleBody(this, (uint8_t*)buf, len, _parsedLength, _contentLength);
_parsedLength += len; _parsedLength += len;
} else { } else {
@ -152,6 +154,7 @@ void AsyncWebServerRequest::_onData(void *buf, size_t len){
if(_parsedLength == _contentLength){ if(_parsedLength == _contentLength){
_parseState = PARSE_REQ_END; _parseState = PARSE_REQ_END;
//check if authenticated before calling handleRequest and request auth instead
if(_handler) _handler->handleRequest(this); if(_handler) _handler->handleRequest(this);
else send(501); else send(501);
} }
@ -254,7 +257,7 @@ bool AsyncWebServerRequest::_parseReqHead(){
_url = u; _url = u;
_addGetParams(g); _addGetParams(g);
if(_temp.startsWith("HTTP/1.1")) if(!_temp.startsWith("HTTP/1.0"))
_version = 1; _version = 1;
_temp = String(); _temp = String();
@ -285,6 +288,9 @@ bool AsyncWebServerRequest::_parseReqHeader(){
} else if(name == "Authorization"){ } else if(name == "Authorization"){
if(value.startsWith("Basic")){ if(value.startsWith("Basic")){
_authorization = value.substring(6); _authorization = value.substring(6);
} else if(value.startsWith("Digest")){
_isDigest = true;
_authorization = value.substring(7);
} }
} else { } else {
if(_interestingHeaders->contains(name) || _interestingHeaders->contains("ANY")){ if(_interestingHeaders->contains(name) || _interestingHeaders->contains("ANY")){
@ -323,6 +329,7 @@ void AsyncWebServerRequest::_handleUploadByte(uint8_t data, bool last){
_itemBuffer[_itemBufferIndex++] = data; _itemBuffer[_itemBufferIndex++] = data;
if(last || _itemBufferIndex == 1460){ if(last || _itemBufferIndex == 1460){
//check if authenticated before calling the upload
if(_handler) if(_handler)
_handler->handleUpload(this, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, false); _handler->handleUpload(this, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, false);
_itemBufferIndex = 0; _itemBufferIndex = 0;
@ -463,6 +470,7 @@ void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last){
_addParam(new AsyncWebParameter(_itemName, _itemValue, true)); _addParam(new AsyncWebParameter(_itemName, _itemValue, true));
} else { } else {
if(_itemSize){ if(_itemSize){
//check if authenticated before calling the upload
if(_handler) _handler->handleUpload(this, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, true); if(_handler) _handler->handleUpload(this, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, true);
_itemBufferIndex = 0; _itemBufferIndex = 0;
_addParam(new AsyncWebParameter(_itemName, _itemFilename, true, true, _itemSize)); _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"; const char * response = "HTTP/1.1 100 Continue\r\n\r\n";
_client->write(response, os_strlen(response)); _client->write(response, os_strlen(response));
} }
//check handler for authentication
if(_contentLength){ if(_contentLength){
_parseState = PARSE_REQ_BODY; _parseState = PARSE_REQ_BODY;
} else { } else {
@ -700,38 +709,54 @@ void AsyncWebServerRequest::redirect(String url){
send(response); send(response);
} }
bool AsyncWebServerRequest::authenticate(const char * username, const char * password, const char * realm, bool passwordIsHash){
bool AsyncWebServerRequest::authenticate(const char * username, const char * password){
if(_authorization.length()){ if(_authorization.length()){
char toencodeLen = os_strlen(username)+os_strlen(password)+1; if(_isDigest)
char *toencode = new char[toencodeLen+1]; return checkDigestAuthentication(_authorization.c_str(), methodToString(), username, password, realm, passwordIsHash, NULL, NULL, NULL);
if(toencode == NULL){ else if(!passwordIsHash)
return false; return checkBasicAuthentication(_authorization.c_str(), username, password);
} else
char *encoded = new char[base64_encode_expected_len(toencodeLen)+1]; return _authorization.equals(password);
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;
} }
return false; return false;
} }
bool AsyncWebServerRequest::authenticate(const char * hash){ 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); AsyncWebServerResponse * r = beginResponse(401);
if(!isDigest && realm == NULL){
r->addHeader("WWW-Authenticate", "Basic realm=\"Login Required\""); 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); send(r);
} }