From f84bc53c8df6b0be0f2ffa779b4711f288921d9c Mon Sep 17 00:00:00 2001 From: Benjamin Sergeant Date: Sun, 23 Jun 2019 14:54:21 -0700 Subject: [PATCH] Feature/httpd (#94) * Stub code for http server * can send a response, cannot process body yet * write headers to the response * remove commented code * add simple test + set default http handler * tweak CI + unittest * add missing file * rewrite http::trim in a simple way * doc --- CHANGELOG.md | 3 +- CMakeLists.txt | 4 + README.md | 42 ++++++- ixwebsocket/IXHttp.cpp | 138 +++++++++++++++++++++++ ixwebsocket/IXHttp.h | 120 ++++++++++++++++++++ ixwebsocket/IXHttpClient.cpp | 43 ++++---- ixwebsocket/IXHttpClient.h | 73 +------------ ixwebsocket/IXHttpServer.cpp | 158 +++++++++++++++++++++++++++ ixwebsocket/IXHttpServer.h | 50 +++++++++ ixwebsocket/IXSocket.cpp | 17 ++- ixwebsocket/IXSocketServer.cpp | 1 - ixwebsocket/IXWebSocketHandshake.cpp | 46 +------- ixwebsocket/IXWebSocketHandshake.h | 2 - ixwebsocket/IXWebSocketServer.h | 6 +- test/CMakeLists.txt | 7 +- test/IXGetFreePort.cpp | 93 ++++++++++++++++ test/IXGetFreePort.h | 12 ++ test/IXHttpServerTest.cpp | 70 ++++++++++++ test/IXTest.cpp | 82 -------------- test/IXTest.h | 3 +- test/IXUnityBuildsTest.cpp | 52 +++++++++ test/data/foo.txt | 1 + test/run.py | 3 +- ws/CMakeLists.txt | 1 + ws/ws.cpp | 8 ++ ws/ws.h | 2 + ws/ws_httpd.cpp | 34 ++++++ 27 files changed, 834 insertions(+), 237 deletions(-) create mode 100644 ixwebsocket/IXHttp.cpp create mode 100644 ixwebsocket/IXHttp.h create mode 100644 ixwebsocket/IXHttpServer.cpp create mode 100644 ixwebsocket/IXHttpServer.h create mode 100644 test/IXGetFreePort.cpp create mode 100644 test/IXGetFreePort.h create mode 100644 test/IXHttpServerTest.cpp create mode 100644 test/IXUnityBuildsTest.cpp create mode 100644 test/data/foo.txt create mode 100644 ws/ws_httpd.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 26fd9850..350f2971 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ # Changelog All notable changes to this project will be documented in this file. -## [unreleased] - 2019-06-09 +## [5.0.0] - 2019-06-23 ### Changed +- New HTTP server / still very early. ws gained a new command, httpd can run a simple webserver serving local files. - IXDNSLookup. Uses weak pointer + smart_ptr + shared_from_this instead of static sets + mutex to handle object going away before dns lookup has resolved - cobra_to_sentry / backtraces are reversed and line number is not extracted correctly - mbedtls and zlib are searched with find_package, and we use the vendored version if nothing is found diff --git a/CMakeLists.txt b/CMakeLists.txt index e45ce34a..33b44531 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,7 +25,9 @@ set( IXWEBSOCKET_SOURCES ixwebsocket/IXCancellationRequest.cpp ixwebsocket/IXConnectionState.cpp ixwebsocket/IXDNSLookup.cpp + ixwebsocket/IXHttp.cpp ixwebsocket/IXHttpClient.cpp + ixwebsocket/IXHttpServer.cpp ixwebsocket/IXNetSystem.cpp ixwebsocket/IXSelectInterrupt.cpp ixwebsocket/IXSelectInterruptFactory.cpp @@ -51,7 +53,9 @@ set( IXWEBSOCKET_HEADERS ixwebsocket/IXCancellationRequest.h ixwebsocket/IXConnectionState.h ixwebsocket/IXDNSLookup.h + ixwebsocket/IXHttp.h ixwebsocket/IXHttpClient.h + ixwebsocket/IXHttpServer.h ixwebsocket/IXNetSystem.h ixwebsocket/IXProgressCallback.h ixwebsocket/IXSelectInterrupt.h diff --git a/README.md b/README.md index 230628a4..09594fda 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## Introduction -[*WebSocket*](https://en.wikipedia.org/wiki/WebSocket) is a computer communications protocol, providing full-duplex and bi-directionnal communication channels over a single TCP connection. *IXWebSocket* is a C++ library for client and server Websocket communication, and for client HTTP communication. The code is derived from [easywsclient](https://github.com/dhbaird/easywsclient) and from the [Satori C SDK](https://github.com/satori-com/satori-rtm-sdk-c). It has been tested on the following platforms. +[*WebSocket*](https://en.wikipedia.org/wiki/WebSocket) is a computer communications protocol, providing full-duplex and bi-directionnal communication channels over a single TCP connection. *IXWebSocket* is a C++ library for client and server Websocket communication, and for client and server HTTP communication. The code is derived from [easywsclient](https://github.com/dhbaird/easywsclient) and from the [Satori C SDK](https://github.com/satori-com/satori-rtm-sdk-c). It has been tested on the following platforms. * macOS * iOS @@ -117,7 +117,7 @@ server.wait(); ``` -Here is what the HTTP client API looks like. Note that HTTP client support is very recent and subject to changes. +Here is what the HTTP client API looks like. ``` // @@ -196,6 +196,44 @@ bool ok = httpClient.performRequest(args, [](const HttpResponsePtr& response) // ok will be false if your httpClient is not async ``` +Here is what the HTTP server API looks like. Note that HTTP server support is very, very recent and subject to changes. + +``` +ix::HttpServer server(port, hostname); + +auto res = server.listen(); +if (!res.first) +{ + std::cerr << res.second << std::endl; + return 1; +} + +server.start(); +server.wait(); +``` + +If you want to handle how requests are processed, implement the setOnConnectionCallback callback, which takes an HttpRequestPtr as input, and returns an HttpResponsePtr. You can look at HttpServer::setDefaultConnectionCallback for a slightly more advanced callback example. + +``` +setOnConnectionCallback( + [this](HttpRequestPtr request, + std::shared_ptr /*connectionState*/) -> HttpResponsePtr + { + // Build a string for the response + std::stringstream ss; + ss << request->method + << " " + << request->uri; + + std::string content = ss.str(); + + return std::make_shared(200, "OK", + HttpErrorCode::Ok, + WebSocketHttpHeaders(), + content); +} +``` + ## Build CMakefiles for the library and the examples are available. This library has few dependencies, so it is possible to just add the source files into your project. Otherwise the usual way will suffice. diff --git a/ixwebsocket/IXHttp.cpp b/ixwebsocket/IXHttp.cpp new file mode 100644 index 00000000..d2aa5f64 --- /dev/null +++ b/ixwebsocket/IXHttp.cpp @@ -0,0 +1,138 @@ +/* + * IXHttp.cpp + * Author: Benjamin Sergeant + * Copyright (c) 2019 Machine Zone, Inc. All rights reserved. + */ + +#include "IXHttp.h" +#include "IXCancellationRequest.h" +#include "IXSocket.h" + +#include +#include + +namespace ix +{ + std::string Http::trim(const std::string& str) + { + std::string out; + for (auto c : str) + { + if (c != ' ' && c != '\n' && c != '\r') + { + out += c; + } + } + + return out; + } + + std::tuple Http::parseRequestLine(const std::string& line) + { + // Request-Line = Method SP Request-URI SP HTTP-Version CRLF + std::string token; + std::stringstream tokenStream(line); + std::vector tokens; + + // Split by ' ' + while (std::getline(tokenStream, token, ' ')) + { + tokens.push_back(token); + } + + std::string method; + if (tokens.size() >= 1) + { + method = trim(tokens[0]); + } + + std::string requestUri; + if (tokens.size() >= 2) + { + requestUri = trim(tokens[1]); + } + + std::string httpVersion; + if (tokens.size() >= 3) + { + httpVersion = trim(tokens[2]); + } + + return std::make_tuple(method, requestUri, httpVersion); + } + + std::tuple Http::parseRequest(std::shared_ptr socket) + { + HttpRequestPtr httpRequest; + + std::atomic requestInitCancellation(false); + + int timeoutSecs = 5; // FIXME + + auto isCancellationRequested = + makeCancellationRequestWithTimeout(timeoutSecs, requestInitCancellation); + + // Read first line + auto lineResult = socket->readLine(isCancellationRequested); + auto lineValid = lineResult.first; + auto line = lineResult.second; + + if (!lineValid) + { + return std::make_tuple(false, "Error reading HTTP request line", httpRequest); + } + + // Parse request line (GET /foo HTTP/1.1\r\n) + auto requestLine = Http::parseRequestLine(line); + auto method = std::get<0>(requestLine); + auto uri = std::get<1>(requestLine); + auto httpVersion = std::get<2>(requestLine); + + // Retrieve and validate HTTP headers + auto result = parseHttpHeaders(socket, isCancellationRequested); + auto headersValid = result.first; + auto headers = result.second; + + if (!headersValid) + { + return std::make_tuple(false, "Error parsing HTTP headers", httpRequest); + } + + httpRequest = std::make_shared(uri, method, httpVersion, headers); + return std::make_tuple(true, "", httpRequest); + } + + bool Http::sendResponse(HttpResponsePtr response, std::shared_ptr socket) + { + // Write the response to the socket + std::stringstream ss; + ss << "HTTP/1.1 "; + ss << response->statusCode; + ss << " "; + ss << response->description; + ss << "\r\n"; + + if (!socket->writeBytes(ss.str(), nullptr)) + { + return false; + } + + // Write headers + ss.str(""); + ss << "Content-Length: " << response->payload.size() << "\r\n"; + for (auto&& it : response->headers) + { + ss << it.first << ": " << it.second << "\r\n"; + } + ss << "\r\n"; + + if (!socket->writeBytes(ss.str(), nullptr)) + { + return false; + } + + return response->payload.empty() + ? true + : socket->writeBytes(response->payload, nullptr); + } +} diff --git a/ixwebsocket/IXHttp.h b/ixwebsocket/IXHttp.h new file mode 100644 index 00000000..ebdc50ad --- /dev/null +++ b/ixwebsocket/IXHttp.h @@ -0,0 +1,120 @@ +/* + * IXHttp.h + * Author: Benjamin Sergeant + * Copyright (c) 2019 Machine Zone, Inc. All rights reserved. + */ + +#pragma once + +#include "IXWebSocketHttpHeaders.h" +#include "IXProgressCallback.h" +#include + +namespace ix +{ + enum class HttpErrorCode : int + { + Ok = 0, + CannotConnect = 1, + Timeout = 2, + Gzip = 3, + UrlMalformed = 4, + CannotCreateSocket = 5, + SendError = 6, + ReadError = 7, + CannotReadStatusLine = 8, + MissingStatus = 9, + HeaderParsingError = 10, + MissingLocation = 11, + TooManyRedirects = 12, + ChunkReadError = 13, + CannotReadBody = 14, + Invalid = 100 + }; + + struct HttpResponse + { + int statusCode; + std::string description; + HttpErrorCode errorCode; + WebSocketHttpHeaders headers; + std::string payload; + std::string errorMsg; + uint64_t uploadSize; + uint64_t downloadSize; + + HttpResponse(int s = 0, + const std::string& des = std::string(), + const HttpErrorCode& c = HttpErrorCode::Ok, + const WebSocketHttpHeaders& h = WebSocketHttpHeaders(), + const std::string& p = std::string(), + const std::string& e = std::string(), + uint64_t u = 0, + uint64_t d = 0) + : statusCode(s) + , description(des) + , errorCode(c) + , headers(h) + , payload(p) + , errorMsg(e) + , uploadSize(u) + , downloadSize(d) + { + ; + } + }; + + using HttpResponsePtr = std::shared_ptr; + using HttpParameters = std::map; + using Logger = std::function; + using OnResponseCallback = std::function; + + struct HttpRequestArgs + { + std::string url; + std::string verb; + WebSocketHttpHeaders extraHeaders; + std::string body; + int connectTimeout; + int transferTimeout; + bool followRedirects; + int maxRedirects; + bool verbose; + bool compress; + Logger logger; + OnProgressCallback onProgressCallback; + }; + + using HttpRequestArgsPtr = std::shared_ptr; + + struct HttpRequest + { + std::string uri; + std::string method; + std::string version; + WebSocketHttpHeaders headers; + + HttpRequest(const std::string& u, + const std::string& m, + const std::string& v, + const WebSocketHttpHeaders& h = WebSocketHttpHeaders()) + : uri(u) + , method(m) + , version(v) + , headers(h) + { + } + }; + + using HttpRequestPtr = std::shared_ptr; + + class Http + { + public: + static std::tuple parseRequest(std::shared_ptr socket); + static bool sendResponse(HttpResponsePtr response, std::shared_ptr socket); + + static std::tuple parseRequestLine(const std::string& line); + static std::string trim(const std::string& str); + }; +} diff --git a/ixwebsocket/IXHttpClient.cpp b/ixwebsocket/IXHttpClient.cpp index 35211675..2d2d0408 100644 --- a/ixwebsocket/IXHttpClient.cpp +++ b/ixwebsocket/IXHttpClient.cpp @@ -118,6 +118,7 @@ namespace ix int code = 0; WebSocketHttpHeaders headers; std::string payload; + std::string description; std::string protocol, host, path, query; int port; @@ -126,9 +127,9 @@ namespace ix { std::stringstream ss; ss << "Cannot parse url: " << url; - return std::make_shared(code, HttpErrorCode::UrlMalformed, - headers, payload, ss.str(), - uploadSize, downloadSize); + return std::make_shared(code, description, HttpErrorCode::UrlMalformed, + headers, payload, ss.str(), + uploadSize, downloadSize); } bool tls = protocol == "https"; @@ -137,9 +138,9 @@ namespace ix if (!_socket) { - return std::make_shared(code, HttpErrorCode::CannotCreateSocket, - headers, payload, errorMsg, - uploadSize, downloadSize); + return std::make_shared(code, description, HttpErrorCode::CannotCreateSocket, + headers, payload, errorMsg, + uploadSize, downloadSize); } // Build request string @@ -200,7 +201,7 @@ namespace ix { std::stringstream ss; ss << "Cannot connect to url: " << url << " / error : " << errMsg; - return std::make_shared(code, HttpErrorCode::CannotConnect, + return std::make_shared(code, description, HttpErrorCode::CannotConnect, headers, payload, ss.str(), uploadSize, downloadSize); } @@ -226,7 +227,7 @@ namespace ix if (!_socket->writeBytes(req, isCancellationRequested)) { std::string errorMsg("Cannot send request"); - return std::make_shared(code, HttpErrorCode::SendError, + return std::make_shared(code, description, HttpErrorCode::SendError, headers, payload, errorMsg, uploadSize, downloadSize); } @@ -240,7 +241,7 @@ namespace ix if (!lineValid) { std::string errorMsg("Cannot retrieve status line"); - return std::make_shared(code, HttpErrorCode::CannotReadStatusLine, + return std::make_shared(code, description, HttpErrorCode::CannotReadStatusLine, headers, payload, errorMsg, uploadSize, downloadSize); } @@ -255,7 +256,7 @@ namespace ix if (sscanf(line.c_str(), "HTTP/1.1 %d", &code) != 1) { std::string errorMsg("Cannot parse response code from status line"); - return std::make_shared(code, HttpErrorCode::MissingStatus, + return std::make_shared(code, description, HttpErrorCode::MissingStatus, headers, payload, errorMsg, uploadSize, downloadSize); } @@ -267,7 +268,7 @@ namespace ix if (!headersValid) { std::string errorMsg("Cannot parse http headers"); - return std::make_shared(code, HttpErrorCode::HeaderParsingError, + return std::make_shared(code, description, HttpErrorCode::HeaderParsingError, headers, payload, errorMsg, uploadSize, downloadSize); } @@ -278,7 +279,7 @@ namespace ix if (headers.find("Location") == headers.end()) { std::string errorMsg("Missing location header for redirect"); - return std::make_shared(code, HttpErrorCode::MissingLocation, + return std::make_shared(code, description, HttpErrorCode::MissingLocation, headers, payload, errorMsg, uploadSize, downloadSize); } @@ -287,7 +288,7 @@ namespace ix { std::stringstream ss; ss << "Too many redirects: " << redirects; - return std::make_shared(code, HttpErrorCode::TooManyRedirects, + return std::make_shared(code, description, HttpErrorCode::TooManyRedirects, headers, payload, ss.str(), uploadSize, downloadSize); } @@ -299,7 +300,7 @@ namespace ix if (verb == "HEAD") { - return std::make_shared(code, HttpErrorCode::Ok, + return std::make_shared(code, description, HttpErrorCode::Ok, headers, payload, std::string(), uploadSize, downloadSize); } @@ -320,7 +321,7 @@ namespace ix if (!chunkResult.first) { errorMsg = "Cannot read chunk"; - return std::make_shared(code, HttpErrorCode::ChunkReadError, + return std::make_shared(code, description, HttpErrorCode::ChunkReadError, headers, payload, errorMsg, uploadSize, downloadSize); } @@ -338,7 +339,7 @@ namespace ix if (!lineResult.first) { - return std::make_shared(code, HttpErrorCode::ChunkReadError, + return std::make_shared(code, description, HttpErrorCode::ChunkReadError, headers, payload, errorMsg, uploadSize, downloadSize); } @@ -365,7 +366,7 @@ namespace ix if (!chunkResult.first) { errorMsg = "Cannot read chunk"; - return std::make_shared(code, HttpErrorCode::ChunkReadError, + return std::make_shared(code, description, HttpErrorCode::ChunkReadError, headers, payload, errorMsg, uploadSize, downloadSize); } @@ -376,7 +377,7 @@ namespace ix if (!lineResult.first) { - return std::make_shared(code, HttpErrorCode::ChunkReadError, + return std::make_shared(code, description, HttpErrorCode::ChunkReadError, headers, payload, errorMsg, uploadSize, downloadSize); } @@ -391,7 +392,7 @@ namespace ix else { std::string errorMsg("Cannot read http body"); - return std::make_shared(code, HttpErrorCode::CannotReadBody, + return std::make_shared(code, description, HttpErrorCode::CannotReadBody, headers, payload, errorMsg, uploadSize, downloadSize); } @@ -405,14 +406,14 @@ namespace ix if (!gzipInflate(payload, decompressedPayload)) { std::string errorMsg("Error decompressing payload"); - return std::make_shared(code, HttpErrorCode::Gzip, + return std::make_shared(code, description, HttpErrorCode::Gzip, headers, payload, errorMsg, uploadSize, downloadSize); } payload = decompressedPayload; } - return std::make_shared(code, HttpErrorCode::Ok, + return std::make_shared(code, description, HttpErrorCode::Ok, headers, payload, std::string(), uploadSize, downloadSize); } diff --git a/ixwebsocket/IXHttpClient.h b/ixwebsocket/IXHttpClient.h index d31b64d4..901ed5d4 100644 --- a/ixwebsocket/IXHttpClient.h +++ b/ixwebsocket/IXHttpClient.h @@ -8,6 +8,7 @@ #include "IXSocket.h" #include "IXWebSocketHttpHeaders.h" +#include "IXHttp.h" #include #include #include @@ -20,78 +21,6 @@ namespace ix { - enum class HttpErrorCode : int - { - Ok = 0, - CannotConnect = 1, - Timeout = 2, - Gzip = 3, - UrlMalformed = 4, - CannotCreateSocket = 5, - SendError = 6, - ReadError = 7, - CannotReadStatusLine = 8, - MissingStatus = 9, - HeaderParsingError = 10, - MissingLocation = 11, - TooManyRedirects = 12, - ChunkReadError = 13, - CannotReadBody = 14, - Invalid = 100 - }; - - struct HttpResponse - { - int statusCode; - HttpErrorCode errorCode; - WebSocketHttpHeaders headers; - std::string payload; - std::string errorMsg; - uint64_t uploadSize; - uint64_t downloadSize; - - HttpResponse(int s = 0, - const HttpErrorCode& c = HttpErrorCode::Ok, - const WebSocketHttpHeaders& h = WebSocketHttpHeaders(), - const std::string& p = std::string(), - const std::string& e = std::string(), - uint64_t u = 0, - uint64_t d = 0) - : statusCode(s) - , errorCode(c) - , headers(h) - , payload(p) - , errorMsg(e) - , uploadSize(u) - , downloadSize(d) - { - ; - } - }; - - using HttpResponsePtr = std::shared_ptr; - using HttpParameters = std::map; - using Logger = std::function; - using OnResponseCallback = std::function; - - struct HttpRequestArgs - { - std::string url; - std::string verb; - WebSocketHttpHeaders extraHeaders; - std::string body; - int connectTimeout; - int transferTimeout; - bool followRedirects; - int maxRedirects; - bool verbose; - bool compress; - Logger logger; - OnProgressCallback onProgressCallback; - }; - - using HttpRequestArgsPtr = std::shared_ptr; - class HttpClient { public: diff --git a/ixwebsocket/IXHttpServer.cpp b/ixwebsocket/IXHttpServer.cpp new file mode 100644 index 00000000..80d483da --- /dev/null +++ b/ixwebsocket/IXHttpServer.cpp @@ -0,0 +1,158 @@ +/* + * IXHttpServer.cpp + * Author: Benjamin Sergeant + * Copyright (c) 2019 Machine Zone, Inc. All rights reserved. + */ + +#include "IXHttpServer.h" +#include "IXSocketConnect.h" +#include "IXSocketFactory.h" +#include "IXNetSystem.h" + +#include +#include +#include +#include + +namespace +{ + std::pair> load(const std::string& path) + { + std::vector memblock; + + std::ifstream file(path); + if (!file.is_open()) return std::make_pair(false, memblock); + + file.seekg(0, file.end); + std::streamoff size = file.tellg(); + file.seekg(0, file.beg); + + memblock.resize((size_t) size); + file.read((char*)&memblock.front(), static_cast(size)); + + return std::make_pair(true, memblock); + } + + std::pair readAsString(const std::string& path) + { + auto res = load(path); + auto vec = res.second; + return std::make_pair(res.first, std::string(vec.begin(), vec.end())); + } +} + +namespace ix +{ + HttpServer::HttpServer(int port, + const std::string& host, + int backlog, + size_t maxConnections) : SocketServer(port, host, backlog, maxConnections), + _connectedClientsCount(0) + { + setDefaultConnectionCallback(); + } + + HttpServer::~HttpServer() + { + stop(); + } + + void HttpServer::stop() + { + stopAcceptingConnections(); + + // FIXME: cancelling / closing active clients ... + + SocketServer::stop(); + } + + void HttpServer::setOnConnectionCallback(const OnConnectionCallback& callback) + { + _onConnectionCallback = callback; + } + + void HttpServer::handleConnection( + int fd, + std::shared_ptr connectionState) + { + _connectedClientsCount++; + + std::string errorMsg; + auto socket = createSocket(fd, errorMsg); + + // Set the socket to non blocking mode + other tweaks + SocketConnect::configure(fd); + + auto ret = Http::parseRequest(socket); + // FIXME: handle errors in parseRequest + + if (std::get<0>(ret)) + { + auto response = _onConnectionCallback(std::get<2>(ret), connectionState); + if (!Http::sendResponse(response, socket)) + { + logError("Cannot send response"); + } + } + connectionState->setTerminated(); + + _connectedClientsCount--; + } + + size_t HttpServer::getConnectedClientsCount() + { + return _connectedClientsCount; + } + + void HttpServer::setDefaultConnectionCallback() + { + setOnConnectionCallback( + [this](HttpRequestPtr request, + std::shared_ptr /*connectionState*/) -> HttpResponsePtr + { + std::string uri(request->uri); + if (uri.empty() || uri == "/") + { + uri = "/index.html"; + } + + std::string path("." + uri); + auto res = readAsString(path); + bool found = res.first; + if (!found) + { + return std::make_shared(404, "Not Found", + HttpErrorCode::Ok, + WebSocketHttpHeaders(), + std::string()); + } + + std::string content = res.second; + + // Log request + std::stringstream ss; + ss << request->method + << " " + << request->uri + << " " + << content.size(); + logInfo(ss.str()); + + WebSocketHttpHeaders headers; + // FIXME: check extensions to set the content type + // headers["Content-Type"] = "application/octet-stream"; + headers["Accept-Ranges"] = "none"; + + for (auto&& it : request->headers) + { + headers[it.first] = it.second; + } + + return std::make_shared(200, "OK", + HttpErrorCode::Ok, + headers, + content); + } + ); + } +} diff --git a/ixwebsocket/IXHttpServer.h b/ixwebsocket/IXHttpServer.h new file mode 100644 index 00000000..5b843827 --- /dev/null +++ b/ixwebsocket/IXHttpServer.h @@ -0,0 +1,50 @@ +/* + * IXHttpServer.h + * Author: Benjamin Sergeant + * Copyright (c) 2018 Machine Zone, Inc. All rights reserved. + */ + +#pragma once + +#include "IXSocketServer.h" +#include "IXWebSocket.h" +#include "IXHttp.h" +#include +#include +#include +#include +#include +#include +#include // pair + +namespace ix +{ + class HttpServer final : public SocketServer + { + public: + using OnConnectionCallback = + std::function)>; + + HttpServer(int port = SocketServer::kDefaultPort, + const std::string& host = SocketServer::kDefaultHost, + int backlog = SocketServer::kDefaultTcpBacklog, + size_t maxConnections = SocketServer::kDefaultMaxConnections); + virtual ~HttpServer(); + virtual void stop() final; + + void setOnConnectionCallback(const OnConnectionCallback& callback); + + private: + // Member variables + OnConnectionCallback _onConnectionCallback; + std::atomic _connectedClientsCount; + + // Methods + virtual void handleConnection(int fd, + std::shared_ptr connectionState) final; + virtual size_t getConnectedClientsCount() final; + + void setDefaultConnectionCallback(); + }; +} // namespace ix + diff --git a/ixwebsocket/IXSocket.cpp b/ixwebsocket/IXSocket.cpp index 314265d5..0feb03e1 100644 --- a/ixwebsocket/IXSocket.cpp +++ b/ixwebsocket/IXSocket.cpp @@ -232,19 +232,28 @@ namespace ix bool Socket::writeBytes(const std::string& str, const CancellationRequest& isCancellationRequested) { + char* buffer = const_cast(str.c_str()); + int len = (int) str.size(); + while (true) { if (isCancellationRequested && isCancellationRequested()) return false; - char* buffer = const_cast(str.c_str()); - int len = (int) str.size(); - ssize_t ret = send(buffer, len); // We wrote some bytes, as needed, all good. if (ret > 0) { - return ret == len; + if (ret == len) + { + return true; + } + else + { + buffer += ret; + len -= ret; + continue; + } } // There is possibly something to be writen, try again else if (ret < 0 && Socket::isWaitNeeded()) diff --git a/ixwebsocket/IXSocketServer.cpp b/ixwebsocket/IXSocketServer.cpp index a941371c..4fb103cd 100644 --- a/ixwebsocket/IXSocketServer.cpp +++ b/ixwebsocket/IXSocketServer.cpp @@ -11,7 +11,6 @@ #include #include -#include #include #include diff --git a/ixwebsocket/IXWebSocketHandshake.cpp b/ixwebsocket/IXWebSocketHandshake.cpp index e8e9ff25..70e004dc 100644 --- a/ixwebsocket/IXWebSocketHandshake.cpp +++ b/ixwebsocket/IXWebSocketHandshake.cpp @@ -7,6 +7,7 @@ #include "IXWebSocketHandshake.h" #include "IXSocketConnect.h" #include "IXUrlParser.h" +#include "IXHttp.h" #include "libwshandshake.hpp" @@ -31,15 +32,6 @@ namespace ix } - std::string WebSocketHandshake::trim(const std::string& str) - { - std::string out(str); - out.erase(std::remove(out.begin(), out.end(), ' '), out.end()); - out.erase(std::remove(out.begin(), out.end(), '\r'), out.end()); - out.erase(std::remove(out.begin(), out.end(), '\n'), out.end()); - return out; - } - bool WebSocketHandshake::insensitiveStringCompare(const std::string& a, const std::string& b) { return std::equal(a.begin(), a.end(), @@ -50,40 +42,6 @@ namespace ix }); } - std::tuple WebSocketHandshake::parseRequestLine(const std::string& line) - { - // Request-Line = Method SP Request-URI SP HTTP-Version CRLF - std::string token; - std::stringstream tokenStream(line); - std::vector tokens; - - // Split by ' ' - while (std::getline(tokenStream, token, ' ')) - { - tokens.push_back(token); - } - - std::string method; - if (tokens.size() >= 1) - { - method = trim(tokens[0]); - } - - std::string requestUri; - if (tokens.size() >= 2) - { - requestUri = trim(tokens[1]); - } - - std::string httpVersion; - if (tokens.size() >= 3) - { - httpVersion = trim(tokens[2]); - } - - return std::make_tuple(method, requestUri, httpVersion); - } - std::string WebSocketHandshake::genRandomString(const int len) { std::string alphanum = @@ -294,7 +252,7 @@ namespace ix } // Validate request line (GET /foo HTTP/1.1\r\n) - auto requestLine = parseRequestLine(line); + auto requestLine = Http::parseRequestLine(line); auto method = std::get<0>(requestLine); auto uri = std::get<1>(requestLine); auto httpVersion = std::get<2>(requestLine); diff --git a/ixwebsocket/IXWebSocketHandshake.h b/ixwebsocket/IXWebSocketHandshake.h index 1424b7c2..3d0b33fa 100644 --- a/ixwebsocket/IXWebSocketHandshake.h +++ b/ixwebsocket/IXWebSocketHandshake.h @@ -64,8 +64,6 @@ namespace ix // Parse HTTP headers WebSocketInitResult sendErrorResponse(int code, const std::string& reason); - std::tuple parseRequestLine(const std::string& line); - std::string trim(const std::string& str); bool insensitiveStringCompare(const std::string& a, const std::string& b); std::atomic& _requestInitCancellation; diff --git a/ixwebsocket/IXWebSocketServer.h b/ixwebsocket/IXWebSocketServer.h index f14cff72..a3cb7a64 100644 --- a/ixwebsocket/IXWebSocketServer.h +++ b/ixwebsocket/IXWebSocketServer.h @@ -19,12 +19,12 @@ namespace ix { - using OnConnectionCallback = - std::function, std::shared_ptr)>; - class WebSocketServer final : public SocketServer { public: + using OnConnectionCallback = + std::function, std::shared_ptr)>; + WebSocketServer(int port = SocketServer::kDefaultPort, const std::string& host = SocketServer::kDefaultHost, int backlog = SocketServer::kDefaultTcpBacklog, diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 31a9867d..8565da9b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -7,7 +7,7 @@ project (ixwebsocket_unittest) set (CMAKE_CXX_STANDARD 14) -if (NOT WIN32) +if (MAC) set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/../third_party/sanitizers-cmake/cmake" ${CMAKE_MODULE_PATH}) find_package(Sanitizers) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=thread") @@ -28,6 +28,7 @@ include_directories( set (SOURCES test_runner.cpp IXTest.cpp + IXGetFreePort.cpp ../third_party/msgpack11/msgpack11.cpp ../ws/ixcore/utils/IXCoreLogger.cpp @@ -38,6 +39,8 @@ set (SOURCES IXUrlParserTest.cpp IXWebSocketServerTest.cpp IXHttpClientTest.cpp + IXHttpServerTest.cpp + IXUnityBuildsTest.cpp ) # Some unittest don't work on windows yet @@ -64,7 +67,7 @@ endif() add_executable(ixwebsocket_unittest ${SOURCES}) -if (NOT WIN32) +if (MAC) add_sanitizers(ixwebsocket_unittest) endif() diff --git a/test/IXGetFreePort.cpp b/test/IXGetFreePort.cpp new file mode 100644 index 00000000..134e029f --- /dev/null +++ b/test/IXGetFreePort.cpp @@ -0,0 +1,93 @@ +/* + * IXGetFreePort.cpp + * Author: Benjamin Sergeant + * Copyright (c) 2019 Machine Zone. All rights reserved. + */ + +#include "IXGetFreePort.h" +#include +#include + +#include +#include + +namespace ix +{ + int getAnyFreePortRandom() + { + std::random_device rd; + std::uniform_int_distribution dist(1024 + 1, 65535); + + return dist(rd); + } + + int getAnyFreePort() + { + int defaultPort = 8090; + int sockfd; + if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) + { + return getAnyFreePortRandom(); + } + + int enable = 1; + if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, + (char*) &enable, sizeof(enable)) < 0) + { + return getAnyFreePortRandom(); + } + + // Bind to port 0. This is the standard way to get a free port. + struct sockaddr_in server; // server address information + server.sin_family = AF_INET; + server.sin_port = htons(0); + server.sin_addr.s_addr = inet_addr("127.0.0.1"); + + if (bind(sockfd, (struct sockaddr *)&server, sizeof(server)) < 0) + { + Socket::closeSocket(sockfd); + return getAnyFreePortRandom(); + } + + struct sockaddr_in sa; // server address information + socklen_t len = sizeof(sa); + if (getsockname(sockfd, (struct sockaddr *) &sa, &len) < 0) + { + Socket::closeSocket(sockfd); + return getAnyFreePortRandom(); + } + + int port = ntohs(sa.sin_port); + Socket::closeSocket(sockfd); + + return port; + } + + int getFreePort() + { + while (true) + { +#if defined(__has_feature) +# if __has_feature(address_sanitizer) + int port = getAnyFreePortRandom(); +# else + int port = getAnyFreePort(); +# endif +#else + int port = getAnyFreePort(); +#endif + // + // Only port above 1024 can be used by non root users, but for some + // reason I got port 7 returned with macOS when binding on port 0... + // + if (port > 1024) + { + return port; + } + } + + return -1; + } +} // namespace ix + + diff --git a/test/IXGetFreePort.h b/test/IXGetFreePort.h new file mode 100644 index 00000000..868faf52 --- /dev/null +++ b/test/IXGetFreePort.h @@ -0,0 +1,12 @@ +/* + * IXGetFreePort.h + * Author: Benjamin Sergeant + * Copyright (c) 2019 Machine Zone. All rights reserved. + */ + +#pragma once + +namespace ix +{ + int getFreePort(); +} // namespace ix diff --git a/test/IXHttpServerTest.cpp b/test/IXHttpServerTest.cpp new file mode 100644 index 00000000..cb858172 --- /dev/null +++ b/test/IXHttpServerTest.cpp @@ -0,0 +1,70 @@ +/* + * IXSocketTest.cpp + * Author: Benjamin Sergeant + * Copyright (c) 2019 Machine Zone. All rights reserved. + */ + +#include +#include +#include +#include "IXGetFreePort.h" + +#include "catch.hpp" + +using namespace ix; + +TEST_CASE("http server", "[httpd]") +{ + SECTION("Connect to a local HTTP server") + { + int port = getFreePort(); + ix::HttpServer server(port, "127.0.0.1"); + + auto res = server.listen(); + REQUIRE(res.first); + server.start(); + + HttpClient httpClient; + WebSocketHttpHeaders headers; + + std::string url("http://127.0.0.1:"); + url += std::to_string(port); + url += "/data/foo.txt"; + auto args = httpClient.createRequest(url); + + args->extraHeaders = headers; + args->connectTimeout = 60; + args->transferTimeout = 60; + args->followRedirects = true; + args->maxRedirects = 10; + args->verbose = true; + args->compress = true; + args->logger = [](const std::string& msg) + { + std::cout << msg; + }; + args->onProgressCallback = [](int current, int total) -> bool + { + std::cerr << "\r" << "Downloaded " + << current << " bytes out of " << total; + return true; + }; + + auto response = httpClient.get(url, args); + + for (auto it : response->headers) + { + std::cerr << it.first << ": " << it.second << std::endl; + } + + std::cerr << "Upload size: " << response->uploadSize << std::endl; + std::cerr << "Download size: " << response->downloadSize << std::endl; + std::cerr << "Status: " << response->statusCode << std::endl; + std::cerr << "Error message: " << response->errorMsg << std::endl; + + REQUIRE(response->errorCode == HttpErrorCode::Ok); + REQUIRE(response->statusCode == 200); + + server.stop(); + } +} diff --git a/test/IXTest.cpp b/test/IXTest.cpp index e246ce86..79924b3a 100644 --- a/test/IXTest.cpp +++ b/test/IXTest.cpp @@ -72,88 +72,6 @@ namespace ix Logger() << msg; } - int getAnyFreePortRandom() - { - std::random_device rd; - std::uniform_int_distribution dist(1024 + 1, 65535); - - return dist(rd); - } - - int getAnyFreePort() - { - int defaultPort = 8090; - int sockfd; - if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) - { - log("Cannot compute a free port. socket error."); - return getAnyFreePortRandom(); - } - - int enable = 1; - if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, - (char*) &enable, sizeof(enable)) < 0) - { - log("Cannot compute a free port. setsockopt error."); - return getAnyFreePortRandom(); - } - - // Bind to port 0. This is the standard way to get a free port. - struct sockaddr_in server; // server address information - server.sin_family = AF_INET; - server.sin_port = htons(0); - server.sin_addr.s_addr = inet_addr("127.0.0.1"); - - if (bind(sockfd, (struct sockaddr *)&server, sizeof(server)) < 0) - { - log("Cannot compute a free port. bind error."); - - Socket::closeSocket(sockfd); - return getAnyFreePortRandom(); - } - - struct sockaddr_in sa; // server address information - socklen_t len = sizeof(sa); - if (getsockname(sockfd, (struct sockaddr *) &sa, &len) < 0) - { - log("Cannot compute a free port. getsockname error."); - - Socket::closeSocket(sockfd); - return getAnyFreePortRandom(); - } - - int port = ntohs(sa.sin_port); - Socket::closeSocket(sockfd); - - return port; - } - - int getFreePort() - { - while (true) - { -#if defined(__has_feature) -# if __has_feature(address_sanitizer) - int port = getAnyFreePortRandom(); -# else - int port = getAnyFreePort(); -# endif -#else - int port = getAnyFreePort(); -#endif - // - // Only port above 1024 can be used by non root users, but for some - // reason I got port 7 returned with macOS when binding on port 0... - // - if (port > 1024) - { - return port; - } - } - - return -1; - } - void hexDump(const std::string& prefix, const std::string& s) { diff --git a/test/IXTest.h b/test/IXTest.h index 5deefd08..463ad153 100644 --- a/test/IXTest.h +++ b/test/IXTest.h @@ -8,6 +8,7 @@ #include #include +#include "IXGetFreePort.h" #include #include #include @@ -46,7 +47,5 @@ namespace ix void log(const std::string& msg); - int getFreePort(); - bool startWebSocketEchoServer(ix::WebSocketServer& server); } // namespace ix diff --git a/test/IXUnityBuildsTest.cpp b/test/IXUnityBuildsTest.cpp new file mode 100644 index 00000000..b06ddf68 --- /dev/null +++ b/test/IXUnityBuildsTest.cpp @@ -0,0 +1,52 @@ +/* + * IXUnityBuildsTest.cpp + * Author: Benjamin Sergeant + * Copyright (c) 2019 Machine Zone. All rights reserved. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "catch.hpp" + +using namespace ix; + +TEST_CASE("unity build", "[unity_build]") +{ + SECTION("dummy test") + { + REQUIRE(true); + } +} diff --git a/test/data/foo.txt b/test/data/foo.txt new file mode 100644 index 00000000..802992c4 --- /dev/null +++ b/test/data/foo.txt @@ -0,0 +1 @@ +Hello world diff --git a/test/run.py b/test/run.py index 8163fbb2..fd3887a9 100755 --- a/test/run.py +++ b/test/run.py @@ -102,7 +102,7 @@ def runCMake(sanitizer, buildDir): USE_VENDORED_THIRD_PARTY = 'ON' else: generator = '"Unix Makefiles"' - USE_VENDORED_THIRD_PARTY = 'OFF' + USE_VENDORED_THIRD_PARTY = 'ON' CMAKE_BUILD_TYPE = BUILD_TYPE @@ -111,6 +111,7 @@ def runCMake(sanitizer, buildDir): -B"{buildDir}" \ -DCMAKE_BUILD_TYPE={CMAKE_BUILD_TYPE} \ -DUSE_TLS=1 \ + -DUSE_MBED_TLS=1 \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ -DUSE_VENDORED_THIRD_PARTY={USE_VENDORED_THIRD_PARTY} \ -G{generator}' diff --git a/ws/CMakeLists.txt b/ws/CMakeLists.txt index cb564c77..dc43bab6 100644 --- a/ws/CMakeLists.txt +++ b/ws/CMakeLists.txt @@ -68,6 +68,7 @@ add_executable(ws ws_cobra_to_statsd.cpp ws_cobra_to_sentry.cpp ws_snake.cpp + ws_httpd.cpp ws.cpp) target_link_libraries(ws ixwebsocket) diff --git a/ws/ws.cpp b/ws/ws.cpp index 5edf9c54..4f714537 100644 --- a/ws/ws.cpp +++ b/ws/ws.cpp @@ -216,6 +216,10 @@ int main(int argc, char** argv) ->check(CLI::ExistingPath); runApp->add_flag("-v", verbose, "Verbose"); + CLI::App* httpServerApp = app.add_subcommand("httpd", "HTTP server"); + httpServerApp->add_option("--port", port, "Port"); + httpServerApp->add_option("--host", hostname, "Hostname"); + CLI11_PARSE(app, argc, argv); // pid file handling @@ -313,6 +317,10 @@ int main(int argc, char** argv) redisPassword, verbose, appsConfigPath); } + else if (app.got_subcommand("httpd")) + { + ret = ix::ws_httpd_main(port, hostname); + } ix::uninitNetSystem(); return ret; diff --git a/ws/ws.h b/ws/ws.h index bc6996aa..6489d5c0 100644 --- a/ws/ws.h +++ b/ws/ws.h @@ -93,4 +93,6 @@ namespace ix const std::string& redisPassword, bool verbose, const std::string& appsConfigPath); + + int ws_httpd_main(int port, const std::string& hostname); } // namespace ix diff --git a/ws/ws_httpd.cpp b/ws/ws_httpd.cpp new file mode 100644 index 00000000..e6eb922b --- /dev/null +++ b/ws/ws_httpd.cpp @@ -0,0 +1,34 @@ +/* + * ws_httpd.cpp + * Author: Benjamin Sergeant + * Copyright (c) 2018 Machine Zone, Inc. All rights reserved. + */ + +#include +#include +#include +#include +#include +#include + +namespace ix +{ + int ws_httpd_main(int port, const std::string& hostname) + { + spdlog::info("Listening on {}:{}", hostname, port); + + ix::HttpServer server(port, hostname); + + auto res = server.listen(); + if (!res.first) + { + std::cerr << res.second << std::endl; + return 1; + } + + server.start(); + server.wait(); + + return 0; + } +}