HTTP: started a new http client and server module

Martin Gerhardy 2020-01-14 21:15:14 +01:00
parent b72ff1f24b
commit 9d88352a34
43 changed files with 2260 additions and 1 deletions

View File

@ -3,6 +3,7 @@
# files will not get copied
add_subdirectory(core)
add_subdirectory(http)
add_subdirectory(commonlua)
add_subdirectory(mail)
add_subdirectory(math)

View File

@ -8,6 +8,7 @@
#include "core/Log.h"
#include "core/Assert.h"
#include "backend/entity/ai/AILoader.h"
#include "http/HttpServer.h"
namespace backend {

View File

@ -246,6 +246,8 @@ public:
uint64_t lifetimeInSeconds() const;
float lifetimeInSecondsf() const;
AppState state() const;
/**
* @return the millis since the epoch
*/
@ -336,6 +338,10 @@ inline BindingContext App::bindingContext() const {
return _bindingContext;
}
inline AppState App::state() const {
return _curState;
}
}
namespace io {

View File

@ -7,6 +7,7 @@
#include "Assert.h"
#include <stdint.h>
#include <type_traits>
#include <new>
#include <SDL_stdinc.h>
#ifdef _WIN32
#undef max

View File

@ -12,6 +12,8 @@
namespace core {
namespace priv {
struct EqualCompare {
template<typename T>
inline bool operator() (const T& lhs, const T& rhs) const {
@ -19,10 +21,12 @@ struct EqualCompare {
}
};
}
/**
* @brief Fixed element amount map
*/
template<typename KEYTYPE, typename VALUETYPE, size_t BUCKETSIZE, typename HASHER, typename COMPARE = EqualCompare>
template<typename KEYTYPE, typename VALUETYPE, size_t BUCKETSIZE, typename HASHER, typename COMPARE = priv::EqualCompare>
class Map {
public:
struct KeyValue {

View File

@ -0,0 +1,35 @@
set(SRCS
Http.h Http.cpp
HttpClient.h HttpClient.cpp
HttpHeader.h HttpHeader.cpp
HttpMethod.h
HttpParser.h HttpParser.cpp
HttpQuery.h
HttpResponse.h
HttpServer.h HttpServer.cpp
HttpStatus.h HttpStatus.cpp
Network.h Network.cpp.h Network.cpp
ResponseParser.h ResponseParser.cpp
RequestParser.h RequestParser.cpp
Request.h Request.cpp
Url.h Url.cpp
)
set(LIB http)
engine_add_module(TARGET ${LIB} SRCS ${SRCS} DEPENDENCIES core)
set(TEST_SRCS
tests/HttpClientTest.cpp
tests/HttpHeaderTest.cpp
tests/HttpServerTest.cpp
tests/UrlTest.cpp
tests/ResponseParserTest.cpp
tests/RequestParserTest.cpp
)
gtest_suite_sources(tests ${TEST_SRCS})
gtest_suite_deps(tests ${LIB})
gtest_suite_begin(tests-${LIB} TEMPLATE ${ROOT_DIR}/src/modules/core/tests/main.cpp.in)
gtest_suite_sources(tests-${LIB} ${TEST_SRCS} ../core/tests/AbstractTest.cpp)
gtest_suite_deps(tests-${LIB} ${LIB})
gtest_suite_end(tests-${LIB})

26
src/modules/http/Http.cpp Normal file
View File

@ -0,0 +1,26 @@
/**
* @file
*/
#include "Http.h"
#include "Url.h"
#include "Request.h"
namespace http {
ResponseParser GET(const char *url) {
Url u(url);
Request request(u, HttpMethod::GET);
return request.execute();
}
ResponseParser POST(const char *url, const char *body) {
Url u(url);
Request request(u, HttpMethod::POST);
if (body != nullptr) {
request.body(body);
}
return request.execute();
}
}

14
src/modules/http/Http.h Normal file
View File

@ -0,0 +1,14 @@
/**
* @file
*/
#pragma once
#include "ResponseParser.h"
namespace http {
ResponseParser GET(const char *url);
ResponseParser POST(const char *url, const char *body = nullptr);
}

View File

@ -0,0 +1,40 @@
/**
* @file
*/
#include "HttpClient.h"
#include "Request.h"
#include "core/Log.h"
namespace http {
HttpClient::HttpClient(const std::string &baseUrl) : _baseUrl(baseUrl) {
}
bool HttpClient::setBaseUrl(const std::string &baseUrl) {
Url u(baseUrl.c_str());
_baseUrl = baseUrl;
return u.valid();
}
ResponseParser HttpClient::get(const char *msg, ...) {
va_list ap;
constexpr std::size_t bufSize = 2048;
char text[bufSize];
va_start(ap, msg);
SDL_snprintf(text, bufSize, "%s", _baseUrl.c_str());
SDL_vsnprintf(text + _baseUrl.size(), bufSize - _baseUrl.size(), msg, ap);
text[sizeof(text) - 1] = '\0';
va_end(ap);
Url u(text);
if (!u.valid()) {
Log::error("Invalid url given: '%s'", text);
return ResponseParser(nullptr, 0u);
}
Request request(u, HttpMethod::GET);
return request.execute();
}
}

View File

@ -0,0 +1,28 @@
/**
* @file
*/
#pragma once
#include "ResponseParser.h"
#include <SDL_stdinc.h>
#include <string>
namespace http {
class HttpClient {
private:
std::string _baseUrl;
public:
HttpClient(const std::string &baseUrl = "");
/**
* @brief Change the base url for the http client that is put in front of every request
* @return @c false if the given base url is not a valid url
*/
bool setBaseUrl(const std::string &baseUrl);
ResponseParser get(SDL_PRINTF_FORMAT_STRING const char *msg, ...) SDL_PRINTF_VARARG_FUNC(2);
};
}

View File

@ -0,0 +1,23 @@
/**
* @file
*/
#include "HttpHeader.h"
namespace http {
bool buildHeaderBuffer(char *headers, size_t len, const HeaderMap& _headers) {
char *headersP = headers;
size_t headersSize = len;
for (const auto& h : _headers) {
const size_t written = SDL_snprintf(headersP, headersSize, "%s: %s\r\n", h->key, h->value);
if (written >= headersSize) {
return false;
}
headersSize -= written;
headersP += written;
}
return true;
}
}

View File

@ -0,0 +1,29 @@
/**
* @file
*/
#pragma once
#include "core/collection/Map.h"
namespace http {
using HeaderMap = core::CharPointerMap;
namespace header {
static constexpr const char *CONTENT_ENCODING = "Content-Encoding";
static constexpr const char *ACCEPT_ENCODING = "Accept-Encoding";
static constexpr const char *USER_AGENT = "User-agent";
static constexpr const char *CONNECTION = "Connection";
static constexpr const char *KEEP_ALIVE = "Keep-Alive";
static constexpr const char *CONTENT_TYPE = "Content-Type";
static constexpr const char *ACCEPT = "Accept";
static constexpr const char *SERVER = "Server";
static constexpr const char *HOST = "Host";
static constexpr const char *CONTENT_LENGTH = "Content-length";
}
extern bool buildHeaderBuffer(char *buf, size_t len, const HeaderMap& headers);
}

View File

@ -0,0 +1,13 @@
/**
* @file
*/
#pragma once
namespace http {
enum class HttpMethod {
GET, POST, NOT_SUPPORTED
};
}

View File

@ -0,0 +1,128 @@
/**
* @file
*/
#include "HttpParser.h"
#include "core/String.h"
namespace http {
HttpParser::HttpParser(uint8_t* buffer, const size_t bufferSize) :
buf(buffer), bufSize(bufferSize) {
}
HttpParser& HttpParser::operator=(HttpParser&& other) {
buf = other.buf;
bufSize = other.bufSize;
_valid = other._valid;
protocolVersion = other.protocolVersion;
headers = HTTP_PARSER_NEW_BASE_CHARPTR_MAP(other.headers);
content = other.content;
contentLength = other.contentLength;
other.buf = nullptr;
other.bufSize = 0u;
other.protocolVersion = nullptr;
other.headers.clear();
other.content = nullptr;
other.contentLength = 0u;
return *this;
}
HttpParser::HttpParser(HttpParser&& other) {
buf = other.buf;
bufSize = other.bufSize;
_valid = other._valid;
protocolVersion = other.protocolVersion;
headers = HTTP_PARSER_NEW_BASE_CHARPTR_MAP(other.headers);
content = other.content;
contentLength = other.contentLength;
other.buf = nullptr;
other.bufSize = 0u;
other.protocolVersion = nullptr;
other.headers.clear();
other.content = nullptr;
other.contentLength = 0u;
}
HttpParser& HttpParser::operator=(const HttpParser& other) {
buf = (uint8_t*)SDL_malloc(other.bufSize);
SDL_memcpy(buf, other.buf, other.bufSize);
bufSize = other.bufSize;
_valid = other._valid;
protocolVersion = HTTP_PARSER_NEW_BASE(other.protocolVersion);
headers = HTTP_PARSER_NEW_BASE_CHARPTR_MAP(other.headers);
content = HTTP_PARSER_NEW_BASE(other.content);
contentLength = other.contentLength;
return *this;
}
HttpParser::HttpParser(const HttpParser& other) {
buf = (uint8_t*)SDL_malloc(other.bufSize);
SDL_memcpy(buf, other.buf, other.bufSize);
bufSize = other.bufSize;
_valid = other._valid;
protocolVersion = HTTP_PARSER_NEW_BASE(other.protocolVersion);
headers = HTTP_PARSER_NEW_BASE_CHARPTR_MAP(other.headers);
content = HTTP_PARSER_NEW_BASE(other.content);
contentLength = other.contentLength;
}
HttpParser::~HttpParser() {
SDL_free(buf);
buf = nullptr;
bufSize = 0;
}
size_t HttpParser::remainingBufSize(const char *bufPos) const {
if (bufPos < (const char *)buf) {
return 0u;
}
if (bufPos >= (const char *)buf + bufSize) {
return 0u;
}
const size_t alreadyRead = (size_t)((uint8_t*)bufPos - buf);
const size_t remaining = bufSize - alreadyRead;
return remaining;
}
char* HttpParser::getHeaderLine(char **buffer) {
return core::string::getBeforeToken(buffer, "\r\n", remainingBufSize(*buffer));
}
bool HttpParser::parseHeaders(char **bufPos) {
char *hdrPos = core::string::getBeforeToken(bufPos, "\r\n\r\n", remainingBufSize(*bufPos));
if (hdrPos == nullptr) {
return false;
}
// re-add one newline to simplify the code
// for key/value parsing of the header
const size_t hdrSize = SDL_strlen(hdrPos);
hdrPos[hdrSize + 0] = '\r';
hdrPos[hdrSize + 1] = '\n';
hdrPos[hdrSize + 2] = '\0';
for (;;) {
char *headerEntry = core::string::getBeforeToken(&hdrPos, "\r\n", remainingBufSize(hdrPos));
if (headerEntry == nullptr) {
break;
}
const char *var = core::string::getBeforeToken(&headerEntry, ": ", remainingBufSize(headerEntry));
const char *value = headerEntry;
headers.put(var, value);
}
return true;
}
}

View File

@ -0,0 +1,90 @@
/**
* @file
*/
#pragma once
#include "HttpHeader.h"
#include <stdint.h>
#define HTTP_PARSER_NEW_BASE(ptr) newBase(buf, other.buf, ptr)
#define HTTP_PARSER_NEW_BASE_CHARPTR_MAP(map) newBase(buf, other.buf, map)
namespace http {
/**
* @brief Base class for http response/request protocol messages.
*
* @note It does not copy any string, it modifies the input buffer string so make sure to hand in copies.
* The stored data are just pointers to the modified input buffer.
*/
class HttpParser {
protected:
static inline const char* newBase(const void* newBufPtr, const void* oldBufPtr, const void* oldPtr) {
if (oldPtr == nullptr || oldBufPtr == nullptr) {
return nullptr;
}
return (const char*)newBufPtr + (intptr_t)((const uint8_t*)oldPtr - (const uint8_t*)oldBufPtr);
}
static inline auto newBase(const void* newBufPtr, const void* oldBufPtr, const core::CharPointerMap& map) {
core::CharPointerMap newMap(map.size());
if (oldBufPtr == nullptr) {
return newMap;
}
for (auto i = map.begin(); i != map.end(); ++i) {
newMap.put(newBase(newBufPtr, oldBufPtr, i->key), newBase(newBufPtr, oldBufPtr, i->value));
}
return newMap;
}
uint8_t *buf = nullptr;
size_t bufSize = 0u;
bool _valid = false;
size_t remainingBufSize(const char *bufPos) const;
char* getHeaderLine(char **buffer);
bool parseHeaders(char **bufPos);
public:
/**
* @brief Parses a http response/request buffer
* @note The given memory is owned by this class. You may not
* release it on your own.
*/
HttpParser(uint8_t* buffer, const size_t bufferSize);
/**
* @brief Pointer to that part of the protocol header that stores
* the http version
*/
const char* protocolVersion = nullptr;
/**
* @brief The map key/value pairs are just pointers to the
* protocol header buffer. It's safe to copy this structure, but
* don't manually modify the @c headers map
*/
HeaderMap headers;
/**
* @brief The pointer to the data after the protocol header
*/
const char *content = nullptr;
/**
* @brief The length of the @c content buffer
*/
int contentLength = -1;
bool valid() const;
HttpParser(HttpParser&& other);
HttpParser(const HttpParser& other);
~HttpParser();
HttpParser& operator=(HttpParser&& other);
HttpParser& operator=(const HttpParser& other);
};
inline bool HttpParser::valid() const {
return _valid;
}
}

View File

@ -0,0 +1,23 @@
/**
* @file
*/
#pragma once
#include "core/collection/Map.h"
#include "core/Common.h"
#include "core/Log.h"
namespace http {
using HttpQuery = core::CharPointerMap;
#define HTTP_QUERY_GET_INT(name) \
const char *name##value; \
if (!request.query.get(CORE_STRINGIFY(name), name##value)) { \
Log::debug("Missing query parameter " #name); \
return; \
} \
int name = SDL_atoi(name##value)
}

View File

@ -0,0 +1,34 @@
/**
* @file
*/
#pragma once
#include "HttpStatus.h"
#include "HttpHeader.h"
#include <SDL_stdinc.h>
namespace http {
struct HttpResponse {
HeaderMap headers;
HttpStatus status = HttpStatus::Ok;
// the memory is managed by the server and freed after the response was sent.
const char *body = nullptr;
size_t bodySize = 0u;
// if the route handler sets this to false, the memory is not freed. Can be useful for static content
// like error pages.
bool freeBody = true;
void contentLength(size_t len) {
bodySize = len;
}
void setText(const char *body) {
this->body = body;
contentLength(strlen(body));
freeBody = false;
}
};
}

View File

@ -0,0 +1,381 @@
/**
* @file
*/
#include "HttpServer.h"
#include "RequestParser.h"
#include "core/Assert.h"
#include "core/ArrayLength.h"
#include "core/Log.h"
#include "Network.cpp.h"
#include "core/App.h"
#include <string.h>
#include <SDL_stdinc.h>
namespace http {
HttpServer::HttpServer() :
_socketFD(INVALID_SOCKET) {
FD_ZERO(&_readFDSet);
FD_ZERO(&_writeFDSet);
}
HttpServer::~HttpServer() {
core_assert(_socketFD == INVALID_SOCKET);
}
void HttpServer::setErrorText(HttpStatus status, const char *body) {
auto i = _errorPages.find((int)status);
if (i != _errorPages.end()) {
SDL_free((char*)i->value);
}
_errorPages.put((int)status, SDL_strdup(body));
}
HttpServer::Routes* HttpServer::getRoutes(HttpMethod method) {
if (method == HttpMethod::GET) {
return &_routes[0];
} else /* if (method == HttpMethod::POST) */ {
core_assert(method == HttpMethod::POST);
return &_routes[1];
}
}
void HttpServer::registerRoute(HttpMethod method, const char *path, RouteCallback callback) {
Routes* routes = getRoutes(method);
Log::info("Register callback for %s", path);
routes->put(path, callback);
}
bool HttpServer::unregisterRoute(HttpMethod method, const char *path) {
Routes* routes = getRoutes(method);
return routes->remove(path);
}
bool HttpServer::init(int16_t port) {
_socketFD = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if (_socketFD == INVALID_SOCKET) {
network_cleanup();
return false;
}
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = INADDR_ANY;
sin.sin_port = htons(port);
FD_ZERO(&_readFDSet);
FD_ZERO(&_writeFDSet);
int t = 1;
#ifdef _WIN32
if (setsockopt(_socketFD, SOL_SOCKET, SO_REUSEADDR, (char*) &t, sizeof(t)) != 0) {
#else
if (setsockopt(_socketFD, SOL_SOCKET, SO_REUSEADDR, &t, sizeof(t)) != 0) {
#endif
network_cleanup();
closesocket(_socketFD);
_socketFD = INVALID_SOCKET;
return false;
}
if (bind(_socketFD, (struct sockaddr *) &sin, sizeof(sin)) < 0) {
network_cleanup();
closesocket(_socketFD);
_socketFD = INVALID_SOCKET;
return false;
}
if (listen(_socketFD, 5) < 0) {
network_cleanup();
closesocket(_socketFD);
_socketFD = INVALID_SOCKET;
return false;
}
networkNonBlocking(_socketFD);
FD_SET(_socketFD, &_readFDSet);
return true;
}
HttpServer::ClientSocketsIter HttpServer::closeClient(ClientSocketsIter& iter) {
Client& client = *iter;
const SOCKET clientSocket = client.socket;
FD_CLR(clientSocket, &_readFDSet);
FD_CLR(clientSocket, &_writeFDSet);
closesocket(clientSocket);
client.socket = INVALID_SOCKET;
SDL_free(client.request);
SDL_free(client.response);
return _clientSockets.erase(iter);
}
bool HttpServer::update() {
fd_set readFDsOut;
fd_set writeFDsOut;
memcpy(&readFDsOut, &_readFDSet, sizeof(readFDsOut));
memcpy(&writeFDsOut, &_writeFDSet, sizeof(writeFDsOut));
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 0;
const int ready = select(FD_SETSIZE, &readFDsOut, &writeFDsOut, nullptr, &tv);
if (ready < 0) {
return false;
}
if (_socketFD != INVALID_SOCKET && FD_ISSET(_socketFD, &readFDsOut)) {
const SOCKET clientSocket = accept(_socketFD, nullptr, nullptr);
if (clientSocket != INVALID_SOCKET) {
FD_SET(clientSocket, &_readFDSet);
Client c;
c.socket = clientSocket;
_clientSockets.push_back(c);
networkNonBlocking(clientSocket);
}
}
for (ClientSocketsIter i = _clientSockets.begin(); i != _clientSockets.end();) {
Client& client = *i;
const SOCKET clientSocket = client.socket;
if (clientSocket == INVALID_SOCKET) {
i = closeClient(i);
continue;
}
if (FD_ISSET(clientSocket, &writeFDsOut)) {
if (!sendMessage(client) || client.finished()) {
i = closeClient(i);
} else {
++i;
}
continue;
}
if (!FD_ISSET(clientSocket, &readFDsOut)) {
++i;
continue;
}
constexpr const int BUFFERSIZE = 2048;
uint8_t recvBuf[BUFFERSIZE];
const network_return len = recv(clientSocket, recvBuf, BUFFERSIZE - 1, 0);
if (len < 0) {
i = closeClient(i);
continue;
}
if (len == 0) {
++i;
continue;
}
client.request = (uint8_t*)SDL_realloc(client.request, client.requestLength + len);
memcpy(client.request + client.requestLength, recvBuf, len);
client.requestLength += len;
if (client.requestLength == 0) {
++i;
continue;
}
// GET / HTTP/1.1\r\n\r\n
if (client.requestLength < 18) {
++i;
continue;
}
if (memcmp(client.request, "GET", 3) != 0 && memcmp(client.request, "POST", 4) != 0) {
FD_CLR(clientSocket, &_readFDSet);
FD_CLR(clientSocket, &readFDsOut);
assembleError(client, HttpStatus::NotImplemented);
++i;
continue;
}
if (client.requestLength > _maxRequestBytes) {
FD_CLR(clientSocket, &_readFDSet);
FD_CLR(clientSocket, &readFDsOut);
assembleError(client, HttpStatus::InternalServerError);
++i;
continue;
}
uint8_t *mem = (uint8_t *)SDL_malloc(client.requestLength);
memcpy(mem, client.request, client.requestLength);
const RequestParser request(mem, client.requestLength);
if (!request.valid()) {
++i;
continue;
}
FD_CLR(clientSocket, &_readFDSet);
FD_CLR(clientSocket, &readFDsOut);
HttpResponse response;
if (!route(request, response)) {
assembleError(client, HttpStatus::NotFound);
++i;
continue;
}
assembleResponse(client, response);
if (response.freeBody) {
SDL_free((char*)response.body);
}
}
return true;
}
void HttpServer::assembleError(Client& client, HttpStatus status) {
char buf[512];
SDL_snprintf(buf, sizeof(buf),
"HTTP/1.1 %i %s\r\n"
"Connection: close\r\n"
"Server: %s\r\n"
"\r\n",
(int)status,
toStatusString(status),
core::App::getInstance()->appname().c_str());
const char *errorPage = "";
_errorPages.get((int)status, errorPage);
const size_t responseSize = SDL_strlen(errorPage) + strlen(buf);
char *responseBuf = (char*)SDL_malloc(responseSize + 1);
SDL_snprintf(responseBuf, responseSize + 1, "%s%s", buf, errorPage);
client.setResponse(responseBuf, responseSize);
FD_SET(client.socket, &_writeFDSet);
}
void HttpServer::assembleResponse(Client& client, const HttpResponse& response) {
char headers[2048];
if (!buildHeaderBuffer(headers, lengthof(headers), response.headers)) {
assembleError(client, HttpStatus::InternalServerError);
return;
}
char buf[4096];
const int headerSize = SDL_snprintf(buf, sizeof(buf),
"HTTP/1.1 %i %s\r\n"
"Content-length: %u\r\n"
"%s"
"\r\n",
(int)response.status,
toStatusString(response.status),
(unsigned int)response.bodySize,
headers);
if (headerSize >= lengthof(buf)) {
assembleError(client, HttpStatus::InternalServerError);
return;
}
const size_t responseSize = response.bodySize + strlen(buf);
char *responseBuf = (char*)SDL_malloc(responseSize);
SDL_memcpy(responseBuf, buf, headerSize);
SDL_memcpy(responseBuf + headerSize, response.body, response.bodySize);
client.setResponse(responseBuf, responseSize);
Log::info("Response buffer of size %i", (int)responseSize);
FD_SET(client.socket, &_writeFDSet);
}
bool HttpServer::sendMessage(Client& client) {
core_assert(client.response != nullptr);
int remaining = (int)client.responseLength - (int)client.alreadySent;
if (remaining <= 0) {
return false;
}
const char* p = client.response + client.alreadySent;
const network_return sent = ::send(client.socket, p, remaining, 0);
if (sent < 0) {
Log::debug("Failed to send to the client");
return false;
}
if (sent == 0) {
return true;
}
remaining -= sent;
client.alreadySent += sent;
return remaining > 0;
}
bool HttpServer::route(const RequestParser& request, HttpResponse& response) {
Routes* routes = getRoutes(request.method);
Log::trace("lookup for %s", request.path);
auto i = routes->find(request.path);
if (i == routes->end()) {
// check if there is at least one other /
if (SDL_strchr(&request.path[1], '/')) {
char *path = SDL_strdup(request.path);
char *cut;
for (;;) {
cut = SDL_strrchr(path, '/');
if (cut == path) {
break;
}
*cut = '\0';
Log::trace("lookup for %s", path);
i = routes->find(path);
if (i != routes->end()) {
break;
}
}
SDL_free(path);
} else {
Log::debug("No route found for '%s'", request.path);
return false;
}
}
if (i == routes->end()) {
Log::debug("No route found for '%s'", request.path);
return false;
}
response.headers.put(header::CONTENT_TYPE, "text/plain");
response.headers.put(header::CONNECTION, "close");
response.headers.put(header::SERVER, core::App::getInstance()->appname().c_str());
// TODO urldecode of request data
//core::string::urlDecode(request.query);
i->value(request, &response);
return true;
}
void HttpServer::shutdown() {
const size_t l = lengthof(_routes);
for (size_t i = 0; i < l; ++i) {
_routes[i].clear();
}
for (ClientSocketsIter i = _clientSockets.begin(); i != _clientSockets.end();) {
i = closeClient(i);
}
for (auto i : _errorPages) {
SDL_free((char*)i->second);
}
_errorPages.clear();
FD_ZERO(&_readFDSet);
FD_ZERO(&_writeFDSet);
closesocket(_socketFD);
_socketFD = INVALID_SOCKET;
network_cleanup();
}
HttpServer::Client::Client() :
socket(INVALID_SOCKET) {
}
void HttpServer::Client::setResponse(char* responseBuf, size_t responseBufLength) {
core_assert(response == nullptr);
response = responseBuf;
responseLength = responseBufLength;
alreadySent = 0u;
}
bool HttpServer::Client::finished() const {
if (response == nullptr) {
return false;
}
return responseLength == alreadySent;
}
}

View File

@ -0,0 +1,90 @@
/**
* @file
*/
#pragma once
#include "HttpMethod.h"
#include "HttpResponse.h"
#include "HttpStatus.h"
#include "RequestParser.h"
#include "Network.h"
#include "HttpHeader.h"
#include "HttpQuery.h"
#include "core/collection/Map.h"
#include <stdint.h>
#include <functional>
#include <list>
#include <memory>
namespace http {
class RequestParser;
class HttpServer {
public:
using RouteCallback = std::function<void(const RequestParser& query, HttpResponse* response)>;
private:
SOCKET _socketFD;
fd_set _readFDSet;
fd_set _writeFDSet;
using Routes = core::Map<const char*, RouteCallback, 8, core::hashCharPtr, core::hashCharCompare>;
core::Map<int, const char*, 8, std::hash<int>> _errorPages;
Routes _routes[2];
size_t _maxRequestBytes = 1 * 1024 * 1024;
struct Client {
Client();
SOCKET socket;
uint8_t *request = nullptr;
size_t requestLength = 0u;
char* response = nullptr;
size_t responseLength = 0u;
size_t alreadySent = 0u;
void setResponse(char* responseBuf, size_t responseBufLength);
bool finished() const;
};
using ClientSockets = std::list<Client>;
using ClientSocketsIter = ClientSockets::iterator;
ClientSockets _clientSockets;
ClientSocketsIter closeClient (ClientSocketsIter& i);
bool route(const RequestParser& request, HttpResponse& response);
void assembleResponse(Client& client, const HttpResponse& response);
void assembleError(Client& client, HttpStatus status);
bool sendMessage(Client& client);
Routes* getRoutes(HttpMethod method);
public:
HttpServer();
~HttpServer();
void setMaxRequestSize(size_t maxBytes);
/**
* @param[in] body The status code body. The pointer is copied and then released by the server.
*/
void setErrorText(HttpStatus status, const char *body);
bool init(int16_t port = 8080);
bool update();
void shutdown();
void registerRoute(HttpMethod method, const char *path, RouteCallback callback);
bool unregisterRoute(HttpMethod method, const char *path);
};
inline void HttpServer::setMaxRequestSize(size_t maxBytes) {
_maxRequestBytes = maxBytes;
}
typedef std::shared_ptr<HttpServer> HttpServerPtr;
}

View File

@ -0,0 +1,22 @@
/**
* @file
*/
#include "HttpStatus.h"
namespace http {
const char* toStatusString(HttpStatus status) {
if (status == HttpStatus::InternalServerError) {
return "Internal Server Error";
} else if (status == HttpStatus::Ok) {
return "OK";
} else if (status == HttpStatus::NotFound) {
return "Not Found";
} else if (status == HttpStatus::NotImplemented) {
return "Not Implemented";
}
return "Unknown";
}
}

View File

@ -0,0 +1,31 @@
/**
* @file
*/
#pragma once
#include <stdint.h>
namespace http {
enum class HttpStatus : uint16_t {
Unknown = 0,
Ok = 200,
Created = 201,
Accepted = 202,
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404,
RequestUriTooLong = 414,
InternalServerError = 500,
NotImplemented = 501,
BadGateway = 502,
ServiceUnavailable = 503,
GatewayTimeout = 504,
HttpVersionNotSupported = 505,
};
extern const char* toStatusString(HttpStatus status);
}

View File

@ -0,0 +1,29 @@
/**
* @file
*/
#include "Network.h"
#include "Network.cpp.h"
bool networkInit() {
#ifdef WIN32
WSADATA wsaData;
const int wsaResult = WSAStartup(MAKEWORD(2,2), &wsaData);
if (wsaResult != NO_ERROR) {
return false;
}
#else
signal(SIGPIPE, SIG_IGN);
#endif
return true;
}
void networkNonBlocking(SOCKET socket) {
#ifdef O_NONBLOCK
fcntl(socket, F_SETFL, O_NONBLOCK);
#endif
#ifdef WIN32
unsigned long mode = 1;
ioctlsocket(socket, FIONBIO, &mode);
#endif
}

View File

@ -0,0 +1,31 @@
/**
* @file
*/
#pragma once
#ifdef WIN32
#define network_cleanup() WSACleanup()
#define network_return int
#else
#define network_return ssize_t
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <unistd.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/socket.h>
#include <net/if.h>
#include <netdb.h>
#include <signal.h>
#define closesocket close
#define INVALID_SOCKET -1
#define network_cleanup()
#endif
extern bool networkInit();
extern void networkNonBlocking(SOCKET socket);

View File

@ -0,0 +1,17 @@
/**
* @file
*/
#pragma once
#ifdef WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <winsock2.h>
#include <ws2tcpip.h>
#include <ws2spi.h>
#else
#define SOCKET int
#include <sys/select.h>
#endif

View File

@ -0,0 +1,155 @@
/**
* @file
*/
#include "Request.h"
#include "Url.h"
#include "core/App.h"
#include "core/Log.h"
#include "core/ArrayLength.h"
#include <string.h>
#include "Network.cpp.h"
namespace http {
Request::Request(const Url& url, HttpMethod method) :
_url(url), _socketFD(INVALID_SOCKET), _method(method) {
_headers.put(header::USER_AGENT, core::App::getInstance()->appname().c_str());
_headers.put(header::CONNECTION, "close");
// TODO:
// _headers.put(header::KEEP_ALIVE, "timeout=15");
// _headers.put(header::CONNECTION, "Keep-Alive");
_headers.put(header::ACCEPT_ENCODING, "gzip, deflate");
accept("*/*");
if (HttpMethod::POST == method && !_url.query.empty()) {
contentType("application/x-www-form-urlencoded");
body(_url.query.c_str());
}
}
ResponseParser Request::failed() {
closesocket(_socketFD);
_socketFD = INVALID_SOCKET;
return ResponseParser(nullptr, 0u);
}
ResponseParser Request::execute() {
if (!_url.valid()) {
Log::error("Invalid url given");
return ResponseParser(nullptr, 0u);
}
if (!networkInit()) {
Log::error("Failed to initialize the network");
return ResponseParser(nullptr, 0u);
}
_socketFD = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if (_socketFD == INVALID_SOCKET) {
Log::error("Failed to initialize the socket");
network_cleanup();
return ResponseParser(nullptr, 0u);
}
struct addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
struct addrinfo* results = nullptr;
const int ret = getaddrinfo(_url.hostname.c_str(), nullptr, &hints, &results);
if (ret != 0) {
Log::error("Failed to resolve host for %s", _url.hostname.c_str());
return failed();
}
const struct sockaddr_in* host_addr = (const struct sockaddr_in*) results->ai_addr;
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(_url.port);
memcpy(&sin.sin_addr, &host_addr->sin_addr, sizeof(sin.sin_addr));
freeaddrinfo(results);
if (connect(_socketFD, (const struct sockaddr *)&sin, sizeof(sin)) == -1) {
Log::error("Failed to connect to %s:%i", _url.hostname.c_str(), _url.port);
return failed();
}
char headers[1024];
if (!buildHeaderBuffer(headers, lengthof(headers), _headers)) {
Log::error("Failed to assemble request header");
return failed();
}
char message[4096];
if (_method == HttpMethod::GET) {
if (SDL_snprintf(message, sizeof(message),
"GET %s%s%s HTTP/1.1\r\n"
"Host: %s\r\n"
"%s"
"\r\n",
_url.path.c_str(),
(_url.query.empty() ? "" : "?"),
_url.query.c_str(),
_url.hostname.c_str(),
headers) >= lengthof(message)) {
Log::error("Failed to assemble request");
return failed();
}
} else if (_method == HttpMethod::POST) {
if (SDL_snprintf(message, sizeof(message),
"POST %s HTTP/1.1\r\n"
"Host: %s\r\n"
"%s"
"\r\n"
"%s",
_url.path.c_str(),
_url.hostname.c_str(),
headers,
_body) >= lengthof(message)) {
Log::error("Failed to assemble request");
return failed();
}
} else {
Log::error("Unsupported method");
return failed();
}
size_t sent = 0u;
while (sent < strlen(message)) {
const int ret = send(_socketFD, message + sent, strlen(message) - sent, 0);
if (ret < 0) {
Log::error("Failed to perform http request to %s", _url.url.c_str());
return failed();
}
sent += ret;
}
uint8_t *response = nullptr;
constexpr const int BUFFERSIZE = 1024 * 1024;
uint8_t *recvBuf = (uint8_t*)SDL_malloc(BUFFERSIZE);
ssize_t receivedLength = 0u;
size_t totalReceivedLength = 0u;
while ((receivedLength = recv(_socketFD, recvBuf, BUFFERSIZE, 0)) > 0) {
response = (uint8_t*)SDL_realloc(response, totalReceivedLength + receivedLength);
memcpy(response + totalReceivedLength, recvBuf, receivedLength);
totalReceivedLength += receivedLength;
Log::trace("received data: %i", (int)receivedLength);
}
SDL_free(recvBuf);
if (receivedLength < 0) {
Log::error("Failed to read http response from %s", _url.url.c_str());
return failed();
}
closesocket(_socketFD);
network_cleanup();
ResponseParser parser(response, totalReceivedLength);
const char *encoding;
if (parser.headers.get(header::CONTENT_ENCODING, encoding)) {
// TODO: gunzip
}
return parser;
}
}

View File

@ -0,0 +1,56 @@
/**
* @file
*/
#pragma once
#include "ResponseParser.h"
#include "HttpHeader.h"
#include "HttpMethod.h"
#include "Url.h"
#include "Network.h"
#include <string>
namespace http {
/**
* @note The pointers that are given for the headers must stay valid until execute() was called.
*/
class Request {
private:
const Url _url;
SOCKET _socketFD;
HttpMethod _method;
HeaderMap _headers;
const char *_body = "";
ResponseParser failed();
public:
Request(const Url& url, HttpMethod method);
Request& contentType(const char* mimeType);
Request& accept(const char* mimeType);
Request& header(const char* key, const char *value);
Request& body(const char *body);
ResponseParser execute();
};
inline Request& Request::body(const char *body) {
_body = body;
return *this;
}
inline Request& Request::contentType(const char* mimeType) {
header(header::CONTENT_TYPE, mimeType);
return *this;
}
inline Request& Request::accept(const char* mimeType) {
header(header::ACCEPT, mimeType);
return *this;
}
inline Request& Request::header(const char* key, const char *value) {
_headers.put(key, value);
return *this;
}
}

View File

@ -0,0 +1,127 @@
/**
* @file
*/
#include "RequestParser.h"
#include "core/String.h"
#include "HttpHeader.h"
namespace http {
RequestParser& RequestParser::operator=(RequestParser &&other) {
if (&other != this) {
Super::operator=(std::move(other));
query = HTTP_PARSER_NEW_BASE_CHARPTR_MAP(other.query);
other.query.clear();
method = other.method;
path = HTTP_PARSER_NEW_BASE(other.path);
}
return *this;
}
RequestParser::RequestParser(RequestParser &&other) :
Super(std::move(other)) {
query = HTTP_PARSER_NEW_BASE_CHARPTR_MAP(other.query);
other.query.clear();
method = other.method;
path = HTTP_PARSER_NEW_BASE(other.path);
}
RequestParser& RequestParser::operator=(const RequestParser& other) {
if (&other != this) {
Super::operator=(other);
query = HTTP_PARSER_NEW_BASE_CHARPTR_MAP(other.query);
method = other.method;
path = HTTP_PARSER_NEW_BASE(other.path);
}
return *this;
}
RequestParser::RequestParser(const RequestParser &other) :
Super(other) {
query = HTTP_PARSER_NEW_BASE_CHARPTR_MAP(other.query);
method = other.method;
path = HTTP_PARSER_NEW_BASE(other.path);
}
RequestParser::RequestParser(uint8_t* requestBuffer, size_t requestBufferSize)
: Super(requestBuffer, requestBufferSize) {
if (buf == nullptr || bufSize == 0) {
return;
}
char *bufPos = (char *)buf;
char *statusLine = getHeaderLine(&bufPos);
if (statusLine == nullptr) {
return;
}
char *methodStr = core::string::getBeforeToken(&statusLine, " ", remainingBufSize(bufPos));
if (methodStr == nullptr) {
return;
}
if (!strcmp(methodStr, "GET")) {
method = HttpMethod::GET;
} else if (!strcmp(methodStr, "POST")) {
method = HttpMethod::POST;
} else {
method = HttpMethod::NOT_SUPPORTED;
return;
}
char* request = core::string::getBeforeToken(&statusLine, " ", remainingBufSize(statusLine));
if (request == nullptr) {
return;
}
protocolVersion = statusLine;
if (protocolVersion == nullptr) {
return;
}
path = core::string::getBeforeToken(&request, "?", remainingBufSize(request));
if (path == nullptr) {
path = request;
} else {
char *queryString = request;
bool last = false;
for (;;) {
char *paramValue = core::string::getBeforeToken(&queryString, "&", remainingBufSize(queryString));
if (paramValue == nullptr) {
paramValue = queryString;
last = true;
}
char *key = core::string::getBeforeToken(&paramValue, "=", remainingBufSize(paramValue));
char *value = paramValue;;
if (key == nullptr) {
key = paramValue;
static const char *EMPTY = "";
value = (char*)EMPTY;
}
query.put(key, value);
if (last) {
break;
}
}
}
if (!parseHeaders(&bufPos)) {
return;
}
content = bufPos;
contentLength = remainingBufSize(bufPos);
if (method == HttpMethod::GET) {
_valid = contentLength == 0;
} else if (method == HttpMethod::POST) {
const char *value;
if (!headers.get(header::CONTENT_LENGTH, value)) {
_valid = false;
} else {
_valid = contentLength == SDL_atoi(value);
}
}
}
}

View File

@ -0,0 +1,32 @@
/**
* @file
*/
#pragma once
#include "HttpMethod.h"
#include "HttpParser.h"
#include "HttpQuery.h"
namespace http {
class RequestParser : public HttpParser {
private:
using Super = HttpParser;
public:
RequestParser(uint8_t* requestBuffer, size_t requestBufferSize);
// arrays are not supported as query parameters - but
// that's fine for our use case
HttpQuery query;
HttpMethod method = HttpMethod::NOT_SUPPORTED;
const char* path = nullptr;
RequestParser(RequestParser&& other);
RequestParser(const RequestParser& other);
RequestParser& operator=(RequestParser&& other);
RequestParser& operator=(const RequestParser& other);
};
}

View File

@ -0,0 +1,100 @@
/**
* @file
*/
#include "ResponseParser.h"
#include "core/String.h"
namespace http {
ResponseParser& ResponseParser::operator=(ResponseParser &&other) {
if (&other != this) {
Super::operator=(std::move(other));
status = other.status;
statusText = HTTP_PARSER_NEW_BASE(other.statusText);
}
return *this;
}
ResponseParser::ResponseParser(ResponseParser &&other) :
Super(std::move(other)) {
status = other.status;
statusText = HTTP_PARSER_NEW_BASE(other.statusText);
}
ResponseParser& ResponseParser::operator=(const ResponseParser& other) {
if (&other != this) {
Super::operator=(other);
status = other.status;
statusText = HTTP_PARSER_NEW_BASE(other.statusText);
}
return *this;
}
ResponseParser::ResponseParser(const ResponseParser &other) :
Super(other) {
status = other.status;
statusText = HTTP_PARSER_NEW_BASE(other.statusText);
}
/**
* https://tools.ietf.org/html/rfc2616
*
* @code
* HTTP/1.0 200 OK
* Server: SimpleHTTP/0.6 Python/2.7.17
* Date: Tue, 10 Jan 2020 13:37:42 GMT
* Content-type: text/html; charset=UTF-8
* Content-Length: 940
*
* <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"><html>
* [...]
* </html>
* @endcode
*/
ResponseParser::ResponseParser(uint8_t* responseBuffer, size_t responseBufferSize)
: Super(responseBuffer, responseBufferSize) {
if (buf == nullptr || bufSize == 0) {
return;
}
char *bufPos = (char *)buf;
char *statusLine = getHeaderLine(&bufPos);
if (statusLine == nullptr) {
return;
}
protocolVersion = core::string::getBeforeToken(&statusLine, " ", remainingBufSize(statusLine));
if (protocolVersion == nullptr) {
return;
}
const char *statusNumber = core::string::getBeforeToken(&statusLine, " ", remainingBufSize(statusLine));
if (statusNumber == nullptr) {
return;
}
status = (HttpStatus)SDL_atoi(statusNumber);
statusText = statusLine;
if (!parseHeaders(&bufPos)) {
Log::info("failed to parse the headers");
return;
}
content = bufPos;
contentLength = remainingBufSize(bufPos);
Log::debug("content length: %i", (int)contentLength);
const char *value;
if (!headers.get(header::CONTENT_LENGTH, value)) {
Log::info("no content-length header entry found");
_valid = false;
} else {
_valid = contentLength == SDL_atoi(value);
if (!_valid) {
Log::debug("content-length and received data differ: %i vs %s", (int)contentLength, value);
}
}
}
}

View File

@ -0,0 +1,30 @@
/**
* @file
*/
#pragma once
#include "HttpStatus.h"
#include "HttpHeader.h"
#include "HttpParser.h"
#include <stdint.h>
namespace http {
class ResponseParser : public HttpParser {
private:
using Super = HttpParser;
public:
HttpStatus status = HttpStatus::Unknown;
const char* statusText = nullptr;
ResponseParser(uint8_t* responseBuffer, size_t responseBufferSize);
ResponseParser(ResponseParser&& other);
ResponseParser(const ResponseParser& other);
ResponseParser& operator=(ResponseParser&& other);
ResponseParser& operator=(const ResponseParser& other);
};
}

161
src/modules/http/Url.cpp Normal file
View File

@ -0,0 +1,161 @@
/**
* @file
*/
#include "Url.h"
#include "core/String.h"
#include "core/Log.h"
namespace http {
void Url::parseSchema(char **strPtr) {
char* str = *strPtr;
char* pos = SDL_strchr(str, ':');
if (pos == nullptr) {
_valid = false;
return;
}
schema = std::string(str, pos - str);
++pos;
for (int i = 0; i < 2; ++i) {
if (*pos++ != '/') {
_valid = false;
break;
}
}
*strPtr = pos;
}
void Url::parseHostPart(char **strPtr) {
char* str = *strPtr;
char* endOfHostPort = SDL_strchr(str, '/');
size_t hostPortLength;
if (endOfHostPort == nullptr) {
hostPortLength = (size_t)strlen(str);
} else {
hostPortLength = (size_t)(intptr_t)(endOfHostPort - str);
*strPtr += hostPortLength + 1;
}
bool hasUser = false;
for (size_t i = 0u; i < hostPortLength; ++i) {
if (str[i] == '@') {
hasUser = true;
break;
}
}
char *p = str;
if (hasUser) {
while (*p != '\0' && *p != ':' && *p != '@') {
++p;
}
username = std::string(str, p - str);
if (*p == ':') {
str = ++p;
while (*p != '\0' && *p != '@') {
++p;
}
password = std::string(str, p - str);
}
if (*p != '\0') {
++p;
}
}
char *afterUser = p;
for (;;) {
if (*p == ':') {
// parse port
hostname = std::string(afterUser, p - afterUser);
++p;
afterUser = p;
for (;;) {
if (*p == '\0') {
const std::string buf(afterUser, p - afterUser);
port = core::string::toInt(buf);
*strPtr = p;
return;
} else if (*p == '/') {
const std::string buf(afterUser, p - afterUser);
port = core::string::toInt(buf);
break;
}
++p;
}
break;
} else if (*p == '\0') {
hostname = std::string(afterUser, p - afterUser);
*strPtr = p;
return;
} else if (*p == '/') {
hostname = std::string(afterUser, p - afterUser);
break;
}
++p;
}
if (*p != '/') {
_valid = false;
*strPtr = p;
return;
}
++p;
*strPtr = p;
}
void Url::parsePath(char **strPtr) {
char *pos = *strPtr;
char *p = pos;
while (*p && *p != '#' && *p != '?') {
++p;
}
path = std::string(pos, p - pos);
*strPtr += path.size();
path = "/" + path;
}
void Url::parseQuery(char **strPtr) {
char *pos = *strPtr;
if (*pos != '?') {
return;
}
char *p = ++pos;
while (*p && *p != '#') {
++p;
}
query = std::string(pos, p - pos);
*strPtr += query.size();
}
void Url::parseFragment(char **strPtr) {
char *pos = *strPtr;
if (*pos != '#') {
return;
}
++pos;
char *p = pos;
while (*p) p++;
fragment = std::string(pos, p - pos);
*strPtr += fragment.size();
}
Url::Url(const char *urlParam) :
url(core::string::toLower(urlParam)) {
char *strPtr = (char *)urlParam;
parseSchema(&strPtr);
if (!_valid) return;
parseHostPart(&strPtr);
if (!_valid) return;
parsePath(&strPtr);
if (!_valid) return;
parseQuery(&strPtr);
if (!_valid) return;
parseFragment(&strPtr);
}
Url::~Url() {
}
}

40
src/modules/http/Url.h Normal file
View File

@ -0,0 +1,40 @@
/**
* @file
*/
#pragma once
#include <stdint.h>
#include <string>
namespace http {
class Url {
private:
bool _valid = true;
void parseSchema(char **strPtr);
void parseHostPart(char **strPtr);
void parsePath(char **strPtr);
void parseQuery(char **strPtr);
void parseFragment(char **strPtr);
public:
explicit Url(const char *url);
~Url();
const std::string url;
std::string schema;
std::string username;
std::string password;
std::string hostname;
uint16_t port = 80;
std::string path;
std::string query;
std::string fragment;
bool valid() const;
};
inline bool Url::valid() const {
return _valid;
}
}

View File

@ -0,0 +1,37 @@
/**
* @file
*/
#include "core/tests/AbstractTest.h"
#include "http/HttpClient.h"
#include "http/HttpServer.h"
namespace http {
class HttpClientTest : public core::AbstractTest {
};
TEST_F(HttpClientTest, testSimple) {
_testApp->threadPool().enqueue([this] () {
http::HttpServer _httpServer;
_httpServer.init(8095);
_httpServer.registerRoute(http::HttpMethod::GET, "/", [] (const http::RequestParser& request, HttpResponse* response) {
response->setText("Success");
});
while (_testApp->state() == core::AppState::Running) {
_httpServer.update();
}
_httpServer.shutdown();
});
HttpClient client("http://localhost:8095");
ResponseParser response = client.get("/");
EXPECT_TRUE(response.valid());
const char *length;
EXPECT_TRUE(response.headers.get(http::header::CONTENT_LENGTH, length));
EXPECT_STREQ("7", length);
const char *type;
EXPECT_TRUE(response.headers.get(http::header::CONTENT_LENGTH, type));
EXPECT_STREQ("text/plain", length);
}
}

View File

@ -0,0 +1,32 @@
/**
* @file
*/
#include "core/tests/AbstractTest.h"
#include "http/HttpHeader.h"
namespace http {
class HttpHeaderTest : public core::AbstractTest {
};
TEST_F(HttpHeaderTest, testSingle) {
HeaderMap headers;
headers.put("Foo", "Bar");
char buf[1024];
EXPECT_TRUE(buildHeaderBuffer(buf, sizeof(buf), headers));
EXPECT_STREQ("Foo: Bar\r\n", buf);
}
TEST_F(HttpHeaderTest, testMultiple) {
HeaderMap headers;
headers.put("Foo", "Bar");
headers.put("Foo1", "Bar1");
headers.put("Foo2", "Bar2");
char buf[1024];
EXPECT_TRUE(buildHeaderBuffer(buf, sizeof(buf), headers));
EXPECT_STREQ("Foo1: Bar1\r\n" "Foo2: Bar2\r\n" "Foo: Bar\r\n", buf);
}
}

View File

@ -0,0 +1,22 @@
/**
* @file
*/
#include "core/tests/AbstractTest.h"
#include "http/HttpServer.h"
namespace http {
class HttpServerTest : public core::AbstractTest {
};
TEST_F(HttpServerTest, testSimple) {
HttpServer server;
EXPECT_TRUE(server.init(10101));
server.registerRoute(HttpMethod::GET, "/", [] (const http::RequestParser& request, HttpResponse* response) {
});
EXPECT_TRUE(server.unregisterRoute(HttpMethod::GET, "/"));
server.shutdown();
}
}

View File

@ -0,0 +1,69 @@
/**
* @file
*/
#include "core/tests/AbstractTest.h"
#include "http/RequestParser.h"
namespace http {
class RequestParserTest : public core::AbstractTest {
};
TEST_F(RequestParserTest, testSimple) {
char *buf = SDL_strdup(
"GET / HTTP/1.1\r\n"
"Host: localhost:8088\r\n"
"User-Agent: curl/7.67.0\r\n"
"Accept: */*\r\n"
"\r\n");
RequestParser request((uint8_t*)buf, strlen(buf));
EXPECT_TRUE(request.valid());
EXPECT_STREQ("HTTP/1.1", request.protocolVersion);
EXPECT_EQ(HttpMethod::GET, request.method);
EXPECT_STREQ("/", request.path);
EXPECT_EQ(request.headers.size(), 3u);
validateMapEntry(request.headers, header::HOST, "localhost:8088");
}
TEST_F(RequestParserTest, testCopy) {
char *buf = SDL_strdup(
"GET / HTTP/1.1\r\n"
"Host: localhost:8088\r\n"
"User-Agent: curl/7.67.0\r\n"
"Accept: */*\r\n"
"\r\n");
RequestParser request(nullptr, 0u);
{
RequestParser original((uint8_t*)buf, strlen(buf));
request = original;
}
EXPECT_TRUE(request.valid());
EXPECT_STREQ("HTTP/1.1", request.protocolVersion);
EXPECT_EQ(HttpMethod::GET, request.method);
EXPECT_STREQ("/", request.path);
EXPECT_EQ(request.headers.size(), 3u);
validateMapEntry(request.headers, header::HOST, "localhost:8088");
}
TEST_F(RequestParserTest, testQuery) {
char *buf = SDL_strdup(
"GET /foo?param=value&param2=value&param3&param4=1 HTTP/1.1\r\n"
"Host: localhost:8088\r\n"
"User-Agent: curl/7.67.0\r\n"
"Accept: */*\r\n"
"\r\n");
RequestParser request((uint8_t*)buf, strlen(buf));
EXPECT_TRUE(request.valid());
EXPECT_STREQ("HTTP/1.1", request.protocolVersion);
EXPECT_EQ(HttpMethod::GET, request.method);
EXPECT_STREQ("/foo", request.path);
EXPECT_EQ(request.query.size(), 4u);
validateMapEntry(request.query, "param", "value");
validateMapEntry(request.query, "param2", "value");
validateMapEntry(request.query, "param3", "");
validateMapEntry(request.query, "param4", "1");
}
}

View File

@ -0,0 +1,74 @@
/**
* @file
*/
#include "core/tests/AbstractTest.h"
#include "http/Http.h"
#include "http/HttpServer.h"
namespace http {
class ResponseParserTest : public core::AbstractTest {
};
const char *ResponseBuf =
"HTTP/1.0 200 OK\r\n"
"Server: SimpleHTTP/0.6 Python/2.7.17\r\n"
"Date: Tue, 10 Jan 2020 13:37:42 GMT\r\n"
"Content-type: text/html; charset=UTF-8\r\n"
"Content-Length: 1337\r\n"
"\r\n"
"<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n"
"<html>\n"
"</html>\n";
TEST_F(ResponseParserTest, testGETSimple) {
ResponseParser response((uint8_t*)SDL_strdup(ResponseBuf), strlen(ResponseBuf));
ASSERT_EQ(http::HttpStatus::Ok, response.status) << response.statusText;
EXPECT_GE(response.headers.size(), 3u);
validateMapEntry(response.headers, header::SERVER, "SimpleHTTP/0.6 Python/2.7.17");
validateMapEntry(response.headers, header::CONTENT_TYPE, "text/html; charset=UTF-8");
validateMapEntry(response.headers, header::CONTENT_LENGTH, "1337");
EXPECT_EQ(71, response.contentLength);
EXPECT_FALSE(response.valid()) << "Invalid content size should make this response invalid";
const std::string r(response.content, response.contentLength);
EXPECT_STREQ(r.c_str(), "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n<html>\n</html>\n");
}
TEST_F(ResponseParserTest, testCopy) {
ResponseParser response(nullptr, 0u);
{
ResponseParser original((uint8_t*)SDL_strdup(ResponseBuf), strlen(ResponseBuf));
response = original;
}
ASSERT_EQ(http::HttpStatus::Ok, response.status) << response.statusText;
EXPECT_GE(response.headers.size(), 3u);
validateMapEntry(response.headers, header::SERVER, "SimpleHTTP/0.6 Python/2.7.17");
validateMapEntry(response.headers, header::CONTENT_TYPE, "text/html; charset=UTF-8");
validateMapEntry(response.headers, header::CONTENT_LENGTH, "1337");
EXPECT_EQ(71, response.contentLength);
EXPECT_FALSE(response.valid()) << "Invalid content size should make this response invalid";
const std::string r(response.content, response.contentLength);
EXPECT_STREQ(r.c_str(), "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n<html>\n</html>\n");
}
TEST_F(ResponseParserTest, testGETChunk) {
const char *ResponseBufChunk = "HTTP/1.1 200 OK\r\n"
"Content-length: 8\r\n"
"Server: server\r\n"
"Content-Type: application/chunk\r\n"
"Connection: close\r\n"
"\r\n"
"\a\a\a\a\a\a\a\a";
ResponseParser response((uint8_t*)SDL_strdup(ResponseBufChunk), strlen(ResponseBufChunk));
ASSERT_EQ(http::HttpStatus::Ok, response.status) << response.statusText;
EXPECT_GE(response.headers.size(), 4u);
validateMapEntry(response.headers, header::SERVER, "server");
validateMapEntry(response.headers, header::CONTENT_TYPE, "application/chunk");
validateMapEntry(response.headers, header::CONTENT_LENGTH, "8");
EXPECT_EQ(8, response.contentLength);
EXPECT_TRUE(response.valid()) << "Invalid content size should make this response invalid";
EXPECT_EQ('\a', response.content[0]);
}
}

View File

@ -0,0 +1,84 @@
/**
* @file
*/
#include "core/tests/AbstractTest.h"
#include "http/Url.h"
namespace http {
class UrlTest : public core::AbstractTest {
};
TEST_F(UrlTest, testSimple) {
const Url url("http://www.myhost.de");
EXPECT_EQ("http", url.schema);
EXPECT_EQ("www.myhost.de", url.hostname);
EXPECT_EQ(80, url.port);
EXPECT_EQ("/", url.path);
}
TEST_F(UrlTest, testPort) {
const Url url("http://www.myhost.de:8080");
EXPECT_EQ("http", url.schema);
EXPECT_EQ("www.myhost.de", url.hostname);
EXPECT_EQ(8080, url.port);
EXPECT_EQ("/", url.path);
}
TEST_F(UrlTest, testPath) {
const Url url("http://www.myhost.de/path");
EXPECT_EQ("http", url.schema);
EXPECT_EQ("www.myhost.de", url.hostname);
EXPECT_EQ(80, url.port);
EXPECT_EQ("/path", url.path);
}
TEST_F(UrlTest, testPortPath) {
const Url url("http://www.myhost.de:8080/path");
EXPECT_EQ("http", url.schema);
EXPECT_EQ("www.myhost.de", url.hostname);
EXPECT_EQ(8080, url.port);
EXPECT_EQ("/path", url.path);
}
TEST_F(UrlTest, testQuery) {
const Url url("http://www.myhost.de/path?query=1");
EXPECT_EQ("http", url.schema);
EXPECT_EQ("www.myhost.de", url.hostname);
EXPECT_EQ(80, url.port);
EXPECT_EQ("query=1", url.query);
EXPECT_EQ("/path", url.path);
}
TEST_F(UrlTest, testPortPathQuery) {
const Url url("http://www.myhost.de:8080/path?query=1&second=2");
EXPECT_EQ("http", url.schema);
EXPECT_EQ("www.myhost.de", url.hostname);
EXPECT_EQ(8080, url.port);
EXPECT_EQ("/path", url.path);
EXPECT_EQ("query=1&second=2", url.query);
}
TEST_F(UrlTest, testUserPassword) {
const Url url("http://foo:bar@www.myhost.de");
EXPECT_EQ("http", url.schema);
EXPECT_EQ("www.myhost.de", url.hostname);
EXPECT_EQ(80, url.port);
EXPECT_EQ("/", url.path);
EXPECT_EQ("foo", url.username);
EXPECT_EQ("bar", url.password);
}
TEST_F(UrlTest, testPortPathQueryUsernamePassword) {
const Url url("http://foo:bar@www.myhost.de:8080/path?query=1&second=2");
EXPECT_EQ("http", url.schema);
EXPECT_EQ("www.myhost.de", url.hostname);
EXPECT_EQ(8080, url.port);
EXPECT_EQ("/path", url.path);
EXPECT_EQ("query=1&second=2", url.query);
EXPECT_EQ("foo", url.username);
EXPECT_EQ("bar", url.password);
}
}

View File

@ -14,5 +14,6 @@ add_subdirectory(testvoxelfont)
add_subdirectory(testvoxelgpu)
add_subdirectory(testcomputetexture3d)
add_subdirectory(testtraze)
add_subdirectory(testhttpserver)
add_subdirectory(testskybox)
#add_subdirectory(testtemplate)

View File

@ -84,3 +84,7 @@ Just an empty template for new test applications.
# testtraze
* [traze client](testtraze/README.md)
# testhttpserver
A test application around the http module server for e.g. fuzzy testing purposes.

View File

@ -0,0 +1,6 @@
project(testhttpserver)
set(SRCS
TestHttpServer.h TestHttpServer.cpp
)
engine_add_executable(TARGET ${PROJECT_NAME} SRCS ${SRCS} NOINSTALL)
engine_target_link_libraries(TARGET ${PROJECT_NAME} DEPENDENCIES core http)

View File

@ -0,0 +1,100 @@
/**
* @file
*/
#include "TestHttpServer.h"
#include "core/io/Filesystem.h"
#include "core/metric/Metric.h"
#include "core/EventBus.h"
#include "core/TimeProvider.h"
#include "core/Var.h"
TestHttpServer::TestHttpServer(const metric::MetricPtr& metric, const io::FilesystemPtr& filesystem, const core::EventBusPtr& eventBus, const core::TimeProviderPtr& timeProvider) :
Super(metric, filesystem, eventBus, timeProvider) {
init(ORGANISATION, "testhttpserver");
}
core::AppState TestHttpServer::onConstruct() {
core::AppState state = Super::onConstruct();
_framesPerSecondsCap->setVal(5.0f);
_exitAfterRequest = core::Var::get("exitafterrequest", "0");
return state;
}
core::AppState TestHttpServer::onInit() {
core::AppState state = Super::onInit();
if (state != core::AppState::Running) {
return state;
}
_loop = new uv_loop_t;
if (uv_loop_init(_loop) != 0) {
Log::error("Failed to init event loop");
uv_loop_close(_loop);
delete _loop;
_loop = nullptr;
return core::AppState::InitFailure;
}
if (!_input.init(_loop)) {
Log::warn("Could not init console input");
}
const int16_t port = 8088;
if (!_server.init(port)) {
Log::error("Failed to start the http server");
return core::AppState::InitFailure;
}
_server.registerRoute(http::HttpMethod::GET, "/", [&] (const http::RequestParser& request, http::HttpResponse* response) {
Log::error("Got a request for /");
if (_exitAfterRequest->intVal() > 0) {
_remainingFrames = _exitAfterRequest->intVal();
response->setText("Request successful - shutting down the server\n");
} else {
response->setText("Request successful\n");
}
});
_server.setErrorText(http::HttpStatus::NotFound, "Not found\n");
_server.registerRoute(http::HttpMethod::GET, "/shutdown", [&] (const http::RequestParser& requesty, http::HttpResponse* response) {
Log::error("Got a shutdown request");
response->setText("Request successful - shutting down the server after 5 steps\n");
_remainingFrames = 5;
});
Log::info("Running on port %i with %.1f fps", (int)port, _framesPerSecondsCap->floatVal());
Log::info("Use cvar '%s' to shut down after a request", _exitAfterRequest->name().c_str());
return state;
}
core::AppState TestHttpServer::onRunning() {
Super::onRunning();
uv_run(_loop, UV_RUN_NOWAIT);
_server.update();
if (_remainingFrames > 0) {
if (--_remainingFrames <= 0) {
requestQuit();
} else {
Log::info("%i steps until shutdown", _remainingFrames);
}
}
return core::AppState::Running;
}
core::AppState TestHttpServer::onCleanup() {
core::AppState state = Super::onCleanup();
if (_loop != nullptr) {
uv_tty_reset_mode();
uv_loop_close(_loop);
delete _loop;
_loop = nullptr;
}
Log::info("Shuttting down http server");
_server.shutdown();
return state;
}
CONSOLE_APP(TestHttpServer)

View File

@ -0,0 +1,32 @@
/**
* @file
*/
#pragma once
#include "core/ConsoleApp.h"
#include "http/HttpServer.h"
#include "core/Input.h"
#include <uv.h>
/**
* @brief Test application to allow fuzzing the http server code
*
* See e.g. https://github.com/zardus/preeny and https://lolware.net/2015/04/28/nginx-fuzzing.html
*/
class TestHttpServer: public core::ConsoleApp {
private:
using Super = core::ConsoleApp;
http::HttpServer _server;
core::Input _input;
uv_loop_t *_loop = nullptr;
core::VarPtr _exitAfterRequest;
int _remainingFrames = 0;
public:
TestHttpServer(const metric::MetricPtr& metric, const io::FilesystemPtr& filesystem, const core::EventBusPtr& eventBus, const core::TimeProviderPtr& timeProvider);
virtual core::AppState onConstruct() override;
virtual core::AppState onInit() override;
virtual core::AppState onRunning() override;
virtual core::AppState onCleanup() override;
};