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

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
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";

View File

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

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
*/
#include "ESPAsyncWebServer.h"
#include <libb64/cencode.h>
#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);
}