From af50eea208c30938c1244bcd7d96a3c276f5c9a7 Mon Sep 17 00:00:00 2001 From: jmsgrogan Date: Mon, 9 Jan 2023 08:01:37 +0000 Subject: [PATCH] Initial win32 webserver --- src/mesh/CMakeLists.txt | 2 + src/mesh/MeshObjWriter.cpp | 32 ++ src/mesh/MeshObjWriter.h | 14 + src/network/CMakeLists.txt | 17 +- src/network/NetworkManager.cpp | 7 + src/network/NetworkManager.h | 1 - src/network/server/win32/Win32WebServer.cpp | 504 ++++++++++++++++++++ src/network/server/win32/Win32WebServer.h | 42 ++ test/CMakeLists.txt | 1 + test/mesh/CMakeLists.txt | 11 + test/mesh/TestMeshObjWriter.cpp | 15 + test/network/CMakeLists.txt | 10 + test/network/TestWin32WebServer.cpp | 10 + 13 files changed, 661 insertions(+), 5 deletions(-) create mode 100644 src/mesh/MeshObjWriter.cpp create mode 100644 src/mesh/MeshObjWriter.h create mode 100644 src/network/server/win32/Win32WebServer.cpp create mode 100644 src/network/server/win32/Win32WebServer.h create mode 100644 test/mesh/CMakeLists.txt create mode 100644 test/mesh/TestMeshObjWriter.cpp create mode 100644 test/network/TestWin32WebServer.cpp diff --git a/src/mesh/CMakeLists.txt b/src/mesh/CMakeLists.txt index 6e152db..ec50b46 100644 --- a/src/mesh/CMakeLists.txt +++ b/src/mesh/CMakeLists.txt @@ -23,6 +23,8 @@ list(APPEND mesh_LIB_INCLUDES MeshPrimitives.h MeshBuilder.cpp MeshBuilder.h + MeshObjWriter.h + MeshObjWriter.cpp ) diff --git a/src/mesh/MeshObjWriter.cpp b/src/mesh/MeshObjWriter.cpp new file mode 100644 index 0000000..571294b --- /dev/null +++ b/src/mesh/MeshObjWriter.cpp @@ -0,0 +1,32 @@ +#include "MeshObjWriter.h" + +#include "File.h" +#include "TriMesh.h" +#include "AbstractFace.h" + +#include + +std::string MeshObjWriter::serialize(TriMesh* mesh) +{ + std::stringstream output; + for (const auto& node : mesh->getNodes()) + { + const auto x = node->getPoint().getX(); + const auto y = node->getPoint().getY(); + const auto z = node->getPoint().getZ(); + output << "v "<< x << " " << y << " " << z << "\n"; + } + + for (const auto& face : mesh->getFaces()) + { + auto ids = face->getNodeIds(); + output << "f " << 1 + ids[0] << " " << 1 + ids[1] << " " << 1 + ids[2] << "\n"; + } + return output.str(); +} + +void MeshObjWriter::write(const Path& path, TriMesh* mesh) +{ + File file(path); + file.writeText(serialize(mesh)); +} \ No newline at end of file diff --git a/src/mesh/MeshObjWriter.h b/src/mesh/MeshObjWriter.h new file mode 100644 index 0000000..1ef2405 --- /dev/null +++ b/src/mesh/MeshObjWriter.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +using Path = std::filesystem::path; +class TriMesh; + +class MeshObjWriter +{ +public: + static std::string serialize(TriMesh* mesh); + static void write(const Path& path, TriMesh* mesh); +}; \ No newline at end of file diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt index dcca569..7705d3d 100644 --- a/src/network/CMakeLists.txt +++ b/src/network/CMakeLists.txt @@ -1,7 +1,14 @@ set(platform_INCLUDES) +set(platform_LIBS) + if(UNIX) list(APPEND platform_INCLUDES sockets/UnixSocketInterface.cpp) +else() +list(APPEND platform_INCLUDES + server/win32/Win32WebServer.h + server/win32/Win32WebServer.cpp) + list(APPEND platform_LIBS Httpapi.lib) endif() list(APPEND network_HEADERS @@ -9,6 +16,7 @@ list(APPEND network_HEADERS sockets/Socket.h sockets/SocketInterface.h sockets/ISocketMessageHandler.h + web/HttpMessageHandler.h ) list(APPEND network_LIB_INCLUDES @@ -20,11 +28,12 @@ list(APPEND network_LIB_INCLUDES add_library(network SHARED ${network_LIB_INCLUDES} ${platform_INCLUDES} ${network_HEADERS}) target_include_directories(network PUBLIC - "${CMAKE_CURRENT_SOURCE_DIR}" - "${CMAKE_CURRENT_SOURCE_DIR}/sockets" - "${CMAKE_CURRENT_SOURCE_DIR}/web" + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/sockets + ${CMAKE_CURRENT_SOURCE_DIR}/web + ${CMAKE_CURRENT_SOURCE_DIR}/server/win32 ) set_target_properties( network PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON ) -target_link_libraries( network PUBLIC core) +target_link_libraries( network PUBLIC core ${platform_LIBS}) set_property(TARGET network PROPERTY FOLDER src) \ No newline at end of file diff --git a/src/network/NetworkManager.cpp b/src/network/NetworkManager.cpp index 5b47f34..e2e3b96 100644 --- a/src/network/NetworkManager.cpp +++ b/src/network/NetworkManager.cpp @@ -1,6 +1,8 @@ #include "NetworkManager.h" #ifdef __linux__ #include "UnixSocketInterface.h" +#else +#include "Win32WebServer.h" #endif #include @@ -31,6 +33,10 @@ void NetworkManager::Initialize() void NetworkManager::RunHttpServer() { +#ifdef _WIN32 + Win32WebServer server; + server.initialize(); +#else if (!mSocketInterface) { Initialize(); @@ -40,6 +46,7 @@ void NetworkManager::RunHttpServer() mSocketInterface->InitializeSocket(socket); mSocketInterface->Listen(socket); mSocketInterface->Run(socket); +#endif } void NetworkManager::RunHttpClient() diff --git a/src/network/NetworkManager.h b/src/network/NetworkManager.h index 92deef5..dcd0043 100644 --- a/src/network/NetworkManager.h +++ b/src/network/NetworkManager.h @@ -27,7 +27,6 @@ public: private: std::vector mActiveSockets; ISocketInterfaceUPtr mSocketInterface; - }; using NetworkManagerUPtr = std::unique_ptr; diff --git a/src/network/server/win32/Win32WebServer.cpp b/src/network/server/win32/Win32WebServer.cpp new file mode 100644 index 0000000..da438ea --- /dev/null +++ b/src/network/server/win32/Win32WebServer.cpp @@ -0,0 +1,504 @@ +#include "Win32WebServer.h" + +#include "FileLogger.h" +#include "StringUtils.h" + +Win32WebServer::Win32WebServer() + : mListenUrl("http://localhost:80/49152") +{ + +} + +Win32WebServer::~Win32WebServer() +{ + clear(); +} + +void Win32WebServer::clear() +{ + // Remove url + ::HttpRemoveUrl(mWorkingQueue, StringUtils::convert(mListenUrl).c_str()); + + // Close the Request Queue handle. + if (mWorkingQueue) + { + ::CloseHandle(mWorkingQueue); + } + + // Call HttpTerminate. + ::HttpTerminate(HTTP_INITIALIZE_SERVER, nullptr); +} + +void Win32WebServer::initialize() +{ + HTTPAPI_VERSION HttpApiVersion = HTTPAPI_VERSION_1; + + auto retCode = ::HttpInitialize(HttpApiVersion, HTTP_INITIALIZE_SERVER, nullptr); + if (FAILED(retCode)) + { + MLOG_ERROR("Failed to initialize win32 http server."); + return; + } + + retCode = ::HttpCreateHttpHandle(&mWorkingQueue, 0); + if (FAILED(retCode)) + { + MLOG_ERROR("Failed to create request queue handle."); + return; + } + + retCode = ::HttpAddUrl(mWorkingQueue, StringUtils::convert(mListenUrl).c_str(), nullptr); + if (FAILED(retCode)) + { + MLOG_ERROR("Failed to register queue to url: " << mListenUrl); + return; + } +} + +void Win32WebServer::onRequest(PHTTP_REQUEST request) +{ + const std::wstring wide_url = request->CookedUrl.pFullUrl; + const auto url = StringUtils::convert(wide_url); + + switch (request->Verb) + { + case HttpVerbGET: + MLOG_INFO("Got a GET request for: " << url); + auto result = sendHttpResponse(request, 200, "OK", "Hey! You hit the server \r\n"); + break; + case HttpVerbPOST: + MLOG_INFO("Got a POST request for: " << url); + result = sendHttpPostResponse(request); + break; + + default: + MLOG_INFO("Got an unknown request for: " << url); + result = sendHttpResponse(request, 503, "Not Implemented"); + break; + } +} + +void Win32WebServer::initializeHttpResponse(HTTP_RESPONSE& response, USHORT StatusCode, const std::string& reason) +{ + RtlZeroMemory((&response), sizeof(*(&response))); + response.StatusCode = StatusCode; + response.pReason = reason.c_str(); + response.ReasonLength = (USHORT)strlen(reason.c_str()); +} + +#define MAX_ULONG_STR ((ULONG) sizeof("4294967295")) + +DWORD Win32WebServer::sendHttpPostResponse(PHTTP_REQUEST pRequest) +{ + DWORD result; + auto hTempFile = INVALID_HANDLE_VALUE; + + // Allocate space for an entity buffer. Buffer can be increased on demand. + ULONG EntityBufferLength = 2048; + PUCHAR pEntityBuffer = reinterpret_cast(::HeapAlloc(::GetProcessHeap(), 0, (EntityBufferLength))); + if (pEntityBuffer == nullptr) + { + result = ERROR_NOT_ENOUGH_MEMORY; + MLOG_ERROR("Insufficient resources"); + return result; + } + + HTTP_RESPONSE response; + initializeHttpResponse(response, 200, "OK"); + + if (pRequest->Flags & HTTP_REQUEST_FLAG_MORE_ENTITY_BODY_EXISTS) + { + // The entity body is sent over multiple calls. Collect these in a file and send back. Create a temporary file. + TCHAR szTempName[MAX_PATH + 1]; + if (::GetTempFileName(L".", L"New", 0, szTempName) == 0) + { + result = ::GetLastError(); + MLOG_ERROR("Failed to set up temp file for buffer with: " << result); + if (pEntityBuffer) + { + HeapFree(GetProcessHeap(), 0, (pEntityBuffer)); + } + return result; + } + + hTempFile = CreateFile(szTempName, GENERIC_READ | GENERIC_WRITE, + 0, // Do not share. + NULL, // No security descriptor. + CREATE_ALWAYS, // Overrwrite existing. + FILE_ATTRIBUTE_NORMAL, // Normal file. + NULL + ); + if (hTempFile == INVALID_HANDLE_VALUE) + { + result = GetLastError(); + MLOG_ERROR("Failed to create temp file for buffer with: " << result); + if (pEntityBuffer) + { + HeapFree(GetProcessHeap(), 0, (pEntityBuffer)); + } + return result; + } + + DWORD bytesSent; + ULONG TempFileBytesWritten; + CHAR szContentLength[MAX_ULONG_STR]; + HTTP_DATA_CHUNK dataChunk; + + ULONG TotalBytesRead = 0; + ULONG BytesRead = 0; + do + { + // Read the entity chunk from the request. + BytesRead = 0; + result = ::HttpReceiveRequestEntityBody(mWorkingQueue, pRequest->RequestId, 0, + pEntityBuffer, EntityBufferLength, &BytesRead, NULL); + + switch (result) + { + case NO_ERROR: + if (BytesRead != 0) + { + TotalBytesRead += BytesRead; + ::WriteFile(hTempFile, pEntityBuffer, BytesRead, &TempFileBytesWritten, NULL); + } + break; + case ERROR_HANDLE_EOF: + + // + // The last request entity body has been read. + // Send back a response. + // + // To illustrate entity sends via + // HttpSendResponseEntityBody, the response will + // be sent over multiple calls. To do this, + // pass the HTTP_SEND_RESPONSE_FLAG_MORE_DATA + // flag. + + if (BytesRead != 0) + { + TotalBytesRead += BytesRead; + WriteFile( + hTempFile, + pEntityBuffer, + BytesRead, + &TempFileBytesWritten, + NULL + ); + } + + // + // Because the response is sent over multiple + // API calls, add a content-length. + // + // Alternatively, the response could have been + // sent using chunked transfer encoding, by + // passimg "Transfer-Encoding: Chunked". + // + + // NOTE: Because the TotalBytesread in a ULONG + // are accumulated, this will not work + // for entity bodies larger than 4 GB. + // For support of large entity bodies, + // use a ULONGLONG. + // + + + sprintf_s(szContentLength, MAX_ULONG_STR, "%lu", TotalBytesRead); + + ADD_KNOWN_HEADER( + response, + HttpHeaderContentLength, + szContentLength + ); + + result = + HttpSendHttpResponse( + hReqQueue, // ReqQueueHandle + pRequest->RequestId, // Request ID + HTTP_SEND_RESPONSE_FLAG_MORE_DATA, + &response, // HTTP response + NULL, // pReserved1 + &bytesSent, // bytes sent-optional + NULL, // pReserved2 + 0, // Reserved3 + NULL, // LPOVERLAPPED + NULL // pReserved4 + ); + + if (result != NO_ERROR) + { + wprintf( + L"HttpSendHttpResponse failed with %lu \n", + result + ); + goto Done; + } + + // + // Send entity body from a file handle. + // + dataChunk.DataChunkType = + HttpDataChunkFromFileHandle; + + dataChunk.FromFileHandle. + ByteRange.StartingOffset.QuadPart = 0; + + dataChunk.FromFileHandle. + ByteRange.Length.QuadPart = + HTTP_BYTE_RANGE_TO_EOF; + + dataChunk.FromFileHandle.FileHandle = hTempFile; + + result = HttpSendResponseEntityBody( + hReqQueue, + pRequest->RequestId, + 0, // This is the last send. + 1, // Entity Chunk Count. + &dataChunk, + NULL, + NULL, + 0, + NULL, + NULL + ); + + if (result != NO_ERROR) + { + wprintf( + L"HttpSendResponseEntityBody failed %lu\n", + result + ); + } + + goto Done; + + break; + + + default: + wprintf( + L"HttpReceiveRequestEntityBody failed with %lu \n", + result); + goto Done; + } + + } while (TRUE); + } + else + { + // This request does not have an entity body. + // + + result = HttpSendHttpResponse( + hReqQueue, // ReqQueueHandle + pRequest->RequestId, // Request ID + 0, + &response, // HTTP response + NULL, // pReserved1 + &bytesSent, // bytes sent (optional) + NULL, // pReserved2 + 0, // Reserved3 + NULL, // LPOVERLAPPED + NULL // pReserved4 + ); + if (result != NO_ERROR) + { + wprintf(L"HttpSendHttpResponse failed with %lu \n", + result); + } + } + +Done: + + if (pEntityBuffer) + { + FREE_MEM(pEntityBuffer); + } + + if (INVALID_HANDLE_VALUE != hTempFile) + { + CloseHandle(hTempFile); + DeleteFile(szTempName); + } + + return result; +} + +DWORD Win32WebServer::sendHttpResponse(PHTTP_REQUEST pRequest, USHORT StatusCode, const std::string& reason, const std::string& message) +{ + HTTP_RESPONSE response; + RtlZeroMemory((&response), sizeof(*(&response))); + response.StatusCode = StatusCode; + response.pReason = reason.c_str(); + response.ReasonLength = (USHORT)strlen(reason.c_str()); + + const std::string content_type = "text / html"; + response.Headers.KnownHeaders[HttpHeaderContentType].pRawValue = content_type.c_str(); + response.Headers.KnownHeaders[HttpHeaderContentType].RawValueLength = (USHORT)strlen(content_type.c_str()); + + if (!message.empty()) + { + auto entity_string = message; + HTTP_DATA_CHUNK dataChunk; + dataChunk.DataChunkType = HttpDataChunkFromMemory; + dataChunk.FromMemory.pBuffer = entity_string.data(); + dataChunk.FromMemory.BufferLength = (ULONG)strlen(entity_string.data()); + + response.EntityChunkCount = 1; + response.pEntityChunks = &dataChunk; + } + + DWORD bytesSent; + DWORD result = ::HttpSendHttpResponse( + mWorkingQueue, // ReqQueueHandle + pRequest->RequestId, // Request ID + 0, // Flags + &response, // HTTP response + NULL, // pReserved1 + &bytesSent, // bytes sent (OPTIONAL) + NULL, // pReserved2 (must be NULL) + 0, // Reserved3 (must be 0) + NULL, // LPOVERLAPPED(OPTIONAL) + NULL // pReserved4 (must be NULL) + ); + + if (result != NO_ERROR) + { + MLOG_ERROR("Http response failed with error: " << result); + } + return result; +} + +void Win32WebServer::run() +{ + // Allocate a 2 KB buffer. This size should work for most + // requests. The buffer size can be increased if required. Space + // is also required for an HTTP_REQUEST structure. + ULONG RequestBufferLength = sizeof(HTTP_REQUEST) + 2048; + PCHAR pRequestBuffer = reinterpret_cast(::HeapAlloc(::GetProcessHeap(), 0, (RequestBufferLength))); + if (pRequestBuffer == nullptr) + { + MLOG_ERROR("Failed to allocate http request buffer"); + return; + } + PHTTP_REQUEST pRequest = (PHTTP_REQUEST)pRequestBuffer; + + // Wait for a new request. This is indicated by a NULL + // request ID. + HTTP_REQUEST_ID requestId; + HTTP_SET_NULL_ID(&requestId); + while(true) + { + RtlZeroMemory(pRequest, RequestBufferLength); + + DWORD bytesRead; + ULONG result = ::HttpReceiveHttpRequest( + mWorkingQueue, // Req Queue + requestId, // Req ID + 0, // Flags + pRequest, // HTTP request buffer + RequestBufferLength,// req buffer length + &bytesRead, // bytes received + nullptr // LPOVERLAPPED + ); + + if (NO_ERROR == result) + { + switch (pRequest->Verb) + { + case HttpVerbGET: + wprintf(L"Got a GET request for %ws \n", + pRequest->CookedUrl.pFullUrl); + + result = SendHttpResponse( + hReqQueue, + pRequest, + 200, + "OK", + "Hey! You hit the server \r\n" + ); + break; + + case HttpVerbPOST: + + wprintf(L"Got a POST request for %ws \n", + pRequest->CookedUrl.pFullUrl); + + result = SendHttpPostResponse(hReqQueue, pRequest); + break; + + default: + wprintf(L"Got a unknown request for %ws \n", + pRequest->CookedUrl.pFullUrl); + + result = SendHttpResponse( + hReqQueue, + pRequest, + 503, + "Not Implemented", + NULL + ); + break; + } + + if (result != NO_ERROR) + { + break; + } + + // + // Reset the Request ID to handle the next request. + // + HTTP_SET_NULL_ID(&requestId); + } + else if (result == ERROR_MORE_DATA) + { + // + // The input buffer was too small to hold the request + // headers. Increase the buffer size and call the + // API again. + // + // When calling the API again, handle the request + // that failed by passing a RequestID. + // + // This RequestID is read from the old buffer. + // + requestId = pRequest->RequestId; + + // + // Free the old buffer and allocate a new buffer. + // + RequestBufferLength = bytesRead; + FREE_MEM(pRequestBuffer); + pRequestBuffer = (PCHAR)ALLOC_MEM(RequestBufferLength); + + if (pRequestBuffer == NULL) + { + result = ERROR_NOT_ENOUGH_MEMORY; + break; + } + + pRequest = (PHTTP_REQUEST)pRequestBuffer; + + } + else if (ERROR_CONNECTION_INVALID == result && + !HTTP_IS_NULL_ID(&requestId)) + { + // The TCP connection was corrupted by the peer when + // attempting to handle a request with more buffer. + // Continue to the next request. + + HTTP_SET_NULL_ID(&requestId); + } + else + { + break; + } + + } + + if (pRequestBuffer) + { + FREE_MEM(pRequestBuffer); + } +} \ No newline at end of file diff --git a/src/network/server/win32/Win32WebServer.h b/src/network/server/win32/Win32WebServer.h new file mode 100644 index 0000000..5641ce5 --- /dev/null +++ b/src/network/server/win32/Win32WebServer.h @@ -0,0 +1,42 @@ +#pragma once + +#ifndef UNICODE +#define UNICODE +#endif + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include + +#ifndef _WIN32_WINNT +#define _WIN32_WINNT 0x0600 +#endif +#include + +#include + +class Win32WebServer +{ +public: + Win32WebServer(); + + ~Win32WebServer(); + + void initialize(); + + void clear(); + + void run(); + +private: + void onRequest(PHTTP_REQUEST request); + + DWORD sendHttpResponse(PHTTP_REQUEST pRequest, USHORT StatusCode, const std::string& reason, const std::string& message = {}); + DWORD sendHttpPostResponse(PHTTP_REQUEST pRequest); + + void initializeHttpResponse(HTTP_RESPONSE& response, USHORT StatusCode, const std::string& reason); + + HANDLE mWorkingQueue{ 0 }; + std::string mListenUrl; +}; \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 3ee2e80..7f4de71 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -13,6 +13,7 @@ set(TEST_MODULES image ipc network + mesh publishing video web diff --git a/test/mesh/CMakeLists.txt b/test/mesh/CMakeLists.txt new file mode 100644 index 0000000..f1ab8d5 --- /dev/null +++ b/test/mesh/CMakeLists.txt @@ -0,0 +1,11 @@ + +set(MESH_UNIT_TEST_FILES + mesh/TestMeshObjWriter.cpp + PARENT_SCOPE + ) + + +set(MESH_UNIT_TEST_DEPENDENCIES + mesh + PARENT_SCOPE + ) \ No newline at end of file diff --git a/test/mesh/TestMeshObjWriter.cpp b/test/mesh/TestMeshObjWriter.cpp new file mode 100644 index 0000000..93b18bc --- /dev/null +++ b/test/mesh/TestMeshObjWriter.cpp @@ -0,0 +1,15 @@ +#include "MeshObjWriter.h" + +#include "TestFramework.h" +#include "TestUtils.h" + +#include "MeshPrimitives.h" + +TEST_CASE(TestMeshObjWriter, "mesh") +{ + auto mesh = MeshPrimitives::buildRectangleAsTriMesh(); + + MeshObjWriter writer; + writer.write(TestUtils::getTestOutputDir(__FILE__) / "out.obj", mesh.get()); + +}; \ No newline at end of file diff --git a/test/network/CMakeLists.txt b/test/network/CMakeLists.txt index b8f3140..7209c0c 100644 --- a/test/network/CMakeLists.txt +++ b/test/network/CMakeLists.txt @@ -3,8 +3,18 @@ set(NETWORK_UNIT_TEST_FILES network/TestNetworkManagerServer.cpp PARENT_SCOPE ) + +set(NETWORK_INTEGRATION_TEST_FILES + network/TestWin32WebServer.cpp + PARENT_SCOPE + ) set(NETWORK_UNIT_TEST_DEPENDENCIES network PARENT_SCOPE + ) + +set(NETWORK_INTEGRATION_TEST_DEPENDENCIES + network + PARENT_SCOPE ) \ No newline at end of file diff --git a/test/network/TestWin32WebServer.cpp b/test/network/TestWin32WebServer.cpp new file mode 100644 index 0000000..c0d1da9 --- /dev/null +++ b/test/network/TestWin32WebServer.cpp @@ -0,0 +1,10 @@ +#include "NetworkManager.h" + +#include "TestFramework.h" + +TEST_CASE(TestWin32WebServer, "network") +{ + auto network_manager = NetworkManager::Create(); + + network_manager->RunHttpServer(); +}