From c464265f60ddd367786b08f5d49cd7a6d650b7d6 Mon Sep 17 00:00:00 2001 From: Roland Reichwein Date: Sat, 21 Jan 2023 19:05:43 +0100 Subject: First websocket connection --- whiteboard.cpp | 286 +++++++++++++++++++++++++++++++++------------------------ 1 file changed, 167 insertions(+), 119 deletions(-) (limited to 'whiteboard.cpp') diff --git a/whiteboard.cpp b/whiteboard.cpp index 6466635..be8bcb0 100644 --- a/whiteboard.cpp +++ b/whiteboard.cpp @@ -7,8 +7,6 @@ #include #include -#include - #include #include #include @@ -22,6 +20,15 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "libreichwein/file.h" @@ -38,20 +45,20 @@ namespace { void usage() { std::cout << "Usage: \n" - " whiteboard [-c]\n" + " whiteboard [options]\n" "\n" "Options:\n" - " -c : Cleanup database according to timeout rules (config: maxage)\n" + " -c : specify configuration file including path\n" + " -C : clean up database according to timeout rules (config: maxage)\n" + " -h : this help\n" "\n" - "Without options, whiteboard will be started as FCGI application" + "Without options, whiteboard will be started as websocket application" << std::endl; } } // namespace -Whiteboard::Whiteboard(): - m_config(), - m_storage(m_config) +Whiteboard::Whiteboard() { } @@ -61,136 +68,177 @@ void Whiteboard::storage_cleanup() while(true) { { std::lock_guard lock(m_storage_mutex); - m_storage.cleanup(); + if (!m_storage) + throw std::runtime_error("Storage not initialized"); + m_storage->cleanup(); } std::this_thread::sleep_for(std::chrono::minutes(10)); } } +std::string Whiteboard::handle_request(const std::string& request) +{ + try { + std::lock_guard lock(m_storage_mutex); + if (!m_storage) + throw std::runtime_error("Storage not initialized"); + + pt::ptree xml; + std::istringstream ss{request}; + pt::xml_parser::read_xml(ss, xml); + + std::string command {xml.get("request.command")}; + + if (command == "modify") { + std::string id {xml.get("request.id")}; + std::string data {xml.get("request.data")}; + m_storage->setDocument(id, data); + return {}; + } else if (command == "getfile") { + std::string id {xml.get("request.id")}; + + std::string filedata {m_storage->getDocument(id)}; + + if (filedata.size() > 30000000) + throw std::runtime_error("File too big"); + + return filedata; + } else if (command == "checkupdate") { + std::string id {xml.get("request.id")}; + std::string checksum_s {xml.get("request.checksum")}; + uint32_t checksum{static_cast(stoul(checksum_s))}; + + std::string filedata {m_storage->getDocument(id)}; + if (checksum != m_storage->checksum32(filedata)) { + return filedata; + } else { + return {}; + } + } else if (command == "newid") { + return m_storage->generate_id(); + } else if (command == "qrcode") { + std::string url{xml.get("request.url")}; + + if (url.size() > 1000) + throw std::runtime_error("URL too big"); + + std::string pngdata {QRCode::getQRCode(url)}; + + return pngdata; + } else { + throw std::runtime_error("Bad command: "s + command); + } + + } catch (const std::exception& ex) { + return "Message handling error: "s + ex.what(); + } +} + +void Whiteboard::do_session(boost::asio::ip::tcp::socket socket) +{ + try { + // Construct the stream by moving in the socket + boost::beast::websocket::stream ws{std::move(socket)}; + + // Set a decorator to change the Server of the handshake + ws.set_option(boost::beast::websocket::stream_base::decorator( + [](boost::beast::websocket::response_type& res) + { + res.set(boost::beast::http::field::server, + std::string("Reichwein.IT Whiteboard")); + })); + + boost::beast::http::request_parser parser; + boost::beast::http::request req; + boost::beast::flat_buffer buffer; + + boost::beast::http::read(ws.next_layer(), buffer, parser); + req = parser.get(); + + ws.accept(req); + + while (true) { + boost::beast::flat_buffer buffer; + + ws.read(buffer); + + ws.text(ws.got_text()); + std::string data(boost::asio::buffers_begin(buffer.data()), boost::asio::buffers_end(buffer.data())); + data = handle_request(data); + buffer.consume(buffer.size()); + boost::beast::ostream(buffer) << data; + ws.write(buffer.data()); + } + } catch (boost::beast::system_error const& se) { + // This indicates that the session was closed + if (se.code() != boost::beast::websocket::error::closed) + std::cerr << "Boost system_error in session: " << se.code().message() << std::endl; + } catch (std::exception const& ex) { + std::cerr << "Error in session: " << ex.what() << std::endl; + } +} + // the actual main() for testability int Whiteboard::run(int argc, char* argv[]) { - if (argc == 2) { - if (argv[1] == "-h"s || argv[1] == "-?"s) { - usage(); - exit(0); - } else if (argv[1] == "-c"s) { - m_storage.cleanup(); + try { + bool flag_cleanup{}; + fs::path configFile; + + if (argc == 2) { + if (argv[1] == "-h"s || argv[1] == "-?"s) { + usage(); + exit(0); + } else if (argv[1] == "-C"s) { + flag_cleanup = true; + } + } else if (argc == 3) { + if (argv[1] == "-c"s) { + configFile = argv[2]; + } + } + + if (configFile.empty()) + m_config = std::make_unique(); + else + m_config = std::make_unique(configFile); + + m_storage = std::make_unique(*m_config); + + if (flag_cleanup) { + m_storage->cleanup(); exit(0); } - } - std::thread storage_cleanup_thread(std::bind(&Whiteboard::storage_cleanup, this)); + std::thread storage_cleanup_thread(std::bind(&Whiteboard::storage_cleanup, this)); - QRCode::init(); + QRCode::init(); - int result = FCGX_Init(); - if (result != 0) { // error on init - fprintf(stderr, "Error: FCGX_Init()\n"); - return 1; - } + auto const address = boost::asio::ip::make_address(m_config->getListenAddress()); + auto const port = static_cast(m_config->getListenPort()); - result = FCGX_IsCGI(); - if (result) { - fprintf(stderr, "Error: No FCGI environment available.\n"); - return 1; - } + // The io_context is required for all I/O + boost::asio::io_context ioc{m_config->getThreads()}; - FCGX_Request request; - result = FCGX_InitRequest(&request, 0, 0); - if (result != 0) { - fprintf(stderr, "Error: FCGX_InitRequest()\n"); - return 1; - } + // The acceptor receives incoming connections + boost::asio::ip::tcp::acceptor acceptor{ioc, {address, port}}; + while (true) { + // This will receive the new connection + boost::asio::ip::tcp::socket socket{ioc}; - while (FCGX_Accept_r(&request) >= 0) { - try { - std::lock_guard lock(m_storage_mutex); - char* method = FCGX_GetParam("REQUEST_METHOD", request.envp); - - // POST for server actions, changes - if (!strcmp(method, "POST")) { - size_t contentLength { std::stoul(FCGX_GetParam("CONTENT_LENGTH", request.envp)) }; - std::string postData(contentLength, '\0'); // contentLength number of bytes, initialize with 0 - if (FCGX_GetStr(postData.data(), contentLength, request.in) != static_cast(contentLength)) { - throw std::runtime_error("Bad data read: Content length mismatch.\r\n"); - } - // postData contains POST data - std::string contentType(FCGX_GetParam("CONTENT_TYPE", request.envp)); - - std::string xmlData = postData; // default: interpret whole POST data as xml request - - pt::ptree xml; - std::istringstream ss{xmlData}; - pt::xml_parser::read_xml(ss, xml); - - std::string command {xml.get("request.command")}; - - if (command == "modify") { - std::string id {xml.get("request.id")}; - std::string data {xml.get("request.data")}; - m_storage.setDocument(id, data); - FCGX_PutS("Content-Type: text/plain\r\n\r\n", request.out); - } else if (command == "getfile") { - std::string id {xml.get("request.id")}; - - std::string filedata {m_storage.getDocument(id)}; - - if (filedata.size() > 30000000) - throw std::runtime_error("File too big"); - - FCGX_PutS("Content-Type: application/octet-stream\r\n", request.out); - FCGX_FPrintF(request.out, "Content-Length: %d\r\n\r\n", filedata.size()); - FCGX_PutStr(filedata.c_str(), filedata.size(), request.out); - } else if (command == "checkupdate") { - std::string id {xml.get("request.id")}; - std::string checksum_s {xml.get("request.checksum")}; - uint32_t checksum{static_cast(stoul(checksum_s))}; - - std::string filedata {m_storage.getDocument(id)}; - if (checksum != m_storage.checksum32(filedata)) { - //std::cout << "Sending change..." << std::endl; - FCGX_PutS("Content-Type: application/octet-stream\r\n", request.out); - FCGX_FPrintF(request.out, "Content-Length: %d\r\n\r\n", filedata.size()); - FCGX_PutStr(filedata.c_str(), filedata.size(), request.out); - } else { - //std::cout << "No change..." << std::endl; - FCGX_PutS("Content-Type: text/plain\r\n\r\n", request.out); - FCGX_PutS("No change.\r\n", request.out); - } - } else if (command == "newid") { - FCGX_PutS("Content-Type: text/plain\r\n\r\n", request.out); - FCGX_PutS(m_storage.generate_id().c_str(), request.out); - } else if (command == "qrcode") { - std::string url{xml.get("request.url")}; - - if (url.size() > 1000) - throw std::runtime_error("URL too big"); - - std::string pngdata {QRCode::getQRCode(url)}; - - FCGX_PutS("Content-Type: image/png\r\n", request.out); - FCGX_FPrintF(request.out, "Content-Length: %d\r\n\r\n", pngdata.size()); - FCGX_PutStr(pngdata.c_str(), pngdata.size(), request.out); - } else { - throw std::runtime_error("Bad command: "s + command); - } + // Block until we get a connection + acceptor.accept(socket); - } else { - throw std::runtime_error("Unsupported method.\r\n"); - } - } catch (const std::runtime_error& ex) { - FCGX_PutS("Status: 500 Internal Server Error\r\n", request.out); - FCGX_PutS("Content-Type: text/html\r\n\r\n", request.out); - FCGX_FPrintF(request.out, "Error: %s\r\n", ex.what()); - } catch (const std::exception& ex) { - FCGX_PutS("Status: 500 Internal Server Error\r\n", request.out); - FCGX_PutS("Content-Type: text/html\r\n\r\n", request.out); - FCGX_FPrintF(request.out, "Unknown exception: %s\r\n", ex.what()); + // Launch the session, transferring ownership of the socket + std::thread( + &Whiteboard::do_session, this, + std::move(socket)).detach(); } - } - storage_cleanup_thread.join(); + storage_cleanup_thread.join(); + } catch (const std::exception& ex) { + std::cerr << "Error: " << ex.what() << std::endl; + } return 0; } -- cgit v1.2.3