HTTP: started a new http client and server module
parent
b72ff1f24b
commit
9d88352a34
|
@ -3,6 +3,7 @@
|
|||
# files will not get copied
|
||||
|
||||
add_subdirectory(core)
|
||||
add_subdirectory(http)
|
||||
add_subdirectory(commonlua)
|
||||
add_subdirectory(mail)
|
||||
add_subdirectory(math)
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#include "core/Log.h"
|
||||
#include "core/Assert.h"
|
||||
#include "backend/entity/ai/AILoader.h"
|
||||
#include "http/HttpServer.h"
|
||||
|
||||
namespace backend {
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
#include "Assert.h"
|
||||
#include <stdint.h>
|
||||
#include <type_traits>
|
||||
#include <new>
|
||||
#include <SDL_stdinc.h>
|
||||
#ifdef _WIN32
|
||||
#undef max
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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})
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* @file
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace http {
|
||||
|
||||
enum class HttpMethod {
|
||||
GET, POST, NOT_SUPPORTED
|
||||
};
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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);
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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(¶mValue, "=", 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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¶m2=value¶m3¶m4=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");
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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]);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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;
|
||||
};
|
Loading…
Reference in New Issue