From 4220d4462545e07c59d19442d18e438e55d62ec6 Mon Sep 17 00:00:00 2001 From: Kenneth Barbour Date: Thu, 15 Feb 2018 11:37:46 -0500 Subject: [PATCH] gitignore, simpler directory structure and Makefile --- .gitignore | 4 + Makefile | 6 +- README.md | 4 + {libraries/HttpServer => src}/HttpHeaders.cpp | 19 +++- {libraries/HttpServer => src}/HttpHeaders.h | 0 src/HttpRequest.cpp | 92 +++++++++++++++++++ src/HttpRequest.h | 31 +++++++ test/dummy/DummyStream.h | 20 ++-- test/test_DummyStream.cpp | 51 ++++++++++ test/test_HttpHeaders.cpp | 35 +++---- test/test_HttpRequest.cpp | 32 +++++++ 11 files changed, 266 insertions(+), 28 deletions(-) create mode 100644 .gitignore create mode 100644 README.md rename {libraries/HttpServer => src}/HttpHeaders.cpp (70%) rename {libraries/HttpServer => src}/HttpHeaders.h (100%) create mode 100644 src/HttpRequest.cpp create mode 100644 src/HttpRequest.h create mode 100644 test/test_DummyStream.cpp create mode 100644 test/test_HttpRequest.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a7c089a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.build +.deps +run_tests +*.swp diff --git a/Makefile b/Makefile index 008da32..cce4c6d 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..790f8eb --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +#TODO: +* header name should be case-insensitive +* HttpRequest.capture() handle improperly formed request +* HttpRequest.capture() handle running out of stream diff --git a/libraries/HttpServer/HttpHeaders.cpp b/src/HttpHeaders.cpp similarity index 70% rename from libraries/HttpServer/HttpHeaders.cpp rename to src/HttpHeaders.cpp index 4b827a3..adeeac9 100644 --- a/libraries/HttpServer/HttpHeaders.cpp +++ b/src/HttpHeaders.cpp @@ -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); diff --git a/libraries/HttpServer/HttpHeaders.h b/src/HttpHeaders.h similarity index 100% rename from libraries/HttpServer/HttpHeaders.h rename to src/HttpHeaders.h diff --git a/src/HttpRequest.cpp b/src/HttpRequest.cpp new file mode 100644 index 0000000..b0115b1 --- /dev/null +++ b/src/HttpRequest.cpp @@ -0,0 +1,92 @@ +#include "HttpRequest.h" +#include + +#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); +} diff --git a/src/HttpRequest.h b/src/HttpRequest.h new file mode 100644 index 0000000..b95db5d --- /dev/null +++ b/src/HttpRequest.h @@ -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; +}; diff --git a/test/dummy/DummyStream.h b/test/dummy/DummyStream.h index 67c3d2b..713c483 100644 --- a/test/dummy/DummyStream.h +++ b/test/dummy/DummyStream.h @@ -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 diff --git a/test/test_DummyStream.cpp b/test/test_DummyStream.cpp new file mode 100644 index 0000000..ac7c898 --- /dev/null +++ b/test/test_DummyStream.cpp @@ -0,0 +1,51 @@ +#include "catch.hpp" +#include "dummy/DummyStream.h" +#include + +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); +} diff --git a/test/test_HttpHeaders.cpp b/test/test_HttpHeaders.cpp index 2be9f43..23c11a1 100644 --- a/test/test_HttpHeaders.cpp +++ b/test/test_HttpHeaders.cpp @@ -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")); +}*/ diff --git a/test/test_HttpRequest.cpp b/test/test_HttpRequest.cpp new file mode 100644 index 0000000..65d1b4d --- /dev/null +++ b/test/test_HttpRequest.cpp @@ -0,0 +1,32 @@ +#include "catch.hpp" +#include "HttpRequest.h" +#include + +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")); +} +