QuickSander/ArduinoHttpServer

View on GitHub
src/internals/StreamHttpRequest.hpp

Summary

Maintainability
Test Coverage
//
//! \file
//  ArduinoHttpServer
//
//  Created by Sander van Woensel on 24-02-15.
//  Copyright (c) 2015 Sander van Woensel. All rights reserved.
//
//! HTTP Request read from a Stream.

#ifndef __ArduinoHttpServer__StreamHttpRequest__
#define __ArduinoHttpServer__StreamHttpRequest__

#include "FixString.hpp"
#include "HttpResource.hpp"
#include "HttpField.hpp"
#include "HttpVersion.hpp"
#include "ArduinoHttpServerDebug.h"

#include <Arduino.h>
#ifndef ARDUINO_HTTP_SERVER_NO_BASIC_AUTH
   #include <Base64.h>
#endif

#include <string.h>

namespace ArduinoHttpServer
{

enum class Method : char
{
   Invalid, Get, Put, Post, Head, Delete
};


typedef FixString<32> ErrorMessageString;
typedef FixString<128> ErrorString;

//------------------------------------------------------------------------------
//                             Class Declaration
//------------------------------------------------------------------------------
//! HTTP request read from a _Stream_.
template <size_t MAX_BODY_SIZE>
class StreamHttpRequest
{

public:
    StreamHttpRequest(Stream& stream);

    ~StreamHttpRequest() { };

    bool readRequest();

    // Header retrieval methods.
    inline const ArduinoHttpServer::HttpResource& getResource() const { return m_resource; };
    inline const ArduinoHttpServer::HttpVersion& getVersion() const { return m_version; };
    inline const ArduinoHttpServer::Method getMethod() const { return m_method; };

    // Field retrieval methods.
    inline const String& getContentType() const { return m_contentTypeField.getValueAsString(); };
    inline const int getContentLength() const { return m_contentLengthField.getValueAsInt(); };

    // Body retrieval methods.
    //! Retrieve zero terminated body content.
    inline const char* const getBody() const { return m_body; };

    // State retrieval
    const ErrorString getError() const;
    Stream& getStream() { return m_stream; };

    // Validate if client provided credentials match _username_ and _password_.
    #ifndef ARDUINO_HTTP_SERVER_NO_BASIC_AUTH
    bool authenticate(const char * username, const char * password) const;
    #endif

private:

   enum class Error: char {
      OK,
      TIMEOUT,
      CANNOT_HANDLE_HTTP_METHOD,
      PARSE_ERROR_INVALID_HTTP_VERSION,
      PARSE_ERROR_NO_RESOURCE
   };

   //! \todo To reduce program memory size reduce these to the proper types: char and int.
   //!    Or better yet, make these Template variables.
   static const int MAX_LINE_SIZE = 255+1;
   static const int MAX_BODY_LENGTH = MAX_BODY_SIZE-1; //!< Byte size of array. Leaves space for terminating \0.
   static const long LINE_READ_TIMEOUT_MS = 10000L; //!< [ms] Wait 10s for reception of a complete line.
   static const int MAX_RETRIES_WAIT_DATA_AVAILABLE = 255;

   void parseRequest(char lineBuffer[MAX_LINE_SIZE]);
   void parseMethod(char lineBuffer[MAX_LINE_SIZE]);
   void parseResource();
   void parseVersion();
   void parseField(char lineBuffer[MAX_LINE_SIZE]);

   void neglectToken();

   bool readLine(char lineBuffer[MAX_LINE_SIZE]);

   void setError(const Error, const ErrorMessageString& errorMessage = ErrorMessageString());

   Stream& m_stream;
   char m_body[MAX_BODY_SIZE];
   Method m_method;
   ArduinoHttpServer::HttpResource m_resource;
   ArduinoHttpServer::HttpVersion m_version;
   ArduinoHttpServer::HttpField m_contentTypeField;
   ArduinoHttpServer::HttpField m_contentLengthField;
   ArduinoHttpServer::HttpField m_authorizationField;

   Error m_error;
   ErrorMessageString m_errorDetail;

   char *m_lineBufferStrTokContext;
};

}

//------------------------------------------------------------------------------
//                             Class Definition
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
//! \brief Constructor. sets Stream timeout for reading data.
template <size_t MAX_BODY_SIZE>
ArduinoHttpServer::StreamHttpRequest<MAX_BODY_SIZE>::StreamHttpRequest(Stream& stream) :
    m_stream(stream),
    m_body{0},
    m_method(Method::Invalid),
    m_resource(),
    m_version(),
    m_contentTypeField(),
    m_contentLengthField(),
    m_error(Error::OK),
    m_errorDetail(),
    m_lineBufferStrTokContext(0)
{
   static_assert(MAX_BODY_SIZE >= 0, "HTTP body length less then zero specified.");
   m_stream.setTimeout(LINE_READ_TIMEOUT_MS);
}

