diff --git a/CMakeLists.txt b/CMakeLists.txt index 046ecb0..ae037d3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,6 +40,8 @@ add_executable(terminal-web-server src/auth/resources.h src/auth/jwt.cpp src/auth/jwt.h + src/auth/utils.cpp + src/auth/utils.h ) find_package(Boost 1.53.0 COMPONENTS system thread filesystem log log_setup REQUIRED) diff --git a/src/auth/jwt.cpp b/src/auth/jwt.cpp index 31910a0..60f20c6 100644 --- a/src/auth/jwt.cpp +++ b/src/auth/jwt.cpp @@ -1,5 +1,80 @@ -// -// Created by vlad on 04.11.2024. -// - #include "jwt.h" +#include + +#include "utils.h" + +std::string http::auth::jwt::secretKey; + +void http::auth::jwt::generateSecretKey() { + secretKey.clear(); + + std::random_device rd; + std::mt19937 generator(rd()); + std::uniform_int_distribution<> distribution('!', '~'); + for (size_t i = 0; i < 32; ++i) { + secretKey += static_cast(distribution(generator)); + } +} + +// payload, signature +static std::pair parseJwtFromCookie(const std::string& input) { + std::string val1, val2; + size_t dotPos = input.find('.'); + + // Если точка найдена + if (dotPos != std::string::npos) { + val1 = input.substr(0, dotPos); + + // Если в val1 есть еще точки, нужно найти последнюю точку + size_t lastDotPos = val1.find_last_of('.'); + if (lastDotPos != std::string::npos) { + val1 = val1.substr(0, lastDotPos + 1); + } + + val2 = input.substr(dotPos + 1); + } else { + // Точка не найдена, val1 - вся строка + val1 = input; + } + + return std::make_pair(http::utils::b64Decode(val1), val2); +} + +http::auth::jwt::Jwt http::auth::jwt::Jwt::fromCookies(const std::string &cookie) { + auto pc = utils::parseCookies(cookie); + Jwt t; + if (pc.find("auth") != pc.end()) { + auto tmp = parseJwtFromCookie(pc.at("auth")); + t.payload = tmp.first; + t.signature = tmp.second; + } + + return t; +} + +http::auth::jwt::Jwt http::auth::jwt::Jwt::fromUser(const std::string &user) { + Jwt t; + t.payload = user; + return t; +} + +bool http::auth::jwt::Jwt::isValid() { + if (payload.empty() || signature.empty()) { + return false; + } + + auto realSignature = utils::sha256(this->payload + secretKey); + return signature == realSignature; +} + +std::string http::auth::jwt::Jwt::getUsername() { + return payload; +} + +std::string http::auth::jwt::Jwt::asCookie() { + signature = utils::sha256(this->payload + secretKey); + auto val = utils::b64Encode(payload) + "." + signature; + return val + ";Path=/; Max-Age=86400; HttpOnly; SameSite=Lax"; +} + +http::auth::jwt::Jwt::~Jwt() = default; diff --git a/src/auth/jwt.h b/src/auth/jwt.h index d0c6cf6..0456ab4 100644 --- a/src/auth/jwt.h +++ b/src/auth/jwt.h @@ -4,10 +4,20 @@ #include "resources.h" namespace http::auth::jwt { + extern std::string secretKey; + + void generateSecretKey(); + + /** + * Упрощенная реализация JWT (Json Web Token). Токен имеет вид: `{ username | base64 }.{ signature | base64 }`. + * Сигнатура вычисляется следующим образом: `SHA256(username + secret)`. + * Имя cookie: `auth`. + */ class Jwt { + std::string payload; + std::string signature; public: static Jwt fromCookies(const std::string& cookie); - static Jwt fromString(const std::string& cookie); static Jwt fromUser(const std::string& User); bool isValid(); diff --git a/src/auth/resources.cpp b/src/auth/resources.cpp index 7b2a57e..58d0ebb 100644 --- a/src/auth/resources.cpp +++ b/src/auth/resources.cpp @@ -1,15 +1,79 @@ -// -// Created by vlad on 31.10.2024. -// - #include "resources.h" +#include +#include +#include "jwt.h" +#include "utils.h" -#include -// http::auth::AuentificationRequiredResource::AuentificationRequiredResource(const std::string &path, AuthProvider& provider, resource::respGenerator generator): BasicResource(path), generator_(std::move(generator)) { -// } -// -// void http::auth::AuentificationRequiredResource::handle(const server::Request &req, server::Reply &rep) { -// } -// -// http::auth::AuentificationRequiredResource::~AuentificationRequiredResource() = default; +http::auth::User::User(const std::string &username, const std::string &passwordHash): username(username), passwordHash(passwordHash) {} + +bool http::auth::User::checkPassword(const std::string &pass) const { + return utils::sha256(pass) == passwordHash; +} + +void http::auth::User::setPassword(const std::string &pass) { + this->passwordHash = utils::sha256(pass); +} + +bool http::auth::User::checkPremisions(uint32_t p) const { + if (this->perms & SUPERUSER) { + return true; + } else { + return (this->perms & p) == p; + } +} + +void http::auth::User::setPremisions(uint32_t p) { + if (p & SUPERUSER) { + this->perms = SUPERUSER; + } else { + this->perms |= p; + } +} + +void http::auth::User::resetPremisions(uint32_t p) { + this->perms &= p; +} + +http::auth::User::~User() = default; + + +http::auth::AuthProvider::AuthProvider() = default; + +std::shared_ptr http::auth::AuthProvider::doAuth(const std::string &username, const std::string &password, server::Reply &rep) { + for (const auto& u: users) { + if (u->username == username) { + if (u->checkPassword(password)) { + auto t = jwt::Jwt::fromUser(u->username); + rep.headers.push_back({.name = "Set-Cookie", .value = t.asCookie()}); + return u; + } + BOOST_LOG_TRIVIAL(warning) << "http::auth::AuthProvider::doAuth(): Failed to login " << username << ", password: " << password << " (incorrect password)"; + return nullptr; + } + } + BOOST_LOG_TRIVIAL(warning) << "http::auth::AuthProvider::doAuth(): Failed to login " << username << ", password: " << password << " (user not found)"; + return nullptr; +} + +std::shared_ptr http::auth::AuthProvider::getSession(const server::Request &req) { + for (const auto& header: req.headers) { + if (boost::iequals(header.name, "cookie")) { + auto t = jwt::Jwt::fromCookies(header.value); + if (t.isValid()) { + const auto name = t.getUsername(); + // токен валидный, ищем юзера + for (auto& u: users) { + if (u->username == name) { + return u; + } + } + + BOOST_LOG_TRIVIAL(warning) << "http::auth::AuthProvider::getSession(): Found valid session for a non-existent user " << name; + } + } + } + return nullptr; +} + +http::auth::AuthProvider::~AuthProvider() = default; diff --git a/src/auth/resources.h b/src/auth/resources.h index f8fd052..4d623ec 100644 --- a/src/auth/resources.h +++ b/src/auth/resources.h @@ -9,20 +9,25 @@ namespace http::auth { */ class User { private: - uint32_t perms; + uint32_t perms{}; public: const std::string username; std::string passwordHash; - User(const std::string& username, const std::string& passwordHash); + /** + * Конструктор пользователя. + * @param username Имя пользователя, он же - логин. + * @param passwordHash Хеш sha256 пароля пользователя. Если передать пустой, то пароль будет сгенерирован такой же, как и имя пользователя. + */ + explicit User(const std::string& username, const std::string& passwordHash = ""); /** * Проверить пароль на соответствие хешу * @param pass * @return */ - bool checkPassword(const std::string& pass); + bool checkPassword(const std::string& pass) const; /** * Установка пароля @@ -43,7 +48,7 @@ namespace http::auth { * @param p набор прав, из констант данного класса. * @return */ - bool checkPremisions(uint32_t p); + bool checkPremisions(uint32_t p) const; void setPremisions(uint32_t p); void resetPremisions(uint32_t p); @@ -65,9 +70,11 @@ namespace http::auth { * @param rep * @return true, в случае успешной авторизации */ - bool doAuth(const std::string& username, const std::string& password, server::Reply& rep); + std::shared_ptr doAuth(const std::string &username, const std::string &password, server::Reply &rep); - std::shared_ptr getSession(server::Request& req); + std::vector> users; + + std::shared_ptr getSession(const server::Request &req); ~AuthProvider(); }; diff --git a/src/auth/utils.cpp b/src/auth/utils.cpp new file mode 100644 index 0000000..e484c07 --- /dev/null +++ b/src/auth/utils.cpp @@ -0,0 +1,78 @@ +#include "utils.h" +#include +#include +#include +#include +#include +#include + +std::string http::utils::sha256(const std::string &payload) { + // Вычисляем SHA256 хеш + unsigned char hash[SHA256_DIGEST_LENGTH]; + SHA256(reinterpret_cast(payload.c_str()), payload.length(), hash); + + // Преобразуем хеш в шестнадцатеричную строку + std::stringstream ss; + for (unsigned char i : hash) { + ss << std::hex << std::setw(2) << std::setfill('0') << static_cast(i); + } + return ss.str(); +} + +std::string http::utils::b64Encode(const std::string &payload) { + BIO *bio_mem = BIO_new(BIO_s_mem()); + BIO *bio_b64 = BIO_new(BIO_f_base64()); + bio_b64 = BIO_push(bio_b64, bio_mem); + + BIO_write(bio_b64, payload.c_str(), payload.length()); + BIO_flush(bio_b64); + + BUF_MEM *bptr = nullptr; + BIO_get_mem_data(bio_mem, &bptr); + + std::string result(bptr->data, bptr->length); + BIO_free_all(bio_b64); + return result; +} + +std::string http::utils::b64Decode(const std::string &payload) { + BIO *bio_mem = BIO_new(BIO_s_mem()); + BIO *bio_b64 = BIO_new(BIO_f_base64()); + bio_b64 = BIO_push(bio_b64, bio_mem); + + BIO_write(bio_b64, payload.c_str(), static_cast(payload.size())); + BIO_flush(bio_b64); + + const int len = BIO_get_mem_data(bio_mem, NULL); + char buffer[static_cast(len)]; + BIO_read(bio_mem, buffer, len); + + std::string result(buffer, static_cast(len)); + BIO_free_all(bio_b64); + return result; +} + +std::map http::utils::parseCookies(const std::string& cookieString) { + std::map cookies; + std::istringstream cookieStream(cookieString); + + std::string cookie; + while (std::getline(cookieStream, cookie, ';')) { + // Разделяем имя и значение Cookie + size_t equalPos = cookie.find('='); + if (equalPos == std::string::npos) { + continue; // Неверный формат Cookie + } + std::string name = cookie.substr(0, equalPos); + std::string value = cookie.substr(equalPos + 1); + + // Удаляем пробелы с начала и конца значения Cookie + value.erase(0, value.find_first_not_of(' ')); + value.erase(value.find_last_not_of(' ') + 1); + + // Добавляем Cookie в map + cookies[name] = value; + } + + return cookies; +} \ No newline at end of file diff --git a/src/auth/utils.h b/src/auth/utils.h new file mode 100644 index 0000000..4c64a2c --- /dev/null +++ b/src/auth/utils.h @@ -0,0 +1,17 @@ +#ifndef UTILS_H +#define UTILS_H + +#include +#include + +namespace http::utils { + std::string sha256(const std::string& payload); + std::string sha256AsB64(const std::string& payload); + + std::string b64Encode(const std::string& payload); + std::string b64Decode(const std::string& payload); + + std::map parseCookies(const std::string& cookieStrin); +} + +#endif //UTILS_H diff --git a/src/main.cpp b/src/main.cpp index 89346e9..a3eae26 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -11,12 +11,15 @@ #include #include #include +#include +#include #include #include #include #include "terminal_api_driver.h" #include "auth/resources.h" +#include "auth/jwt.h" namespace ssl = boost::asio::ssl; // from @@ -76,11 +79,11 @@ void init_logging() { class ServerResources { std::unique_ptr sf; std::unique_ptr api; + http::auth::AuthProvider auth{}; public: static constexpr const char* INDEX_HTML = "static/main.html"; static constexpr const char* LOGIN_HTML = "static/login.html"; - static constexpr const char* LOGIN_FAILED_HTML = "static/login-failed.html"; // картинки, их даже можно кешировать static constexpr const char* FAVICON_ICO = "static/favicon.png"; @@ -94,9 +97,10 @@ public: ServerResources(const ServerResources&) = delete; ServerResources(): sf(std::make_unique()), api(std::make_unique()) { + auth.users.emplace_back(std::make_shared("admin")); + sf->registerFile(INDEX_HTML, mime_types::text_html, false); sf->registerFile(LOGIN_HTML, mime_types::text_html, false); - sf->registerFile(LOGIN_FAILED_HTML, mime_types::text_html, false); sf->registerFile(FAVICON_ICO, mime_types::image_png, true); sf->registerFile(KROKODIL_GIF, mime_types::image_gif, true); @@ -114,15 +118,36 @@ public: void registerResources(http::server::Server& s) { s.resources.emplace_back(std::make_unique("/", [this](const auto& req, auto& rep) { - boost::ignore_unused(req); - sf->serve(INDEX_HTML, rep); + auto user = auth.getSession(req); + if (user == nullptr) { + http::server::httpRedirect(rep, "/login"); + } else { + sf->serve(INDEX_HTML, rep); + } })); s.resources.emplace_back(std::make_unique("/login", [this](const auto& req, auto& rep) { if (req.method == "GET") { sf->serve(LOGIN_HTML, rep); } else if (req.method == "POST") { - sf->serve(LOGIN_FAILED_HTML, rep); + rep.status = http::server::ok; + rep.headers.clear(); + rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)}); + try { + std::istringstream is(req.body); + boost::property_tree::ptree pt; + boost::property_tree::read_json(is, pt); + + auto u = auth.doAuth(req); + if (u == nullptr) { + throw std::runtime_error("invalid session"); + } + std::string result = R"({"redirect":"/"})"; + rep.content.insert(rep.content.end(), result.c_str(), result.c_str() + result.size()); + } catch (std::exception &e) { + std::string result = R"({"error":"Неверный логин или пароль"})"; + rep.content.insert(rep.content.end(), result.c_str(), result.c_str() + result.size()); + } } else { http::server::stockReply(http::server::bad_request, rep); } @@ -214,6 +239,9 @@ int main(int argc, char *argv[]) { BOOST_LOG_TRIVIAL(info) << "Starting RELEASE build" << argv[0]; #endif + http::auth::jwt::generateSecretKey(); + BOOST_LOG_TRIVIAL(info) << "Generated new secret key " << http::auth::jwt::secretKey; + ServerResources resources; // Initialise the server. diff --git a/src/server/connection.cpp b/src/server/connection.cpp index 4178014..aeb6eb9 100644 --- a/src/server/connection.cpp +++ b/src/server/connection.cpp @@ -126,9 +126,7 @@ namespace http::server { void SslConnection::doWrite() { reply_.headers.push_back({.name = "Server", .value = SERVER_HEADER_VALUE}); - if (!reply_.content.empty()) { - reply_.headers.push_back({.name = "Content-Length", .value = std::to_string(reply_.content.size())}); - } + reply_.headers.push_back({.name = "Content-Length", .value = std::to_string(reply_.content.size())}); if (request_.http_version_major == 1) { reply_.headers.push_back({.name = "Connection", .value = "keep-alive"}); } diff --git a/src/server/reply.cpp b/src/server/reply.cpp index 66e2cff..e07d477 100644 --- a/src/server/reply.cpp +++ b/src/server/reply.cpp @@ -13,6 +13,7 @@ namespace http::server { const std::string multiple_choices = "HTTP/1.1 300 Multiple Choices\r\n"; const std::string moved_permanently = "HTTP/1.1 301 Moved Permanently\r\n"; const std::string moved_temporarily = "HTTP/1.1 302 Moved Temporarily\r\n"; + const std::string see_other_redirect = "HTTP/1.1 303 See Other\r\n"; const std::string not_modified = "HTTP/1.1 304 Not Modified\r\n"; const std::string bad_request = "HTTP/1.1 400 Bad Request\r\n"; const std::string unauthorized = "HTTP/1.1 401 Unauthorized\r\n"; @@ -39,6 +40,8 @@ namespace http::server { return boost::asio::buffer(moved_permanently); case status_type::moved_temporarily: return boost::asio::buffer(moved_temporarily); + case status_type::see_other_redirect: + return boost::asio::buffer(see_other_redirect); case status_type::not_modified: return boost::asio::buffer(not_modified); case status_type::bad_request: @@ -78,7 +81,9 @@ namespace http::server { buffers.push_back(boost::asio::buffer(misc_strings::crlf)); } buffers.emplace_back(boost::asio::buffer(misc_strings::crlf)); - buffers.emplace_back(boost::asio::buffer(content)); + if (!content.empty()) { + buffers.emplace_back(boost::asio::buffer(content)); + } return buffers; } @@ -212,4 +217,11 @@ namespace http::server { rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::text_html)}); stock_replies::as_content(status, rep.content); } + + void httpRedirect(Reply &rep, const std::string &location) { + rep.status = see_other_redirect; + rep.content.clear(); + rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::text_html)}); + rep.headers.push_back({.name = "Location", .value = location}); + } } // namespace http::server diff --git a/src/server/reply.hpp b/src/server/reply.hpp index e84536d..0193323 100644 --- a/src/server/reply.hpp +++ b/src/server/reply.hpp @@ -16,6 +16,7 @@ namespace http::server { multiple_choices = 300, moved_permanently = 301, moved_temporarily = 302, + see_other_redirect = 303, not_modified = 304, bad_request = 400, unauthorized = 401, @@ -46,6 +47,7 @@ namespace http::server { /// Get a stock reply. void stockReply(status_type status, Reply& rep); + void httpRedirect(Reply& rep, const std::string& location); } // namespace http::Server diff --git a/static/login-failed.html b/static/login-failed.html deleted file mode 100644 index 793ed81..0000000 --- a/static/login-failed.html +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - RSCM-101 | Вход - - - - - -
-

Вход

-
- {% csrf_token %} -
- Неверный логин или пароль -
- -
- - -
- -
- - -
- -
- -
-
-
- - - - \ No newline at end of file diff --git a/static/login.html b/static/login.html index 1a2a57d..8bc2298 100644 --- a/static/login.html +++ b/static/login.html @@ -61,13 +61,8 @@

Вход

-
- - - - - - + +
@@ -97,6 +92,41 @@ document.getElementById("login-form").submit() } } + + const loginForm = document.getElementById('login-form'); + const formErrorMessage = document.getElementById('form-error-message') + + function submitLoginForm() { + const username = document.getElementById('username').value + const password = document.getElementById('password').value + + const requestData = { + "username": username, + "password": password + }; + + fetch('/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requestData) + }).then(response => { + // Обработка ответа сервера + response.json().then((value) => { + if (value["error"]) { + formErrorMessage.innerText = value["error"] + formErrorMessage.removeAttribute("hidden") + } else { + window.location = "/" + } + }) + }).catch(error => { + formErrorMessage.innerText = error + formErrorMessage.removeAttribute("hidden") + console.error('Ошибка отправки запроса:', error) // Обработка ошибки отправки запроса + }) + } \ No newline at end of file