esp32_https_server icon indicating copy to clipboard operation
esp32_https_server copied to clipboard

Client closing connection results in crash for HTTP post multipart requests

Open kinafu opened this issue 5 years ago • 3 comments

Describe the bug

When a client aborts a request, the program hangs.

The sketch below has been adapted from the HTTP forms example.

Please use the sketch from the second post, it's simpler!

More complex sketch with multipart/form-data
/**
 * Example for the ESP32 HTTP(S) Webserver
 *
 * IMPORTANT NOTE:
 * To run this script, you need to
 *  1) Enter your WiFi SSID and PSK below this comment
 *  2) Make sure to have certificate data available. You will find a
 *     shell script and instructions to do so in the library folder
 *     under extras/
 *
 * This script will install an HTTPS Server on your ESP32 with the following
 * functionalities:
 *  - Show simple page on web server root that includes some HTML Forms
 *  - Define a POST handler that handles the forms using the HTTPBodyParser API
 *    provided by the library.
 *  - 404 for everything else
 */

// TODO: Configure your WiFi here
#define WIFI_SSID "<insert your WiFi>"
#define WIFI_PSK  "<insert your WiFi pass>"

// Include certificate data (see note above)

// We will use wifi
#include <WiFi.h>

// We will use SPIFFS and FS
#include <SPIFFS.h>
#include <FS.h>

// Includes for the server
#include <HTTPSServer.hpp>
#include <SSLCert.hpp>
#include <HTTPRequest.hpp>
#include <HTTPResponse.hpp>
#include <HTTPBodyParser.hpp>
#include <HTTPMultipartBodyParser.hpp>
#include <HTTPURLEncodedBodyParser.hpp>

// The HTTPS Server comes in a separate namespace. For easier use, include it here.
using namespace httpsserver;

// Create an SSL certificate object from the files included above
// SSLCert cert = SSLCert(
//   example_crt_DER, example_crt_DER_len,
//   example_key_DER, example_key_DER_len
// );

// Create an SSL-enabled server that uses the certificate
// The contstructor takes some more parameters, but we go for default values here.
SSLCert * cert;
HTTPSServer * secureServer;


// Declare some handler functions for the various URLs on the server
// See the static-page example for how handler functions work.
// The comments in setup() describe what each handler function does in this example.

void handleFormUpload(HTTPRequest * req, HTTPResponse * res);

void handleFormUpload(HTTPRequest * req, HTTPResponse * res) {
  // First, we need to check the encoding of the form that we have received.
  // The browser will set the Content-Type request header, so we can use it for that purpose.
  // Then we select the body parser based on the encoding.
  // Actually we do this only for documentary purposes, we know the form is going
  // to be multipart/form-data.
  HTTPBodyParser *parser;
  std::string contentType = req->getHeader("Content-Type");

  // The content type may have additional properties after a semicolon, for exampel:
  // Content-Type: text/html;charset=utf-8
  // Content-Type: multipart/form-data;boundary=------s0m3w31rdch4r4c73rs
  // As we're interested only in the actual mime _type_, we strip everything after the
  // first semicolon, if one exists:
  size_t semicolonPos = contentType.find(";");
  if (semicolonPos != std::string::npos) {
    contentType = contentType.substr(0, semicolonPos);
  }

  // Now, we can decide based on the content type:
  if (contentType == "multipart/form-data") {
    parser = new HTTPMultipartBodyParser(req);
  } else {
    Serial.printf("Unknown POST Content-Type: %s\n", contentType.c_str());
    return;
  }

  res->println("<html><head><title>File Upload</title></head><body><h1>File Upload</h1>");

  // We iterate over the fields. Any field with a filename is uploaded.
  // Note that the BodyParser consumes the request body, meaning that you can iterate over the request's
  // fields only a single time. The reason for this is that it allows you to handle large requests
  // which would not fit into memory.

  // parser->nextField() will move the parser to the next field in the request body (field meaning a
  // form field, if you take the HTML perspective). After the last field has been processed, nextField()
  // returns false and the while loop ends.
  while(parser->nextField()) {
    // For Multipart data, each field has three properties:
    // The name ("name" value of the <input> tag)
    // The filename (If it was a <input type="file">, this is the filename on the machine of the
    //   user uploading it)
    // The mime type (It is determined by the client. So do not trust this value and blindly start
    //   parsing files only if the type matches)
    std::string name = parser->getFieldName();
    std::string filename = parser->getFieldFilename();
    std::string mimeType = parser->getFieldMimeType();
    // We log all three values, so that you can observe the upload on the serial monitor:
    Serial.printf("handleFormUpload: field name='%s', filename='%s', mimetype='%s'\n", name.c_str(), filename.c_str(), mimeType.c_str());

    // With endOfField you can check whether the end of field has been reached or if there's
    // still data pending. With multipart bodies, you cannot know the field size in advance.
    
    size_t fileLength = 0;

    while (!parser->endOfField()) {
      byte buf[512];
      size_t readLength = parser->read(buf, 512);
      fileLength += readLength;
      // Do nothing with the buffer, to keep things simple
    }
  }
  res->println("</body></html>");
  delete parser;
}

