gitignore, simpler directory structure and Makefile

feature/UrlUtils
Kenneth Barbour 2018-02-15 11:37:46 -05:00
parent 1827b1c558
commit 4220d44625
11 changed files with 266 additions and 28 deletions

4
.gitignore vendored 100644
View File

@ -0,0 +1,4 @@
.build
.deps
run_tests
*.swp

View File

@ -1,11 +1,11 @@
MODULES= test libraries/HttpServer
MODULES= src test
SOURCES ?= $(wildcard $(addsuffix /*.cpp, $(MODULES)))
OBJECTS := $(addsuffix .o, $(addprefix .build/, $(basename $(SOURCES))))
DEPFILES := $(subst .o,.dep, $(subst .build/,.deps/, $(OBJECTS)))
TESTCPPFLAGS = -D_TEST_ -Ilibraries/HttpServer -Itest -Iarduino
TESTCPPFLAGS = -D_TEST_ $(addprefix -I, $(MODULES)) -Iarduino
CPPDEPFLAGS = -MMD -MP -MF .deps/$(basename $<).dep
TEST_TARGET=test_HttpServer
TEST_TARGET=run_tests
.PHONY: all test clean

4
README.md 100644
View File

@ -0,0 +1,4 @@
#TODO:
* header name should be case-insensitive
* HttpRequest.capture() handle improperly formed request
* HttpRequest.capture() handle running out of stream

View File

@ -14,14 +14,27 @@ HttpHeaders::HttpHeaders() : _n(0) {
}
void HttpHeaders::set(const char* name, const char* value) {
int name_len = strlen(name);
int value_len = strlen(value);
// search for existing node with name and append value
for (int i = 0; i < count(); i++) {
if (strcmp(name, headers[i].name) != 0)
continue;
free(headers[i].value);
headers[i].value = (char *) malloc(sizeof(char) * strlen(value));
strcpy(headers[i].value, value);
int old_value_len = strlen(headers[i].value);
headers[i].value = (char *) realloc(headers[i].value, old_value_len + value_len + 2);
strcpy(headers[i].value+old_value_len, ", ");
strcpy(headers[i].value+old_value_len+2, value);
return;
}
// Grow headers if needed
if (_n && (_n % HTTPHEADERS_SIZE) == 0) {
headers = (HttpHeaderNode*) realloc(headers, sizeof(HttpHeaderNode) * HTTPHEADERS_SIZE * (_n / HTTPHEADERS_SIZE + 1));
}
// create and store new node with name and value
headers[_n].name = (char *) malloc(sizeof(char) * strlen(name));
headers[_n].value = (char *) malloc(sizeof(char) * strlen(value));
strcpy(headers[_n].name, name);

View File

@ -0,0 +1,92 @@
#include "HttpRequest.h"
#include <iostream>
#define HTTPREQUEST_TRIM(stream) while(stream.peek()==' ') { stream.read(); }
#define HTTPREQUEST_BUFFER_UNTIL(buff, strm, stop, n)\
for (n=0; strm.peek() != stop;n++) \
{ buff[n] = (char) strm.read(); }
void HttpRequest::capture()
{
long int i, k;
char buffer[HTTPREQUEST_MAX_MESSAGE_SIZE] = {0};
HTTPREQUEST_TRIM(client);
// Read method until ' '
//for (i=0; client.peek() != ' '; i++) buffer[i] = (char) client.read();
HTTPREQUEST_BUFFER_UNTIL(buffer, client, ' ', i);
strncpy(method, buffer, HTTPREQUEST_METHOD_SIZE);
for (k=0; k <= i; k++) buffer[k] = '\0'; //reset buffer
HTTPREQUEST_TRIM(client);
// Read url until ' '
for (i=0; client.peek() != ' '; i++) buffer[i] = (char) client.read();
url = (char*) malloc( i + 1 );
strcpy(url, buffer);
for (k=0; k <= i; k++) buffer[k] = '\0'; //reset buffer
HTTPREQUEST_TRIM(client);
// Read httpver until '\r'
for (i=0; client.peek() != '\r'; i++) buffer[i] = (char) client.read();
strncpy(httpver, buffer, HTTPREQUEST_HTTPVER_SIZE);
for (k=0; k <= i; k++) buffer[k] = '\0'; //reset buffer
// discard line break
if (client.peek() == '\r') client.read();
if (client.peek() == '\n') client.read();
//read each line, parsing headers, discarding \r\n until a blank line is reached
while (client.peek() != '\r' && client.peek() != '\n') {
for (i=0; client.peek() != ':'; i++) // copy client contents into buffer
buffer[i] = (char) client.read();
client.read(); // discard the ':'
HTTPREQUEST_TRIM(client); // discard whitespace
buffer[i++] = '\0';
k = i; // start of value
for (; client.peek() != '\r'; i++) buffer[i] = (char) client.read();
headers.set(buffer, (buffer + k) );
for (k=0; k <= i; k++) buffer[k] = '\0'; //reset buffer
// discard next line break
if (client.peek() == '\r') client.read();
if (client.peek() == '\n') client.read();
}
client.read();
if (client.peek() == '\n') client.read(); // consume \r\n
// Determine or guess the length of the message
long int content_length;
if (headers.has("Content-Length") && !headers.has("Transfer-Encoding")) {
char* length_hdr = headers.get("Content-Length");
char* length_hdr_end = strlen(length_hdr) + length_hdr;
content_length = strtol(length_hdr, &length_hdr_end, 10);
} else {
content_length = HTTPREQUEST_MAX_MESSAGE_SIZE;
}
// copy rest of message
message = (char*) malloc( content_length + 1);
if (!message) {
//todo handle out of memory
}
for (i = 0; i < content_length && client.available(); i++) {
message[i] = (char) client.read();
}
message[i] = '\0';
// handle content_length too short
if (client.available()) {
//todo: content length too short
}
message_length = i;
}
HttpRequest::~HttpRequest()
{
if (url) free(url);
if (message) free(message);
}

31
src/HttpRequest.h 100644
View File

@ -0,0 +1,31 @@
#pragma once
#include "HttpHeaders.h"
#ifndef HTTPREQUEST_MAX_MESSAGE_SIZE
#define HTTPREQUEST_MAX_MESSAGE_SIZE 512
#endif
#ifdef _TEST_
#include "dummy/DummyStream.h"
#else
#include "Arduino.h"
#endif
#define HTTPREQUEST_METHOD_SIZE 8
#define HTTPREQUEST_HTTPVER_SIZE 9
class HttpRequest
{
public:
HttpRequest(Stream& s)
:client(s), method(), url(), httpver(), headers(), message() {};
~HttpRequest();
Stream& client;
void capture();
char method [HTTPREQUEST_METHOD_SIZE];
char * url;
char httpver [HTTPREQUEST_HTTPVER_SIZE];
HttpHeaders headers;
long int message_length;
char * message;
};

View File

@ -1,17 +1,25 @@
#pragma once
#include "stdlib.h"
#include "stdint.h"
#include "string.h"
#include "Arduino.h"
class Stream
{
public:
virtual int available() = 0;
virtual int read() = 0;
virtual int peek() = 0;
virtual size_t write(uint8_t) = 0;
};
class DummyStream: public Stream
{
public:
DummyStream(char* buff): buffer(buffer), pos(0) {};
char * buffer;
DummyStream(const char* buff): buffer(buff), pos(0) {};
const char * buffer;
int pos;
virtual int available() { return (buffer[pos] != '\0'); }
virtual int available() { return (strlen(buffer) - pos); }
virtual int read() { return buffer[pos++]; }
virtual int peek() { return buffer[pos]; }
virtual size_t write(uint8_t c) { return 0; } // gone forever
};
extern DummyStream Stream

View File

@ -0,0 +1,51 @@
#include "catch.hpp"
#include "dummy/DummyStream.h"
#include <iostream>
TEST_CASE("DummyStream is availble","[DummyStream]")
{
const char* input = "Foo\nBar\nBaz\n";
DummyStream stream(input);
REQUIRE(strlen(input) > 0);
REQUIRE(stream.available());
}
TEST_CASE("DummyStream is not available","[DummyStream]")
{
const char input[1] = {0};
DummyStream stream(input);
REQUIRE(strlen(input) == 0);
CHECK(stream.available()==0);
}
TEST_CASE("DummyStream read","[DummyStream]")
{
const char* input = "Foo";
DummyStream stream(input);
REQUIRE(stream.available() == 3);
REQUIRE(stream.read() == 'F');
REQUIRE(stream.available() == 2);
REQUIRE(stream.read() == 'o');
REQUIRE(stream.available() == 1);
REQUIRE(stream.read() == 'o');
REQUIRE(stream.available() == 0);
}
TEST_CASE("DummyStream peek","[DummyStream]")
{
const char* input = "Bar";
DummyStream stream(input);
REQUIRE(stream.peek() == 'B');
REQUIRE(stream.available() == 3);
REQUIRE(stream.read() == 'B');
REQUIRE(stream.peek() == 'a');
REQUIRE(stream.available() == 2);
}
TEST_CASE("DummyStream is a Stream","[DummyStream]")
{
const char* input = "Baz";
Stream* stream = new DummyStream(input);
delete(stream);
}

View File

@ -2,6 +2,8 @@
#include "HttpHeaders.h"
#include "string.h"
using Catch::Matchers::Equals;
TEST_CASE("Empty HttpHeaders","[HttpHeaders]") {
HttpHeaders headers;
@ -20,32 +22,33 @@ TEST_CASE("Get and set headers","[HttpHeaders]"){
CHECK(headers.count() == 1);
}
TEST_CASE("Overwrite header with smaller header","[HttpHeaders]"){
TEST_CASE("Append headers","[HttpHeaders]"){
HttpHeaders headers;
headers.set("foo","sushi");
headers.set("foo","soy");
CHECK(strcmp(headers.get("foo"),"soy") == 0);
CHECK_THAT(headers.get("foo"), Equals("sushi, soy"));
CHECK(headers.count() == 1);
}
TEST_CASE("Overwrite header with larger header","[HttpHeaders]"){
HttpHeaders headers;
headers.set("foo","pear");
headers.set("foo","watermelon");
CHECK(strcmp(headers.get("foo"),"watermelon") == 0);
CHECK(headers.count()==1);
}
TEST_CASE("Force headers to grow","[HttpHeaders]") {
HttpHeaders headers;
char * name = "header_";
/*
for (int i = 0; i < 30; i++) {
char name[8] = "";
strcpy(name, "header");
for (int i = 0; i < 26; i++) {
name[6] = 0x41 + i;
headers.set(name,"Foo");
REQUIRE(strcmp(headers.get(name),"Foo") == 0);
}
*/
CHECK(headers.count() == (i+1) );
}
}
/* TODO: enable this test case while fixing header case sensitivity
TEST_CASE("Header name is case insensitive","[HttpHeaders]") {
HttpHeaders headers;
headers.set("Foo","bar");
CHECK(headers.has("foo"));
}*/

View File

@ -0,0 +1,32 @@
#include "catch.hpp"
#include "HttpRequest.h"
#include <iostream>
using Catch::Matchers::Equals;
TEST_CASE("Create and capture an HTTP Request","[HttpRequest]") {
const char * req_str = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n";
DummyStream client(req_str);
HttpRequest req(client);
req.capture();
CHECK_THAT(req.method, Equals("GET"));
CHECK_THAT(req.url, Equals("/"));
CHECK_THAT(req.httpver, Equals("HTTP/1.1"));
REQUIRE(req.headers.has("Host"));
CHECK_THAT(req.headers.get("Host"), Equals("localhost"));
}
TEST_CASE("Capture a complicated HTTP Request","[HttpRequest]") {
const char * req_str = "POST /foo HTTP/1.1\r\nHost: localhost\r\nContent-Length: 11\r\n\r\nfoo=bar&baz";
DummyStream client(req_str);
HttpRequest req(client);
req.capture();
CHECK_THAT(req.url, Equals("/foo"));
CHECK_THAT(req.headers.get("Content-Length"), Equals("11"));
CHECK_THAT(req.message, Equals("foo=bar&baz"));
}