//------------------------------------------------------------------------------
//! \brief Wait for data to become available on Stream and parses the request.
template <size_t MAX_BODY_SIZE>
bool ArduinoHttpServer::StreamHttpRequest<MAX_BODY_SIZE>::readRequest()
{

   char lineBuffer[MAX_LINE_SIZE] = {0};

   int attempts(0);
   while(!m_stream.available())
   {
      // Quit when failed to retrieve data after n retries.
      if(attempts >= MAX_RETRIES_WAIT_DATA_AVAILABLE)
      {
         setError(Error::TIMEOUT);
         break;
      }

      delay(10);
      ++attempts;
   }

   // Read complete line.
   if(readLine(lineBuffer))
   {
      // Parse the request header (first line).
      parseRequest(lineBuffer);

      // Keep parsing fields until an empty line has been found (with only new line / carriage return).
      while (readLine(lineBuffer))
      {
         parseField(lineBuffer);
      }

      DEBUG_ARDUINO_HTTP_SERVER_PRINTLN("HTTP field parsing complete.");

      // Parse body.
      int contentLength(getContentLength());
      if (contentLength > MAX_BODY_LENGTH)
      {
         DEBUG_ARDUINO_HTTP_SERVER_PRINTLN("Content-Length larger then the maximum content we can consume. Trunkating body.");
         contentLength = MAX_BODY_LENGTH;
      }

      DEBUG_ARDUINO_HTTP_SERVER_PRINT("Content-Length: ");
      DEBUG_ARDUINO_HTTP_SERVER_PRINTLN(contentLength);

      if(contentLength > 0)
      {
         DEBUG_ARDUINO_HTTP_SERVER_PRINT("Parsing body .... ");
         m_stream.readBytes(m_body, contentLength);
         DEBUG_ARDUINO_HTTP_SERVER_PRINTLN("done");
      }
   }

   return m_error == Error::OK;
}


//------------------------------------------------------------------------------
//! \brief Read a single line from Stream into _linebuffer_
template <size_t MAX_BODY_SIZE>
bool ArduinoHttpServer::StreamHttpRequest<MAX_BODY_SIZE>::readLine(char linebuffer[])
{
    if(m_error!=Error::OK) { return false; }

    int bytesRead(0);

    memset(linebuffer, 0, MAX_LINE_SIZE);
    bytesRead = m_stream.readBytesUntil('\r', linebuffer, MAX_LINE_SIZE);

    // Read away remaining new-line and carriage returns
    for (int i=0; i<2 && m_stream.read() != '\n'; i++)
    {
        // Allow SoftwareSerial to process incomming data.
        delay(2);
    }

    return bytesRead > 0 ;
}

//------------------------------------------------------------------------------
//! \brief Parse first line of HTTP request.
template <size_t MAX_BODY_SIZE>
void ArduinoHttpServer::StreamHttpRequest<MAX_BODY_SIZE>::parseRequest(char lineBuffer[])
{
    parseMethod(lineBuffer);
    parseResource();
    parseVersion();
}

//------------------------------------------------------------------------------
//! \brief Parse method: GET, PUT, HEAD, etc.
template <size_t MAX_BODY_SIZE>
void ArduinoHttpServer::StreamHttpRequest<MAX_BODY_SIZE>::parseMethod(char lineBuffer[])
{
   if(m_error!=Error::OK) { return; }

   // First strtok call, initialize with cached line buffer.
   // len("DELETE") + 1 for terminating null = 7
   FixString<7U> token(strtok_r(lineBuffer, " ", &m_lineBufferStrTokContext));

   if(token == "GET")
   {
      m_method = Method::Get;
   }
   else if (token == "PUT")
   {
      m_method = Method::Put;
   }
   else if (token == "POST")
   {
      m_method = Method::Post;
   }
   else if (token == "HEAD")
   {
      m_method = Method::Head;
   }
   else if (token == "DELETE")
   {
      m_method = Method::Delete;
   }
   else
   {
      m_method = Method::Invalid;
      setError(Error::CANNOT_HANDLE_HTTP_METHOD, token);
   }
}

//! Parse "HTTP/1.1" (or any other version).
template <size_t MAX_BODY_SIZE>
void ArduinoHttpServer::StreamHttpRequest<MAX_BODY_SIZE>::parseVersion()
{
    if(m_error!=Error::OK) { return; }

    // HTTP/000.000
    HttpVersion::FixStringT version(strtok_r(0, " ", &m_lineBufferStrTokContext));

    //String version(strtok_r(0, " ", &m_lineBufferStrTokContext));
    int slashPosition(version.lastIndexOf('/'));

    // String returns unsigned int for length.
    if (static_cast<unsigned int>(slashPosition) < version.length() && slashPosition > 0)
    {
        m_version = HttpVersion(version.substring(slashPosition));
    }
    else
    {
        setError(Error::PARSE_ERROR_INVALID_HTTP_VERSION, version);

    }

}

