Files
ESPWebDAV/ESPWebDAV.cpp
2020-04-21 11:18:53 +08:00

569 lines
17 KiB
C++

// WebDAV server using ESP8266 and SD card filesystem
// Targeting Windows 7 Explorer WebDav
#include <ESP8266WiFi.h>
#include <SPI.h>
#include <SdFat.h>
#include <Hash.h>
#include <time.h>
#include "ESPWebDAV.h"
// define cal constants
const char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
const char *wdays[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
// ------------------------
bool ESPWebDAV::init(int chipSelectPin, SPISettings spiSettings, int serverPort) {
// ------------------------
// start the wifi server
server = new WiFiServer(serverPort);
server->begin();
// initialize the SD card
return sd.begin(chipSelectPin, spiSettings);
}
// ------------------------
bool ESPWebDAV::initSD(int chipSelectPin, SPISettings spiSettings) {
// initialize the SD card
return sd.begin(chipSelectPin, spiSettings);
}
// ------------------------
bool ESPWebDAV::startServer() {
// ------------------------
// start the wifi server
server->begin();
}
// ------------------------
void ESPWebDAV::handleNotFound() {
// ------------------------
String message = "Not found\n";
message += "URI: ";
message += uri;
message += " Method: ";
message += method;
message += "\n";
sendHeader("Allow", "OPTIONS,MKCOL,POST,PUT");
send("404 Not Found", "text/plain", message);
DBG_PRINTLN("404 Not Found");
}
// ------------------------
void ESPWebDAV::handleReject(String rejectMessage) {
// ------------------------
DBG_PRINT("Rejecting request: "); DBG_PRINTLN(rejectMessage);
// handle options
if(method.equals("OPTIONS"))
return handleOptions(RESOURCE_NONE);
// handle properties
if(method.equals("PROPFIND")) {
sendHeader("Allow", "PROPFIND,OPTIONS,DELETE,COPY,MOVE");
setContentLength(CONTENT_LENGTH_UNKNOWN);
send("207 Multi-Status", "application/xml;charset=utf-8", "");
sendContent(F("<?xml version=\"1.0\" encoding=\"utf-8\"?><D:multistatus xmlns:D=\"DAV:\"><D:response><D:href>/</D:href><D:propstat><D:status>HTTP/1.1 200 OK</D:status><D:prop><D:getlastmodified>Fri, 30 Nov 1979 00:00:00 GMT</D:getlastmodified><D:getetag>\"3333333333333333333333333333333333333333\"</D:getetag><D:resourcetype><D:collection/></D:resourcetype></D:prop></D:propstat></D:response>"));
if(depthHeader.equals("1")) {
sendContent(F("<D:response><D:href>/"));
sendContent(rejectMessage);
sendContent(F("</D:href><D:propstat><D:status>HTTP/1.1 200 OK</D:status><D:prop><D:getlastmodified>Fri, 01 Apr 2016 16:07:40 GMT</D:getlastmodified><D:getetag>\"2222222222222222222222222222222222222222\"</D:getetag><D:resourcetype/><D:getcontentlength>0</D:getcontentlength><D:getcontenttype>application/octet-stream</D:getcontenttype></D:prop></D:propstat></D:response>"));
}
sendContent(F("</D:multistatus>"));
return;
}
else
// if reached here, means its a 404
handleNotFound();
}
// set http_proxy=http://localhost:36036
// curl -v -X PROPFIND -H "Depth: 1" http://Rigidbot/Old/PipeClip.gcode
// Test PUT a file: curl -v -T c.txt -H "Expect:" http://Rigidbot/c.txt
// C:\Users\gsbal>curl -v -X LOCK http://Rigidbot/EMA_CPP_TRCC_Tutorial/Consumer.cpp -d "<?xml version=\"1.0\" encoding=\"utf-8\" ?><D:lockinfo xmlns:D=\"DAV:\"><D:lockscope><D:exclusive/></D:lockscope><D:locktype><D:write/></D:locktype><D:owner><D:href>CARBON2\gsbal</D:href></D:owner></D:lockinfo>"
// ------------------------
void ESPWebDAV::handleRequest(String blank) {
// ------------------------
ResourceType resource = RESOURCE_NONE;
// does uri refer to a file or directory or a null?
FatFile tFile;
if(tFile.open(sd.vwd(), uri.c_str(), O_READ)) {
resource = tFile.isDir() ? RESOURCE_DIR : RESOURCE_FILE;
tFile.close();
}
DBG_PRINT("\r\nm: "); DBG_PRINT(method);
DBG_PRINT(" r: "); DBG_PRINT(resource);
DBG_PRINT(" u: "); DBG_PRINTLN(uri);
// add header that gets sent everytime
sendHeader("DAV", "2");
// handle properties
if(method.equals("PROPFIND"))
return handleProp(resource);
if(method.equals("GET"))
return handleGet(resource, true);
if(method.equals("HEAD"))
return handleGet(resource, false);
// handle options
if(method.equals("OPTIONS"))
return handleOptions(resource);
// handle file create/uploads
if(method.equals("PUT"))
return handlePut(resource);
// handle file locks
if(method.equals("LOCK"))
return handleLock(resource);
if(method.equals("UNLOCK"))
return handleUnlock(resource);
if(method.equals("PROPPATCH"))
return handlePropPatch(resource);
// directory creation
if(method.equals("MKCOL"))
return handleDirectoryCreate(resource);
// move a file or directory
if(method.equals("MOVE"))
return handleMove(resource);
// delete a file or directory
if(method.equals("DELETE"))
return handleDelete(resource);
// if reached here, means its a 404
handleNotFound();
}
// ------------------------
void ESPWebDAV::handleOptions(ResourceType resource) {
// ------------------------
DBG_PRINTLN("Processing OPTION");
sendHeader("Allow", "PROPFIND,GET,DELETE,PUT,COPY,MOVE");
send("200 OK", NULL, "");
}
// ------------------------
void ESPWebDAV::handleLock(ResourceType resource) {
// ------------------------
DBG_PRINTLN("Processing LOCK");
// does URI refer to an existing resource
if(resource == RESOURCE_NONE)
return handleNotFound();
sendHeader("Allow", "PROPPATCH,PROPFIND,OPTIONS,DELETE,UNLOCK,COPY,LOCK,MOVE,HEAD,POST,PUT,GET");
sendHeader("Lock-Token", "urn:uuid:26e57cb3-834d-191a-00de-000042bdecf9");
size_t contentLen = contentLengthHeader.toInt();
uint8_t buf[1024];
size_t numRead = readBytesWithTimeout(buf, sizeof(buf), contentLen);
if(numRead == 0)
return handleNotFound();
buf[contentLen] = 0;
String inXML = String((char*) buf);
int startIdx = inXML.indexOf("<D:href>");
int endIdx = inXML.indexOf("</D:href>");
if(startIdx < 0 || endIdx < 0)
return handleNotFound();
String lockUser = inXML.substring(startIdx + 8, endIdx);
String resp1 = F("<?xml version=\"1.0\" encoding=\"utf-8\"?><D:prop xmlns:D=\"DAV:\"><D:lockdiscovery><D:activelock><D:locktype><write/></D:locktype><D:lockscope><exclusive/></D:lockscope><D:locktoken><D:href>urn:uuid:26e57cb3-834d-191a-00de-000042bdecf9</D:href></D:locktoken><D:lockroot><D:href>");
String resp2 = F("</D:href></D:lockroot><D:depth>infinity</D:depth><D:owner><a:href xmlns:a=\"DAV:\">");
String resp3 = F("</a:href></D:owner><D:timeout>Second-3600</D:timeout></D:activelock></D:lockdiscovery></D:prop>");
send("200 OK", "application/xml;charset=utf-8", resp1 + uri + resp2 + lockUser + resp3);
}
// ------------------------
void ESPWebDAV::handleUnlock(ResourceType resource) {
// ------------------------
DBG_PRINTLN("Processing UNLOCK");
sendHeader("Allow", "PROPPATCH,PROPFIND,OPTIONS,DELETE,UNLOCK,COPY,LOCK,MOVE,HEAD,POST,PUT,GET");
sendHeader("Lock-Token", "urn:uuid:26e57cb3-834d-191a-00de-000042bdecf9");
send("204 No Content", NULL, "");
}
// ------------------------
void ESPWebDAV::handlePropPatch(ResourceType resource) {
// ------------------------
DBG_PRINTLN("PROPPATCH forwarding to PROPFIND");
handleProp(resource);
}
// ------------------------
void ESPWebDAV::handleProp(ResourceType resource) {
// ------------------------
DBG_PRINTLN("Processing PROPFIND");
// check depth header
DepthType depth = DEPTH_NONE;
if(depthHeader.equals("1"))
depth = DEPTH_CHILD;
else if(depthHeader.equals("infinity"))
depth = DEPTH_ALL;
DBG_PRINT("Depth: "); DBG_PRINTLN(depth);
// does URI refer to an existing resource
if(resource == RESOURCE_NONE)
return handleNotFound();
if(resource == RESOURCE_FILE)
sendHeader("Allow", "PROPFIND,OPTIONS,DELETE,COPY,MOVE,HEAD,POST,PUT,GET");
else
sendHeader("Allow", "PROPFIND,OPTIONS,DELETE,COPY,MOVE");
setContentLength(CONTENT_LENGTH_UNKNOWN);
send("207 Multi-Status", "application/xml;charset=utf-8", "");
sendContent(F("<?xml version=\"1.0\" encoding=\"utf-8\"?>"));
sendContent(F("<D:multistatus xmlns:D=\"DAV:\">"));
// open this resource
SdFile baseFile;
baseFile.open(uri.c_str(), O_READ);
sendPropResponse(false, &baseFile);
if((resource == RESOURCE_DIR) && (depth == DEPTH_CHILD)) {
// append children information to message
SdFile childFile;
while(childFile.openNext(&baseFile, O_READ)) {
yield();
sendPropResponse(true, &childFile);
childFile.close();
}
}
baseFile.close();
sendContent(F("</D:multistatus>"));
}
// ------------------------
void ESPWebDAV::sendPropResponse(boolean recursing, FatFile *curFile) {
// ------------------------
char buf[255];
curFile->getName(buf, sizeof(buf));
// String fullResPath = "http://" + hostHeader + uri;
String fullResPath = uri;
if(recursing)
if(fullResPath.endsWith("/"))
fullResPath += String(buf);
else
fullResPath += "/" + String(buf);
// get file modified time
dir_t dir;
curFile->dirEntry(&dir);
// convert to required format
tm tmStr;
tmStr.tm_hour = FAT_HOUR(dir.lastWriteTime);
tmStr.tm_min = FAT_MINUTE(dir.lastWriteTime);
tmStr.tm_sec = FAT_SECOND(dir.lastWriteTime);
tmStr.tm_year = FAT_YEAR(dir.lastWriteDate) - 1900;
tmStr.tm_mon = FAT_MONTH(dir.lastWriteDate) - 1;
tmStr.tm_mday = FAT_DAY(dir.lastWriteDate);
time_t t2t = mktime(&tmStr);
tm *gTm = gmtime(&t2t);
// Tue, 13 Oct 2015 17:07:35 GMT
sprintf(buf, "%s, %02d %s %04d %02d:%02d:%02d GMT", wdays[gTm->tm_wday], gTm->tm_mday, months[gTm->tm_mon], gTm->tm_year + 1900, gTm->tm_hour, gTm->tm_min, gTm->tm_sec);
String fileTimeStamp = String(buf);
// send the XML information about thyself to client
sendContent(F("<D:response><D:href>"));
// append full file path
sendContent(fullResPath);
sendContent(F("</D:href><D:propstat><D:status>HTTP/1.1 200 OK</D:status><D:prop><D:getlastmodified>"));
// append modified date
sendContent(fileTimeStamp);
sendContent(F("</D:getlastmodified><D:getetag>"));
// append unique tag generated from full path
sendContent("\"" + sha1(fullResPath + fileTimeStamp) + "\"");
sendContent(F("</D:getetag>"));
if(curFile->isDir())
sendContent(F("<D:resourcetype><D:collection/></D:resourcetype>"));
else {
sendContent(F("<D:resourcetype/><D:getcontentlength>"));
// append the file size
sendContent(String(curFile->fileSize()));
sendContent(F("</D:getcontentlength><D:getcontenttype>"));
// append correct file mime type
sendContent(getMimeType(fullResPath));
sendContent(F("</D:getcontenttype>"));
}
sendContent(F("</D:prop></D:propstat></D:response>"));
}
// ------------------------
void ESPWebDAV::handleGet(ResourceType resource, bool isGet) {
// ------------------------
DBG_PRINTLN("Processing GET");
// does URI refer to an existing file resource
if(resource != RESOURCE_FILE)
return handleNotFound();
SdFile rFile;
long tStart = millis();
uint8_t buf[1460];
rFile.open(uri.c_str(), O_READ);
sendHeader("Allow", "PROPFIND,OPTIONS,DELETE,COPY,MOVE,HEAD,POST,PUT,GET");
size_t fileSize = rFile.fileSize();
setContentLength(fileSize);
String contentType = getMimeType(uri);
if(uri.endsWith(".gz") && contentType != "application/x-gzip" && contentType != "application/octet-stream")
sendHeader("Content-Encoding", "gzip");
send("200 OK", contentType.c_str(), "");
if(isGet) {
// disable Nagle if buffer size > TCP MTU of 1460
// client.setNoDelay(1);
// send the file
while(rFile.available()) {
// SD read speed ~ 17sec for 4.5MB file
int numRead = rFile.read(buf, sizeof(buf));
client.write(buf, numRead);
}
}
rFile.close();
DBG_PRINT("File "); DBG_PRINT(fileSize); DBG_PRINT(" bytes sent in: "); DBG_PRINT((millis() - tStart)/1000); DBG_PRINTLN(" sec");
}
// ------------------------
void ESPWebDAV::handlePut(ResourceType resource) {
// ------------------------
DBG_PRINTLN("Processing Put");
// does URI refer to a directory
if(resource == RESOURCE_DIR)
return handleNotFound();
SdFile nFile;
sendHeader("Allow", "PROPFIND,OPTIONS,DELETE,COPY,MOVE,HEAD,POST,PUT,GET");
// if file does not exist, create it
if(resource == RESOURCE_NONE) {
if(!nFile.open(uri.c_str(), O_CREAT | O_WRITE))
return handleWriteError("Unable to create a new file", &nFile);
}
// file is created/open for writing at this point
DBG_PRINT(uri); DBG_PRINTLN(" - ready for data");
// did server send any data in put
size_t contentLen = contentLengthHeader.toInt();
if(contentLen != 0) {
// buffer size is critical *don't change*
const size_t WRITE_BLOCK_CONST = 512;
uint8_t buf[WRITE_BLOCK_CONST];
long tStart = millis();
size_t numRemaining = contentLen;
// high speed raw write implementation
// close any previous file
nFile.close();
// delete old file
sd.remove(uri.c_str());
// create a contiguous file
size_t contBlocks = (contentLen/WRITE_BLOCK_CONST + 1);
uint32_t bgnBlock, endBlock;
if (!nFile.createContiguous(sd.vwd(), uri.c_str(), contBlocks * WRITE_BLOCK_CONST))
return handleWriteError("File create contiguous sections failed", &nFile);
// get the location of the file's blocks
if (!nFile.contiguousRange(&bgnBlock, &endBlock))
return handleWriteError("Unable to get contiguous range", &nFile);
if (!sd.card()->writeStart(bgnBlock, contBlocks))
return handleWriteError("Unable to start writing contiguous range", &nFile);
// read data from stream and write to the file
while(numRemaining > 0) {
size_t numToRead = (numRemaining > WRITE_BLOCK_CONST) ? WRITE_BLOCK_CONST : numRemaining;
size_t numRead = readBytesWithTimeout(buf, sizeof(buf), numToRead);
if(numRead == 0)
break;
// store whole buffer into file regardless of numRead
if (!sd.card()->writeData(buf))
return handleWriteError("Write data failed", &nFile);
// reduce the number outstanding
numRemaining -= numRead;
}
// stop writing operation
if (!sd.card()->writeStop())
return handleWriteError("Unable to stop writing contiguous range", &nFile);
// detect timeout condition
if(numRemaining)
return handleWriteError("Timed out waiting for data", &nFile);
// truncate the file to right length
if(!nFile.truncate(contentLen))
return handleWriteError("Unable to truncate the file", &nFile);
DBG_PRINT("File "); DBG_PRINT(contentLen - numRemaining); DBG_PRINT(" bytes stored in: "); DBG_PRINT((millis() - tStart)/1000); DBG_PRINTLN(" sec");
}
if(resource == RESOURCE_NONE)
send("201 Created", NULL, "");
else
send("200 OK", NULL, "");
nFile.close();
}
// ------------------------
void ESPWebDAV::handleWriteError(String message, FatFile *wFile) {
// ------------------------
// close this file
wFile->close();
// delete the wrile being written
sd.remove(uri.c_str());
// send error
send("500 Internal Server Error", "text/plain", message);
DBG_PRINTLN(message);
}
// ------------------------
void ESPWebDAV::handleDirectoryCreate(ResourceType resource) {
// ------------------------
DBG_PRINTLN("Processing MKCOL");
// does URI refer to anything
if(resource != RESOURCE_NONE)
return handleNotFound();
// create directory
if (!sd.mkdir(uri.c_str(), true)) {
// send error
send("500 Internal Server Error", "text/plain", "Unable to create directory");
DBG_PRINTLN("Unable to create directory");
return;
}
DBG_PRINT(uri); DBG_PRINTLN(" directory created");
sendHeader("Allow", "OPTIONS,MKCOL,LOCK,POST,PUT");
send("201 Created", NULL, "");
}
// ------------------------
void ESPWebDAV::handleMove(ResourceType resource) {
// ------------------------
DBG_PRINTLN("Processing MOVE");
// does URI refer to anything
if(resource == RESOURCE_NONE)
return handleNotFound();
if(destinationHeader.length() == 0)
return handleNotFound();
String dest = urlToUri(destinationHeader);
DBG_PRINT("Move destination: "); DBG_PRINTLN(dest);
// move file or directory
if ( !sd.rename(uri.c_str(), dest.c_str()) ) {
// send error
send("500 Internal Server Error", "text/plain", "Unable to move");
DBG_PRINTLN("Unable to move file/directory");
return;
}
DBG_PRINTLN("Move successful");
sendHeader("Allow", "OPTIONS,MKCOL,LOCK,POST,PUT");
send("201 Created", NULL, "");
}
// ------------------------
void ESPWebDAV::handleDelete(ResourceType resource) {
// ------------------------
DBG_PRINTLN("Processing DELETE");
// does URI refer to anything
if(resource == RESOURCE_NONE)
return handleNotFound();
bool retVal;
if(resource == RESOURCE_FILE)
// delete a file
retVal = sd.remove(uri.c_str());
else
// delete a directory
retVal = sd.rmdir(uri.c_str());
if(!retVal) {
// send error
send("500 Internal Server Error", "text/plain", "Unable to delete");
DBG_PRINTLN("Unable to delete file/directory");
return;
}
DBG_PRINTLN("Delete successful");
sendHeader("Allow", "OPTIONS,MKCOL,LOCK,POST,PUT");
send("200 OK", NULL, "");
}