From 59c1ffd02775ee6a05c03d5f4924b44df954c0c3 Mon Sep 17 00:00:00 2001 From: blue Date: Thu, 28 Dec 2023 17:26:08 -0300 Subject: [PATCH] Some thoughts about poll --- CMakeLists.txt | 2 + database/migrations/m0.sql | 2 +- handler/login.cpp | 4 +- handler/login.h | 1 + handler/poll.cpp | 44 ++++++++++++++++++++++ handler/poll.h | 32 ++++++++++++++++ request/CMakeLists.txt | 1 + request/accepting.h | 13 +++++++ request/redirect.cpp | 12 ++++++ request/redirect.h | 16 ++++++++ request/request.cpp | 75 ++++++++++++++++++++++++++++++-------- request/request.h | 12 ++++-- run.sh.in | 21 +++++++++++ server/router.cpp | 32 ++++++++-------- server/router.h | 8 ++-- server/server.cpp | 29 ++++++++++++--- server/server.h | 1 + server/session.cpp | 18 ++++++++- server/session.h | 6 ++- 19 files changed, 280 insertions(+), 49 deletions(-) create mode 100644 handler/poll.cpp create mode 100644 handler/poll.h create mode 100644 request/accepting.h create mode 100644 request/redirect.cpp create mode 100644 request/redirect.h create mode 100644 run.sh.in diff --git a/CMakeLists.txt b/CMakeLists.txt index 183ff6d..dba39c4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,6 +55,8 @@ add_subdirectory(database) add_subdirectory(utils) configure_file(config.h.in config.h @ONLY) +configure_file(run.sh.in run.sh @ONLY) +execute_process(COMMAND chmod +x ${CMAKE_CURRENT_BINARY_DIR}/run.sh) target_link_libraries(${PROJECT_NAME} PRIVATE FCGI::FCGI diff --git a/database/migrations/m0.sql b/database/migrations/m0.sql index b6e9316..c691187 100644 --- a/database/migrations/m0.sql +++ b/database/migrations/m0.sql @@ -37,7 +37,7 @@ CREATE TABLE IF NOT EXISTS sessions ( `owner` INTEGER UNSIGNED NOT NULL, `started` TIMESTAMP DEFAULT UTC_TIMESTAMP(), `latest` TIMESTAMP DEFAULT UTC_TIMESTAMP(), - `access` CHAR(32), + `access` CHAR(32) NOT NULL UNIQUE, `renew` CHAR(32), `persist` BOOLEAN NOT NULL, `device` TEXT, diff --git a/handler/login.cpp b/handler/login.cpp index 2937a77..c72cdd2 100644 --- a/handler/login.cpp +++ b/handler/login.cpp @@ -34,7 +34,7 @@ void Handler::Login::handle(Request& request) { success = server->validatePassword(login, password); } catch (const DBInterface::NoLogin& e) { std::cerr << "Exception on logging in:\n\t" << e.what() << std::endl; - return error(request, Result::noLogin, Response::Status::badRequest); //can send unauthed instead, to exclude login spoofing + return error(request, Result::wrongCredentials, Response::Status::badRequest); } catch (const std::exception& e) { std::cerr << "Exception on logging in:\n\t" << e.what() << std::endl; return error(request, Result::unknownError, Response::Status::internalError); @@ -43,7 +43,7 @@ void Handler::Login::handle(Request& request) { return error(request, Result::unknownError, Response::Status::internalError); } if (!success) - return error(request, Result::noLogin, Response::Status::badRequest); + return error(request, Result::wrongCredentials, Response::Status::badRequest); nlohmann::json body = nlohmann::json::object(); body["result"] = Result::success; diff --git a/handler/login.h b/handler/login.h index ef0986e..d128f40 100644 --- a/handler/login.h +++ b/handler/login.h @@ -19,6 +19,7 @@ public: emptyLogin, noPassword, emptyPassword, + wrongCredentials, unknownError }; diff --git a/handler/poll.cpp b/handler/poll.cpp new file mode 100644 index 0000000..a6a2e0c --- /dev/null +++ b/handler/poll.cpp @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2023 Yury Gubich +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "handler/poll.h" + +#include "response/response.h" +#include "server/server.h" +#include "request/redirect.h" + +Handler::Poll::Poll (Server* server): + Handler("login", Request::Method::get), + server(server) +{} + +void Handler::Poll::handle (Request& request) { + std::string access = request.getAuthorizationToken(); + if (access.empty()) + return error(request, Result::tokenProblem, Response::Status::unauthorized); + + if (access.size() != 32) + return error(request, Result::tokenProblem, Response::Status::badRequest); + + try { + Session& session = server->getSession(access); + throw Redirect(&session); + } catch (const Redirect& r) { + throw r; + } catch (const std::exception& e) { + std::cerr << "Exception on poll:\n\t" << e.what() << std::endl; + return error(request, Result::unknownError, Response::Status::internalError); + } catch (...) { + std::cerr << "Unknown exception on poll" << std::endl; + return error(request, Result::unknownError, Response::Status::internalError); + } +} + +void Handler::Poll::error(Request& request, Result result, Response::Status status) const { + Response& res = request.createResponse(status); + nlohmann::json body = nlohmann::json::object(); + body["result"] = result; + + res.setBody(body); + res.send(); +} \ No newline at end of file diff --git a/handler/poll.h b/handler/poll.h new file mode 100644 index 0000000..a7b4362 --- /dev/null +++ b/handler/poll.h @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2023 Yury Gubich +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "handler.h" +#include "request/request.h" +#include "response/response.h" + +class Server; +namespace Handler { + +class Poll : public Handler { +public: + Poll(Server* server); + void handle(Request& request) override; + + enum class Result { + success, + tokenProblem, + replace, + timeout, + unknownError + }; + + void error(Request& request, Result result, Response::Status status) const; +private: + Server* server; + +}; + +} \ No newline at end of file diff --git a/request/CMakeLists.txt b/request/CMakeLists.txt index eee9cda..bca1871 100644 --- a/request/CMakeLists.txt +++ b/request/CMakeLists.txt @@ -1,5 +1,6 @@ set(HEADERS request.h + redirectable.h ) set(SOURCES diff --git a/request/accepting.h b/request/accepting.h new file mode 100644 index 0000000..d1b28b8 --- /dev/null +++ b/request/accepting.h @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2023 Yury Gubich +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "request/request.h" + +class Accepting { +public: + virtual void accept(std::unique_ptr request) = 0; +}; \ No newline at end of file diff --git a/request/redirect.cpp b/request/redirect.cpp new file mode 100644 index 0000000..04dac58 --- /dev/null +++ b/request/redirect.cpp @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2023 Yury Gubich +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "redirect.h" + +Redirect::Redirect(Accepting* destination): + destination(destination) +{} + +const char* Redirect::what() const noexcept { + return "This is a redirect, should have beeh handled in router, but if you see it - something went terrebly wrong"; +} \ No newline at end of file diff --git a/request/redirect.h b/request/redirect.h new file mode 100644 index 0000000..ce54f79 --- /dev/null +++ b/request/redirect.h @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2023 Yury Gubich +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "accepting.h" + +class Redirect : std::exception { +public: + Redirect(Accepting* destination); + + Accepting* destination; + const char* what() const noexcept override; +}; \ No newline at end of file diff --git a/request/request.cpp b/request/request.cpp index a3e8701..4b239c9 100644 --- a/request/request.cpp +++ b/request/request.cpp @@ -10,6 +10,7 @@ constexpr static const char* SCRIPT_FILENAME("SCRIPT_FILENAME"); constexpr static const char* SERVER_NAME("SERVER_NAME"); constexpr static const char* CONTENT_TYPE("CONTENT_TYPE"); constexpr static const char* CONTENT_LENGTH("CONTENT_LENGTH"); +constexpr static const char* AUTHORIZATION("HTTP_AUTHORIZATION"); constexpr static const char* urlEncoded("application/x-www-form-urlencoded"); @@ -31,7 +32,8 @@ constexpr std::array< Request::Request (): state(State::initial), raw(), - response(nullptr) + response(nullptr), + path() {} Request::~Request() { @@ -39,25 +41,34 @@ Request::~Request() { } void Request::terminate() { - switch (state) { + switch (state) { + case State::terminated: + case State::initial: + break; case State::accepted: + std::cout << "A termination of accepted request that was not responded to, it's probably an error" << std::endl; + FCGX_Finish_r(&raw); + break; + case State::responding: + std::cout << "A termination of responding request that was not actually sent, it's probably an error" << std::endl; + FCGX_Finish_r(&raw); + break; case State::responded: FCGX_Finish_r(&raw); break; - default: - break; } -} + state = State::terminated; +} std::string_view Request::methodName() const { - if (state == State::initial) + if (state == State::initial || state == State::terminated) throw std::runtime_error("An attempt to read request method on not accepted request"); return FCGX_GetParam(REQUEST_METHOD, raw.envp); } Request::Method Request::method() const { - std::string_view method = methodName(); + std::string_view method = methodName(); for (const auto& pair : methods) { if (pair.first == method) return pair.second; @@ -89,6 +100,10 @@ OStream Request::getErrorStream() { return OStream(raw.err); } +bool Request::active() const { + return state != State::initial && state != State::terminated; +} + Response& Request::createResponse() { if (state != State::accepted) throw std::runtime_error("An attempt create response to the request in the wrong state"); @@ -125,11 +140,15 @@ void Request::responseIsComplete() { throw std::runtime_error("An attempt to mark the request as complete, but it wasn't responded"); break; case State::responding: + std::cout << responseCode() << '\t' << methodName() << '\t' << path << std::endl; state = State::responded; break; case State::responded: throw std::runtime_error("An attempt to mark the request as a complete for the second time"); break; + case State::terminated: + throw std::runtime_error("An attempt to mark the request as a complete on a terminated request"); + break; } } @@ -137,25 +156,28 @@ Request::State Request::currentState() const { return state; } -std::string Request::getPath(const std::string& serverName) const { +void Request::readPath(const std::string& serverName) { if (state != State::accepted) throw std::runtime_error("An attempt to request path on a wrong request state"); - std::string path; + if (!path.empty()) + std::cout << "Request already has path \"" + path + "\", but it's being read again, probably an error"; + std::string_view scriptFileName(FCGX_GetParam(SCRIPT_FILENAME, raw.envp)); std::string::size_type snLocation = scriptFileName.find(serverName); if (snLocation != std::string::npos) { if (snLocation + serverName.size() < scriptFileName.size()) path = scriptFileName.substr(snLocation + serverName.size() + 1); - } if (!path.empty()) { while (path.back() == '/') path.erase(path.end() - 1); } +} +std::string Request::getPath() const { return path; } @@ -167,6 +189,9 @@ std::string Request::getServerName() const { } void Request::printEnvironment(std::ostream& out) { + if (!active()) + throw std::runtime_error("An attempt to print environment of a request in a wrong state"); + char **envp = raw.envp; for (int i = 0; envp[i] != nullptr; ++i) { out << envp[i] << "\n"; @@ -187,8 +212,8 @@ void Request::printEnvironment(nlohmann::json& out) { } bool Request::isFormUrlEncoded() const { - if (state == State::initial) - throw std::runtime_error("An attempt to read request content type on not accepted request"); + if (!active()) + throw std::runtime_error("An attempt to read content type of a request in a wrong state"); std::string_view contentType(FCGX_GetParam(CONTENT_TYPE, raw.envp)); if (!contentType.empty() && contentType.find(urlEncoded) != std::string_view::npos) { @@ -199,15 +224,15 @@ bool Request::isFormUrlEncoded() const { } unsigned int Request::contentLength() const { - if (state == State::initial) - throw std::runtime_error("An attempt to read request content length on not accepted request"); + if (!active()) + throw std::runtime_error("An attempt to read content length of a request in a wrong state"); return atoi(FCGX_GetParam(CONTENT_LENGTH, raw.envp)); } std::map Request::getForm() const { - if (state == State::initial) - throw std::runtime_error("An attempt to read form on not accepted request"); + if (!active()) + throw std::runtime_error("An attempt to read form of a request in a wrong state"); std::map result; std::string_view contentType(FCGX_GetParam(CONTENT_TYPE, raw.envp)); @@ -223,3 +248,21 @@ std::map Request::getForm() const { return result; } + +std::string Request::getAuthorizationToken() const { + if (!active()) + throw std::runtime_error("An attempt to read authorization token of a request in a wrong state"); + + const char* auth = FCGX_GetParam(AUTHORIZATION, raw.envp); + if (auth == nullptr) + return std::string(); + + std::string result(auth); + if (result.find("Bearer") != 0) + return std::string(); + + result.erase(0, 6); + trim(result); + + return result; +} \ No newline at end of file diff --git a/request/request.h b/request/request.h index dc2ca3c..d4bacd2 100644 --- a/request/request.h +++ b/request/request.h @@ -16,6 +16,7 @@ #include "stream/ostream.h" #include "utils/formdecode.h" +#include "utils/helpers.h" #include "response/response.h" class Request { @@ -25,7 +26,8 @@ public: initial, accepted, responding, - responded + responded, + terminated }; enum class Method { @@ -42,7 +44,7 @@ public: Request& operator = (Request&& other) = delete; bool wait(int socketDescriptor); - void terminate(); + bool active() const; Response& createResponse(); Response& createResponse(Response::Status status); @@ -55,8 +57,10 @@ public: unsigned int contentLength() const; std::map getForm() const; - std::string getPath(const std::string& serverName) const; + void readPath(const std::string& serverName); + std::string getPath() const; std::string getServerName() const; + std::string getAuthorizationToken() const; void printEnvironment(std::ostream& out); void printEnvironment(nlohmann::json& out); @@ -64,9 +68,11 @@ private: OStream getOutputStream(); OStream getErrorStream(); void responseIsComplete(); + void terminate(); private: State state; FCGX_Request raw; std::unique_ptr response; + std::string path; }; diff --git a/run.sh.in b/run.sh.in new file mode 100644 index 0000000..ab3070f --- /dev/null +++ b/run.sh.in @@ -0,0 +1,21 @@ +#!/bin/bash + +start_service() { + if systemctl is-active --quiet $1 + then + echo "$1 is already running" + else + sudo systemctl start $1 + echo "$1 started" + fi +} + +if [ ! -d "/run/pica" ]; then + sudo mkdir /run/pica + sudo chown $USER:$USER /run/pica +fi + +start_service "mariadb" +start_service "httpd" + +./@PROJECT_NAME@ \ No newline at end of file diff --git a/server/router.cpp b/server/router.cpp index 93c459c..2a1a63c 100644 --- a/server/router.cpp +++ b/server/router.cpp @@ -3,6 +3,8 @@ #include "router.h" +#include "request/redirect.h" + Router::Router(): get(), post() @@ -25,54 +27,54 @@ void Router::addRoute(Handler handler) { throw std::runtime_error("could'not add route " + handler->path + " to the routing table"); } -void Router::route(const std::string& path, std::unique_ptr request) { +void Router::route(std::unique_ptr request) { std::map::const_iterator itr, end; switch (request->method()) { case Request::Method::get: - itr = get.find(path); + itr = get.find(request->getPath()); end = get.end(); break; case Request::Method::post: - itr = post.find(path); + itr = post.find(request->getPath()); end = post.end(); break; default: - return handleMethodNotAllowed(path, std::move(request)); + return handleMethodNotAllowed(std::move(request)); } if (itr == end) - return handleNotFound(path, std::move(request)); + return handleNotFound(std::move(request)); try { - std::cout << "Handling " << path << "..." << std::endl; itr->second->handle(*request.get()); if (request->currentState() != Request::State::responded) - handleInternalError(path, std::runtime_error("handler failed to handle the request"), std::move(request)); - else - std::cout << request->responseCode() << '\t' << request->methodName() << '\t' << path << std::endl; + handleInternalError(std::runtime_error("handler failed to handle the request"), std::move(request)); + } catch (const Redirect& redirect) { + redirect.destination->accept(std::move(request)); } catch (const std::exception& e) { - handleInternalError(path, e, std::move(request)); + handleInternalError(e, std::move(request)); } } -void Router::handleNotFound(const std::string& path, std::unique_ptr request) { +void Router::handleNotFound(std::unique_ptr request) { Response& notFound = request->createResponse(Response::Status::notFound); + std::string path = request->getPath(); notFound.setBody(std::string("Path \"") + path + "\" was not found"); notFound.send(); std::cerr << notFound.statusCode() << '\t' << request->methodName() << '\t' << path << std::endl; } -void Router::handleInternalError(const std::string& path, const std::exception& exception, std::unique_ptr request) { +void Router::handleInternalError(const std::exception& exception, std::unique_ptr request) { Response& error = request->createResponse(Response::Status::internalError); error.setBody(std::string(exception.what())); error.send(); - std::cerr << error.statusCode() << '\t' << request->methodName() << '\t' << path << std::endl; + std::cerr << error.statusCode() << '\t' << request->methodName() << '\t' << request->getPath() << std::endl; } -void Router::handleMethodNotAllowed(const std::string& path, std::unique_ptr request) { +void Router::handleMethodNotAllowed(std::unique_ptr request) { Response& error = request->createResponse(Response::Status::methodNotAllowed); error.setBody(std::string("Method not allowed")); error.send(); - std::cerr << error.statusCode() << '\t' << request->methodName() << '\t' << path << std::endl; + std::cerr << error.statusCode() << '\t' << request->methodName() << '\t' << request->getPath() << std::endl; } diff --git a/server/router.h b/server/router.h index 95fa8db..83c2fea 100644 --- a/server/router.h +++ b/server/router.h @@ -20,12 +20,12 @@ public: Router(); void addRoute(Handler handler); - void route(const std::string& path, std::unique_ptr request); + void route(std::unique_ptr request); private: - void handleNotFound(const std::string& path, std::unique_ptr request); - void handleInternalError(const std::string& path, const std::exception& exception, std::unique_ptr request); - void handleMethodNotAllowed(const std::string& path, std::unique_ptr request); + void handleNotFound(std::unique_ptr request); + void handleInternalError(const std::exception& exception, std::unique_ptr request); + void handleMethodNotAllowed(std::unique_ptr request); private: std::map get; diff --git a/server/server.cpp b/server/server.cpp index cfb65ea..4bc8ad6 100644 --- a/server/server.cpp +++ b/server/server.cpp @@ -5,13 +5,15 @@ #include +#include "database/exceptions.h" + #include "handler/info.h" #include "handler/env.h" #include "handler/register.h" #include "handler/login.h" constexpr const char* pepper = "well, not much of a secret, huh?"; -constexpr uint8_t currentDbVesion = 1; + constexpr const char* randomChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; constexpr uint8_t saltSize = 16; constexpr uint8_t hashSize = 32; @@ -19,6 +21,8 @@ constexpr uint8_t hashParallel = 1; constexpr uint8_t hashIterations = 2; constexpr uint32_t hashMemoryCost = 65536; +constexpr uint8_t currentDbVesion = 1; + Server::Server(): terminating(false), requestCount(0), @@ -72,8 +76,8 @@ void Server::handleRequest(std::unique_ptr request) { } } - std::string path = request->getPath(serverName.value()); - router.route(path.data(), std::move(request)); + request->readPath(serverName.value()); + router.route(std::move(request)); } std::string Server::generateRandomString(std::size_t length) { @@ -128,9 +132,22 @@ bool Server::validatePassword(const std::string& login, const std::string& passw } Session& Server::openSession(const std::string& login) { - std::string accessToken = generateRandomString(32); - std::string renewToken = generateRandomString(32); - unsigned int sessionId = db->createSession(login, accessToken, renewToken); + std::string accessToken, renewToken; + unsigned int sessionId = 0; + int counter = 10; + do { + try { + accessToken = generateRandomString(32); + renewToken = generateRandomString(32); + sessionId = db->createSession(login, accessToken, renewToken); + break; + } catch (const DBInterface::Duplicate& e) { + std::cout << "Duplicate on creating session, trying again with different tokens"; + } + } while (--counter != 0); + + if (sessionId == 0) + throw std::runtime_error("Couldn't create session, ran out of attempts"); std::unique_ptr& session = sessions[accessToken] = std::make_unique(sessionId, accessToken, renewToken); return *session.get(); diff --git a/server/server.h b/server/server.h index 5e5b321..9c3e187 100644 --- a/server/server.h +++ b/server/server.h @@ -36,6 +36,7 @@ public: unsigned int registerAccount(const std::string& login, const std::string& password); bool validatePassword(const std::string& login, const std::string& password); Session& openSession(const std::string& login); + Session& getSession(const std::string& accessToken); private: void handleRequest(std::unique_ptr request); diff --git a/server/session.cpp b/server/session.cpp index 2e203be..43de593 100644 --- a/server/session.cpp +++ b/server/session.cpp @@ -3,10 +3,13 @@ #include "session.h" +#include "handler/poll.h" + Session::Session(unsigned int id, const std::string& access, const std::string& renew): id(id), access(access), - renew(renew) + renew(renew), + polling(nullptr) {} std::string Session::getAccessToken() const { @@ -16,3 +19,16 @@ std::string Session::getAccessToken() const { std::string Session::getRenewToken() const { return renew; } + +void Session::accept(std::unique_ptr request) { + if (polling) { + Response& res = request->createResponse(Response::Status::ok); + nlohmann::json body = nlohmann::json::object(); + body["result"] = Handler::Poll::Result::replace; + + res.setBody(body); + res.send(); + } + + polling = std::move(request); +} \ No newline at end of file diff --git a/server/session.h b/server/session.h index 6d23636..b5b459e 100644 --- a/server/session.h +++ b/server/session.h @@ -5,15 +5,19 @@ #include -class Session { +#include "request/accepting.h" + +class Session : public Accepting { public: Session(unsigned int id, const std::string& access, const std::string& renew); std::string getAccessToken() const; std::string getRenewToken() const; + void accept(std::unique_ptr request) override; private: unsigned int id; std::string access; std::string renew; + std::unique_ptr polling; };