template <size_t MAX_BODY_SIZE>
void ArduinoHttpServer::StreamHttpRequest<MAX_BODY_SIZE>::parseResource()
{
   if(m_error!=Error::OK) { return; }

    String resource( strtok_r(0, " ", &m_lineBufferStrTokContext) );
    m_resource = ArduinoHttpServer::HttpResource(resource);

    if (!m_resource.isValid())
    {
        setError(Error::PARSE_ERROR_NO_RESOURCE);
    }
}

template <size_t MAX_BODY_SIZE>
void ArduinoHttpServer::StreamHttpRequest<MAX_BODY_SIZE>::neglectToken()
{
    strtok_r(0, " ", &m_lineBufferStrTokContext);
}

template <size_t MAX_BODY_SIZE>
void ArduinoHttpServer::StreamHttpRequest<MAX_BODY_SIZE>::parseField(char lineBuffer[])
{
   if(m_error!=Error::OK) { return; }

   ArduinoHttpServer::HttpField httpField(lineBuffer);

   if(httpField.getType() == ArduinoHttpServer::HttpField::Type::CONTENT_TYPE)
   {
      m_contentTypeField = httpField;
   }
   else if(httpField.getType() == ArduinoHttpServer::HttpField::Type::CONTENT_LENGTH)
   {
      m_contentLengthField = httpField;
   }
   else if(httpField.getType() == ArduinoHttpServer::HttpField::Type::AUTHORIZATION)
   {
      m_authorizationField = httpField;
   }
   else
   {
      // Ignore other fields for now.
   }
}

template <size_t MAX_BODY_SIZE>
void ArduinoHttpServer::StreamHttpRequest<MAX_BODY_SIZE>::setError(const Error error, const ErrorMessageString& errorMessage)
{
   m_error = error;
   m_errorDetail = errorMessage;
}

template <size_t MAX_BODY_SIZE>
const ArduinoHttpServer::ErrorString ArduinoHttpServer::StreamHttpRequest<MAX_BODY_SIZE>::getError() const
{
   ErrorString errorString;
   switch(m_error)
   {
      case Error::OK:
         break;

      case Error::TIMEOUT:
         errorString = AHS_F("Timeout occurred while waiting for data");
         break;

      case Error::CANNOT_HANDLE_HTTP_METHOD:
         errorString = AHS_F("Don't know how to handle HTTP method: \"");
         errorString += m_errorDetail;
         errorString += AHS_F("\"");
         break;

      case Error::PARSE_ERROR_INVALID_HTTP_VERSION:
         errorString = AHS_F("Invalid HTTP version: \"");
         errorString += m_errorDetail;
         errorString += AHS_F("\"");
         break;

      case Error::PARSE_ERROR_NO_RESOURCE:
         errorString = AHS_F("No resource specified.");
         break;

      default:
         break;
   }

   return errorString;
}

#ifndef ARDUINO_HTTP_SERVER_NO_BASIC_AUTH
template <size_t MAX_BODY_SIZE>
bool ArduinoHttpServer::StreamHttpRequest<MAX_BODY_SIZE>::authenticate(const char *username, const char *password) const
{
   if (m_authorizationField.getType() == HttpField::Type::NOT_SUPPORTED)
   {
      return false;
   }

   // HTTP value: "<Type> <Base 64 encoded credentials>"
   // Retrieve type and verify wether it is basic authorization.
   if(!(m_authorizationField.getSubValueString(0) == HttpField::BASIC_AUTH_TYPE_STR))
   {
      DEBUG_ARDUINO_HTTP_SERVER_PRINT("Unsupported authentication type: ");
      DEBUG_ARDUINO_HTTP_SERVER_PRINTLN(m_authorizationField.getSubValueString(0).cStr());
      return false;
   }

   FixString<128U> combinedInput(username);
   combinedInput += AHS_F(":");
   combinedInput += password;

   const int encodedLength = Base64.encodedLength(combinedInput.length());

   char encodedString[encodedLength+1]; // Base64 makes sure _encodedString_ is zero terminated.
   Base64.encode(encodedString, const_cast<char*>(combinedInput.cStr()), combinedInput.length());
   
   DEBUG_ARDUINO_HTTP_SERVER_PRINT("Credentials string in client supplied auth: ");
   DEBUG_ARDUINO_HTTP_SERVER_PRINTLN(m_authorizationField.getSubValueString(1).cStr() );

   if ( m_authorizationField.getSubValueString(1) == encodedString )
   {
      return true;
   }

   return false;
}
#endif

#endif // __ArduinoHttpServer__StreamHttpRequest__