From cfc1b8f1c29a1f5fcc70ea5f034e665511ae1919 Mon Sep 17 00:00:00 2001 From: Benjamin Sergeant Date: Sun, 23 Jun 2019 13:21:27 -0700 Subject: [PATCH] add simple test + set default http handler --- ixwebsocket/IXHttp.cpp | 5 +- ixwebsocket/IXHttpServer.cpp | 97 +++++++++++++++++++++++++++++---- ixwebsocket/IXHttpServer.h | 13 ++--- ixwebsocket/IXSocket.cpp | 17 ++++-- ixwebsocket/IXSocketServer.cpp | 1 - ixwebsocket/IXWebSocketServer.h | 6 +- test/CMakeLists.txt | 3 + 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 + 14 files changed, 342 insertions(+), 113 deletions(-) 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 diff --git a/ixwebsocket/IXHttp.cpp b/ixwebsocket/IXHttp.cpp index 72fbb1d8..c94049b3 100644 --- a/ixwebsocket/IXHttp.cpp +++ b/ixwebsocket/IXHttp.cpp @@ -97,7 +97,6 @@ namespace ix return std::make_tuple(true, "", httpRequest); } - // FIXME: Write a mime type bool Http::sendResponse(HttpResponsePtr response, std::shared_ptr socket) { // Write the response to the socket @@ -127,6 +126,8 @@ namespace ix return false; } - return socket->writeBytes(response->payload, nullptr); + return response->payload.empty() + ? true + : socket->writeBytes(response->payload, nullptr); } } diff --git a/ixwebsocket/IXHttpServer.cpp b/ixwebsocket/IXHttpServer.cpp index 4fe2d5bc..add43324 100644 --- a/ixwebsocket/IXHttpServer.cpp +++ b/ixwebsocket/IXHttpServer.cpp @@ -11,22 +11,45 @@ #include #include -#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 { - const int HttpServer::kDefaultHandShakeTimeoutSecs(3); // 3 seconds - HttpServer::HttpServer(int port, const std::string& host, int backlog, - size_t maxConnections, - int handshakeTimeoutSecs) : SocketServer(port, host, backlog, maxConnections), - _handshakeTimeoutSecs(handshakeTimeoutSecs), + size_t maxConnections) : SocketServer(port, host, backlog, maxConnections), _connectedClientsCount(0) { - + setDefaultConnectionCallback(); } HttpServer::~HttpServer() @@ -56,16 +79,21 @@ namespace ix 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 - auto response = _onConnectionCallback(std::get<2>(ret), connectionState); - Http::sendResponse(response, socket); - - logInfo("HttpServer::handleConnection() done"); + 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--; @@ -75,4 +103,49 @@ namespace ix { return _connectedClientsCount; } + + void HttpServer::setDefaultConnectionCallback() + { + setOnConnectionCallback( + [this](HttpRequestPtr request, + std::shared_ptr /*connectionState*/) -> HttpResponsePtr + { + std::string path("." + request->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; + 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, + WebSocketHttpHeaders(), + content); + } + ); + } } diff --git a/ixwebsocket/IXHttpServer.h b/ixwebsocket/IXHttpServer.h index 00594b36..5b843827 100644 --- a/ixwebsocket/IXHttpServer.h +++ b/ixwebsocket/IXHttpServer.h @@ -19,17 +19,16 @@ namespace ix { - using OnConnectionCallback = - std::function)>; - 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, - int handshakeTimeoutSecs = HttpServer::kDefaultHandShakeTimeoutSecs); + size_t maxConnections = SocketServer::kDefaultMaxConnections); virtual ~HttpServer(); virtual void stop() final; @@ -37,15 +36,15 @@ namespace ix private: // Member variables - int _handshakeTimeoutSecs; OnConnectionCallback _onConnectionCallback; - const static int kDefaultHandShakeTimeoutSecs; 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/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..a4340a64 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -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 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