void setup() {
  // For logging
  Serial.begin(115200);
  // Connect to WiFi
  Serial.println("Setting up WiFi");
  WiFi.begin(WIFI_SSID, WIFI_PSK);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }
  Serial.print("Connected. IP=");
  Serial.println(WiFi.localIP());

  // Setup filesystem
  if (!SPIFFS.begin(true)) Serial.println("Mounting SPIFFS failed");
  cert = new SSLCert();
  int createCertResult = createSelfSignedCert(
    *cert,
    KEYSIZE_1024,
    "CN=myesp32.local,O=FancyCompany,C=DE",
    "20190101000000",
    "20300101000000"
  );
  if (createCertResult != 0) {
    Serial.printf("Cerating certificate failed. Error Code = 0x%02X, check SSLCert.hpp for details", createCertResult);
    while(true) delay(500);
  }
  secureServer = new HTTPSServer(cert);

  // For every resource available on the server, we need to create a ResourceNode
  // The ResourceNode links URL and HTTP method to a handler function

  // The handleFormUpload handler handles the file upload from the root node. As the form
  // is submitted via post, we need to specify that as handler method here:
  ResourceNode * nodeFormUpload = new ResourceNode("/upload", "POST", &handleFormUpload);

  // Add all nodes to the server so they become accessible:

  secureServer->registerNode(nodeFormUpload);

  Serial.println("Starting server...");
  secureServer->start();
  if (secureServer->isRunning()) {
    Serial.println("Server ready.");
  }
}

void loop() {
  // This call will let the server do its work
  secureServer->loop();

  // Other code would go here...
  delay(1);
}

Make a POST request to the endpoint using a large enough file I used the GUI tool insomnia for that. You may also use curl or any other tool. Just make sure to use a file large enough to abort the requests while the file has not been fully transmitted.

Stop the transmission midway. e.g. by pressing Ctrl + C or clicking on Cancel in Insomnia

Expected Behavior The server should stop processing the request and indicate that information e.g. by a method returning a boolean.

Actual Behavior The server either sees transmisson of zero-length data or does not complete the req->readBytes() method.

ESP32 Module Please provide specifications of your module

  • RAM/PSRAM: 320kiB
  • Flash Size: 4MiB

Software

  • IDE and Version: PlatformIO 4.3.4
  • OS: Windows 10
  • Client used to access the server: Insomnia

kinafu avatar Aug 17 '20 15:08 kinafu

The same thing occurs on a normal request. So I further simplified the sketch.

Just send a post request to the endpoint and abort the connection during the send process. (E.g. by disconnecting your client from the network or by pressing a cancel button)


// TODO: Configure your WiFi here
#define WIFI_SSID "<insert your WiFi>"
#define WIFI_PSK  "<insert your WiFi pass>"


#include <WiFi.h>

// Includes for the server
#include <HTTPSServer.hpp>
#include <SSLCert.hpp>
#include <HTTPRequest.hpp>
#include <HTTPResponse.hpp>

using namespace httpsserver;

// Create an SSL-enabled server that uses the certificate
SSLCert * cert;
HTTPSServer * secureServer;

// Declare some handler functions for the various URLs on the server

void handleUpload(HTTPRequest * req, HTTPResponse * res);

void handleUpload(HTTPRequest * req, HTTPResponse * res) {
  size_t bodyLength = 0;

  while ( !req->requestComplete() ) {
    const int bufSize = 2048;
    byte buf[bufSize];

    Serial.println("before");
    
    // read received data into temporary buffer
    size_t readLength = req->readBytes(buf, bufSize);
    // print amount of received bytes
    printf("%zd\n", readLength); 

    Serial.println("after");

    bodyLength += readLength;
  }

  Serial.print("total file length: ");
  Serial.println(bodyLength);

  res->setStatusCode(200);
  res->setStatusText("OK");
  res->println("Done");
}

void setup() {
  // For logging
  Serial.begin(115200);

  // Connect to WiFi
  Serial.println("Setting up WiFi");
  WiFi.begin(WIFI_SSID, WIFI_PSK);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }
  Serial.print("Connected. IP=");
  Serial.println(WiFi.localIP());

  // Generate and apply server certificate
  cert = new SSLCert();
  int createCertResult = createSelfSignedCert(
    *cert,
    KEYSIZE_1024,
    "CN=myesp32.local,O=FancyCompany,C=DE",
    "20190101000000",
    "20300101000000"
  );
  if (createCertResult != 0) {
    Serial.printf("Cerating certificate failed. Error Code = 0x%02X, check SSLCert.hpp for details", createCertResult);
    while(true) delay(500);
  }

  secureServer = new HTTPSServer(cert);
  ResourceNode * nodeUpload = new ResourceNode("/upload", "POST", &handleUpload);
  secureServer->registerNode(nodeUpload);
  
  // Start webserver
  Serial.println("Starting server...");
  secureServer->start();
  if (secureServer->isRunning()) {
    Serial.println("Server ready.");
  }
}

void loop() {
  // This call will let the server do its work
  secureServer->loop();

  // Other code would go here...
  delay(1);
}

kinafu avatar Aug 26 '20 16:08 kinafu

Thanks for the detailed description, I'll look into it, but as you may have noticed, the issues in this repo have queued over the last weeks, so it may still take some time.

fhessel avatar Sep 03 '20 18:09 fhessel

Hello :wave:

This is still happening today, i was randomly doing code checks for the REST API implemented with this library and after some investigation i pinned the crash here :cry:

This is actually a serious bug and should be fixed or a warning message should be added in the README.

Good luck :+1:

justbendev avatar Feb 08 '23 00:02 justbendev