фича: автообновление сессии

This commit is contained in:
Vladislav Ostapov 2025-01-17 19:09:44 +03:00
parent a4214fd007
commit 3537965393
16 changed files with 170 additions and 96 deletions

View File

@ -25,7 +25,26 @@ else()
message(FATAL_ERROR "You must set `MODEM_TYPE` \"SCPC\" or \"TDMA\"!") message(FATAL_ERROR "You must set `MODEM_TYPE` \"SCPC\" or \"TDMA\"!")
endif() endif()
add_compile_options(-Wall -Wextra -Wsign-conversion) SET(PROJECT_GIT_REVISION "0")
FIND_PACKAGE(Git)
IF (GIT_FOUND)
EXECUTE_PROCESS (
COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
OUTPUT_VARIABLE GIT_HEAD
# ERROR_VARIABLE ERROR_RESULT
# RESULT_VARIABLE INFO_RESULT
ERROR_QUIET
OUTPUT_STRIP_TRAILING_WHITESPACE
)
IF ( ${GIT_HEAD} MATCHES "^.+$" )
STRING ( SUBSTRING ${GIT_HEAD} 0 8 VERSION_REVISION )
SET ( PROJECT_GIT_REVISION ${VERSION_REVISION} )
ENDIF()
ENDIF()
add_compile_options(-Wall -Wextra -Wsign-conversion -DPROJECT_GIT_REVISION="${PROJECT_GIT_REVISION}")
# максимальный размер тела запроса 200mb # максимальный размер тела запроса 200mb
add_definitions(-DHTTP_MAX_PAYLOAD=200000000) add_definitions(-DHTTP_MAX_PAYLOAD=200000000)

View File

@ -213,7 +213,7 @@
"values": [{"label": "РРУ", "value": "false"}, {"label": "АРУ", "value": "true"}] "values": [{"label": "РРУ", "value": "false"}, {"label": "АРУ", "value": "true"}]
}, },
{"widget": "number", "label": "Усиление, дБ", "name": "rxManualGain", "min": -40, "step": 0.01, "max": 40, "v_show": "paramRxtx.rxAgcEn === false"}, {"widget": "number", "label": "Усиление, дБ", "name": "rxManualGain", "min": -40, "step": 0.01, "max": 40, "v_show": "paramRxtx.rxAgcEn === false"},
{"widget": "watch", "label": "Текущее усиление", "model": "rxManualGain", "v_show": "paramRxtx.rxAgcEn === true"}, {"widget": "watch", "label": "Текущее усиление", "model": "paramRxtx.rxManualGain", "v_show": "paramRxtx.rxAgcEn === true"},
{"widget": "checkbox", "label": "Инверсия спектра", "name": "rxSpectrumInversion"}, {"widget": "checkbox", "label": "Инверсия спектра", "name": "rxSpectrumInversion"},
{"widget": "number", "label": "Центральная частота, КГц", "name": "rxCentralFreq", "min": 900000, "step": 0.01}, {"widget": "number", "label": "Центральная частота, КГц", "name": "rxCentralFreq", "min": 900000, "step": 0.01},
{"widget": "number", "label": "Символьная скорость, Бод", "name": "rxBaudrate", "min": 0, "step": 1}, {"widget": "number", "label": "Символьная скорость, Бод", "name": "rxBaudrate", "min": 0, "step": 1},

View File

@ -12,7 +12,7 @@
} }
this.submitStatus.{{ g['group'] }} = true this.submitStatus.{{ g['group'] }} = true
fetch('/api/set/{{ g["group"] }}', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(query) }) fetch('/api/set/{{ g["group"] }}', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(query), credentials: 'same-origin' })
.then(async (resp) => { let vals = await resp.json(); if (vals['status'] !== 'ok') { throw new Error(vals['error'] ? vals['error'] : "Server returns undefined error") } this.update{{ g['group'] | title }}Settings(vals) }) .then(async (resp) => { let vals = await resp.json(); if (vals['status'] !== 'ok') { throw new Error(vals['error'] ? vals['error'] : "Server returns undefined error") } this.update{{ g['group'] | title }}Settings(vals) })
.catch((reason) => { alert(`Ошибка при применении настроек: ${reason}`) }) .catch((reason) => { alert(`Ошибка при применении настроек: ${reason}`) })
.finally(() => { this.submitStatus.{{ g['group'] }} = false }) .finally(() => { this.submitStatus.{{ g['group'] }} = false })

View File

@ -93,7 +93,7 @@
resetPacketsStatistics() { resetPacketsStatistics() {
fetch('/api/resetPacketStatistics', { fetch('/api/resetPacketStatistics', {
method: 'POST' method: 'POST', credentials: 'same-origin'
}).then(() => { }).then(() => {
this.statRx.packetsOk = 0 this.statRx.packetsOk = 0
this.statRx.packetsBad = 0 this.statRx.packetsBad = 0

View File

@ -60,7 +60,7 @@
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify(query) body: JSON.stringify(query), credentials: 'same-origin'
}).then(async (resp) => { }).then(async (resp) => {
this.submitStatusQos = false this.submitStatusQos = false
if (resp['error']) { throw new Error(resp['error']) } if (resp['error']) { throw new Error(resp['error']) }

View File

@ -191,7 +191,7 @@
} }
} else { } else {
try { try {
let d = await fetch("/api/get/statistics") let d = await fetch("/api/get/statistics", { credentials: 'same-origin' })
this.updateStatistics(await d.json()) this.updateStatistics(await d.json())
} catch (e) { } catch (e) {
this.initState = "Ошибка обновления статистики" this.initState = "Ошибка обновления статистики"

View File

@ -1,8 +1,15 @@
#include "jwt.h" #include "jwt.h"
#include <random> #include <random>
#include "utils.h" #include "utils.h"
#include <sys/time.h>
static int64_t milliseconds() {
timeval tv{};
gettimeofday(&tv,nullptr);
return ((tv.tv_sec * 1000000l) + tv.tv_usec) / 1000;
}
std::string http::auth::jwt::secretKey; std::string http::auth::jwt::secretKey;
void http::auth::jwt::generateSecretKey() { void http::auth::jwt::generateSecretKey() {
@ -16,37 +23,45 @@ void http::auth::jwt::generateSecretKey() {
} }
} }
// payload, signature
static std::pair<std::string, std::string> 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) { http::auth::jwt::Jwt http::auth::jwt::Jwt::fromCookies(const std::string &cookie) {
auto pc = utils::parseCookies(cookie); auto pc = utils::parseCookies(cookie);
Jwt t; Jwt t;
if (pc.find("auth") != pc.end()) { if (pc.find("auth") != pc.end()) {
auto tmp = parseJwtFromCookie(pc.at("auth")); const auto auth = pc.at("auth");
t.payload = tmp.first; int firstDot = -1;
t.signature = tmp.second; int secondDot = -1;
for (size_t i = 0; i < auth.size(); i++) {
if (auth[i] == '.') {
if (firstDot < 0) { firstDot = static_cast<int>(i); }
else if (secondDot < 0) { secondDot = static_cast<int>(i); }
else {
// так быть не должно
return t;
}
}
}
if (firstDot < 0 || secondDot < 0 || secondDot - firstDot == 0) {
// так тоже быть не должно
return t;
}
try {
t.payload = auth.substr(0, firstDot);
t.signature = auth.substr(secondDot + 1);
t.lastUpdate = std::stol(auth.substr(firstDot + 1, secondDot - firstDot - 1));
// теперь проверим, что сигнатура верная, только тогда декодируем строку юзера
auto realSignature = utils::sha256(t.payload + std::to_string(t.lastUpdate) + secretKey);
if (t.signature != realSignature) {
t.payload.clear();
} else {
t.payload = utils::b64Decode(t.payload);
}
} catch (std::exception& e) {
t.payload.clear();
t.lastUpdate = 0;
t.signature.clear();
}
} }
return t; return t;
@ -55,6 +70,7 @@ http::auth::jwt::Jwt http::auth::jwt::Jwt::fromCookies(const std::string &cookie
http::auth::jwt::Jwt http::auth::jwt::Jwt::fromUser(const std::string &user) { http::auth::jwt::Jwt http::auth::jwt::Jwt::fromUser(const std::string &user) {
Jwt t; Jwt t;
t.payload = user; t.payload = user;
t.lastUpdate = milliseconds();
return t; return t;
} }
@ -63,18 +79,40 @@ bool http::auth::jwt::Jwt::isValid() {
return false; return false;
} }
auto realSignature = utils::sha256(this->payload + secretKey); // проверка сигнатуры не нужна, она была на стадии парсинга куки
return signature == realSignature; // auto realSignature = utils::sha256(utils::b64Encode(this->payload) + std::to_string(this->lastUpdate) + secretKey);
// if (signature != realSignature) {
// return false;
// }
const auto currTime = milliseconds();
return currTime <= lastUpdate + SESSION_LIVE_MS && currTime >= lastUpdate;
} }
std::string http::auth::jwt::Jwt::getUsername() { std::string http::auth::jwt::Jwt::getUsername() {
return payload; return payload;
} }
std::string http::auth::jwt::Jwt::asCookie() { std::string http::auth::jwt::Jwt::asCookie(bool isSecure) {
signature = utils::sha256(this->payload + secretKey); this->lastUpdate = milliseconds();
auto val = utils::b64Encode(payload) + "." + signature; const auto uTime = std::to_string(this->lastUpdate);
return "auth=" + val + ";Path=/; Max-Age=86400; HttpOnly; SameSite=Lax"; const auto encodedPayload = utils::b64Encode(payload);
signature = utils::sha256(encodedPayload + uTime + secretKey);
const auto val = encodedPayload + "." + uTime + "." + signature;
std::string cookie = "auth=";
cookie += val;
cookie += ";Path=/; Max-Age=";
cookie += std::to_string(SESSION_LIVE_MS / 1000);
if (isSecure) {
cookie += "; Secure";
}
cookie += "; HttpOnly; SameSite=Lax";
return cookie;
}
bool http::auth::jwt::Jwt::needUpdate() const {
return milliseconds() >= lastUpdate + SESSION_UPDATE_THRESHOLD;
} }
http::auth::jwt::Jwt::~Jwt() = default; http::auth::jwt::Jwt::~Jwt() = default;

View File

@ -6,7 +6,10 @@
namespace http::auth::jwt { namespace http::auth::jwt {
extern std::string secretKey; extern std::string secretKey;
constexpr const char* EMPTY_AUTH_COOKIE = "auth=;Path=/; Max-Age=86400; HttpOnly; SameSite=Lax";; constexpr const char* EMPTY_AUTH_COOKIE = "auth=;Path=/; Max-Age=86400; HttpOnly; SameSite=Lax";
constexpr int64_t SESSION_LIVE_MS = 24 * 60 * 60 * 1000; // 24 часа
constexpr int64_t SESSION_UPDATE_THRESHOLD = 10 * 60 * 1000; // 10 минут
void generateSecretKey(); void generateSecretKey();
@ -17,6 +20,7 @@ namespace http::auth::jwt {
*/ */
class Jwt { class Jwt {
std::string payload; std::string payload;
int64_t lastUpdate = 0;
std::string signature; std::string signature;
public: public:
static Jwt fromCookies(const std::string& cookie); static Jwt fromCookies(const std::string& cookie);
@ -26,7 +30,9 @@ namespace http::auth::jwt {
std::string getUsername(); std::string getUsername();
std::string asCookie(); std::string asCookie(bool isSecure = false);
bool needUpdate() const;
~Jwt(); ~Jwt();
}; };

View File

@ -44,12 +44,12 @@ http::auth::User::~User() = default;
http::auth::AuthProvider::AuthProvider() = default; http::auth::AuthProvider::AuthProvider() = default;
std::shared_ptr<http::auth::User> http::auth::AuthProvider::doAuth(const std::string &username, const std::string &password, server::Reply &rep) { std::shared_ptr<http::auth::User> http::auth::AuthProvider::doAuth(const std::string &username, const std::string &password, const server::Request &req, server::Reply &rep) {
for (const auto& u: users) { for (const auto& u: users) {
if (u->username == username) { if (u->username == username) {
if (u->checkPassword(password)) { if (u->checkPassword(password)) {
auto t = jwt::Jwt::fromUser(u->username); auto t = jwt::Jwt::fromUser(u->username);
rep.headers.push_back({.name = "Set-Cookie", .value = t.asCookie()}); rep.headers.push_back({.name = "Set-Cookie", .value = t.asCookie(req.isSecure)});
return u; return u;
} }
BOOST_LOG_TRIVIAL(warning) << "http::auth::AuthProvider::doAuth(): Failed to login " << username << ", password: " << password << " (incorrect password)"; BOOST_LOG_TRIVIAL(warning) << "http::auth::AuthProvider::doAuth(): Failed to login " << username << ", password: " << password << " (incorrect password)";
@ -60,13 +60,17 @@ std::shared_ptr<http::auth::User> http::auth::AuthProvider::doAuth(const std::st
return nullptr; return nullptr;
} }
std::shared_ptr<http::auth::User> http::auth::AuthProvider::getSession(const server::Request &req) { std::shared_ptr<http::auth::User> http::auth::AuthProvider::getSession(const server::Request &req, server::Reply &rep) {
auto t = jwt::Jwt::fromCookies(req.getHeaderValue("cookie")); auto t = jwt::Jwt::fromCookies(req.getHeaderValue("cookie"));
if (t.isValid()) { if (t.isValid()) {
const auto name = t.getUsername(); const auto name = t.getUsername();
// токен валидный, ищем юзера // токен валидный, ищем юзера
for (auto& u: users) { for (auto& u: users) {
if (u->username == name) { if (u->username == name) {
// на всякий случай тут проверяем, что токен пора обновлять
if (t.needUpdate()) {
rep.headers.push_back({.name = "Set-Cookie", .value = t.asCookie(req.isSecure)});
}
return u; return u;
} }
} }
@ -84,7 +88,7 @@ http::auth::AuthRequiredResource::AuthRequiredResource(const std::string &path,
BasicResource(path), provider_(provider), generator_(std::move(generator)), perms(perms) {} BasicResource(path), provider_(provider), generator_(std::move(generator)), perms(perms) {}
void http::auth::AuthRequiredResource::handle(const server::Request &req, server::Reply &rep) { void http::auth::AuthRequiredResource::handle(const server::Request &req, server::Reply &rep) {
if (auto user = this->provider_.getSession(req)) { if (auto user = this->provider_.getSession(req, rep)) {
if (user->checkPremisions(this->perms)) { if (user->checkPremisions(this->perms)) {
this->generator_(req, rep); this->generator_(req, rep);
return; return;

View File

@ -70,20 +70,23 @@ namespace http::auth {
* @note Класс устанавливает заголовок 'Set-Cookie' в ответе, и этот заголовок должен дойти до пользователя! * @note Класс устанавливает заголовок 'Set-Cookie' в ответе, и этот заголовок должен дойти до пользователя!
*/ */
class AuthProvider { class AuthProvider {
void updateSessionHook();
public: public:
AuthProvider(); AuthProvider();
/** /**
* Авторизовать пользователя. * Авторизовать пользователя.
* *
* @param req
* @param rep * @param rep
* @return true, в случае успешной авторизации * @return true, в случае успешной авторизации
*/ */
std::shared_ptr<http::auth::User> doAuth(const std::string &username, const std::string &password, server::Reply &rep); std::shared_ptr<http::auth::User> doAuth(const std::string &username, const std::string &password, const server::Request &req, server::Reply &rep);
std::vector<std::shared_ptr<User>> users; std::vector<std::shared_ptr<User>> users;
std::shared_ptr<User> getSession(const server::Request &req); std::shared_ptr<User> getSession(const server::Request &req, server::Reply &rep);
~AuthProvider(); ~AuthProvider();
}; };

View File

@ -147,7 +147,7 @@ public:
void registerResources(http::server::Server& s) { void registerResources(http::server::Server& s) {
s.resources.emplace_back(std::make_unique<http::resource::GenericResource>("/", [this](const auto& req, auto& rep) { s.resources.emplace_back(std::make_unique<http::resource::GenericResource>("/", [this](const auto& req, auto& rep) {
auto user = auth.getSession(req); auto user = auth.getSession(req, rep);
if (user == nullptr) { if (user == nullptr) {
http::server::httpRedirect(rep, "/login"); http::server::httpRedirect(rep, "/login");
} else { } else {
@ -157,7 +157,7 @@ public:
s.resources.emplace_back(std::make_unique<http::resource::GenericResource>("/login", [this](const auto& req, auto& rep) { s.resources.emplace_back(std::make_unique<http::resource::GenericResource>("/login", [this](const auto& req, auto& rep) {
if (req.method == "GET") { if (req.method == "GET") {
auto user = auth.getSession(req); auto user = auth.getSession(req, rep);
if (user == nullptr) { if (user == nullptr) {
sf->serve(LOGIN_HTML, rep); sf->serve(LOGIN_HTML, rep);
} else { } else {
@ -172,7 +172,7 @@ public:
boost::property_tree::ptree pt; boost::property_tree::ptree pt;
read_json(is, pt); read_json(is, pt);
auto u = auth.doAuth(pt.get<std::string>("username"), pt.get<std::string>("password"), rep); auto u = auth.doAuth(pt.get<std::string>("username"), pt.get<std::string>("password"), req, rep);
if (u == nullptr) { if (u == nullptr) {
throw std::runtime_error("invalid session"); throw std::runtime_error("invalid session");
} }
@ -206,10 +206,10 @@ public:
s.resources.emplace_back(std::make_unique<http::auth::AuthRequiredResource>("/api/get/statistics", this->auth, http::auth::User::WATCH_STATISTICS, [this](const auto& req, auto& rep) { s.resources.emplace_back(std::make_unique<http::auth::AuthRequiredResource>("/api/get/statistics", this->auth, http::auth::User::WATCH_STATISTICS, [this](const auto& req, auto& rep) {
if (req.method != "GET") { if (req.method != "GET") {
http::server::stockReply(http::server::bad_request, rep); http::server::stockReply(http::server::bad_request, rep);
return;
} }
rep.status = http::server::ok; rep.status = http::server::ok;
rep.headers.clear();
rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)}); rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)});
std::string result = R"({"mainState":)"; std::string result = R"({"mainState":)";
result += api->loadTerminalState(); result += api->loadTerminalState();
@ -222,10 +222,10 @@ public:
s.resources.emplace_back(std::make_unique<http::auth::AuthRequiredResource>("/api/get/settings", this->auth, http::auth::User::WATCH_SETTINGS, [this](const auto& req, auto& rep) { s.resources.emplace_back(std::make_unique<http::auth::AuthRequiredResource>("/api/get/settings", this->auth, http::auth::User::WATCH_SETTINGS, [this](const auto& req, auto& rep) {
if (req.method != "GET") { if (req.method != "GET") {
http::server::stockReply(http::server::bad_request, rep); http::server::stockReply(http::server::bad_request, rep);
return;
} }
rep.status = http::server::ok; rep.status = http::server::ok;
rep.headers.clear();
rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)}); rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)});
std::string result = R"({"settings":)"; std::string result = R"({"settings":)";
result += api->loadSettings(); result += api->loadSettings();
@ -236,10 +236,10 @@ public:
s.resources.emplace_back(std::make_unique<http::auth::AuthRequiredResource>("/api/get/aboutFirmware", this->auth, 0, [this](const auto& req, auto& rep) { s.resources.emplace_back(std::make_unique<http::auth::AuthRequiredResource>("/api/get/aboutFirmware", this->auth, 0, [this](const auto& req, auto& rep) {
if (req.method != "GET") { if (req.method != "GET") {
http::server::stockReply(http::server::bad_request, rep); http::server::stockReply(http::server::bad_request, rep);
return;
} }
rep.status = http::server::ok; rep.status = http::server::ok;
rep.headers.clear();
rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)}); rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)});
const auto result = api->loadFirmwareVersion(); const auto result = api->loadFirmwareVersion();
rep.content.insert(rep.content.end(), result.c_str(), result.c_str() + result.size()); rep.content.insert(rep.content.end(), result.c_str(), result.c_str() + result.size());
@ -248,11 +248,11 @@ public:
s.resources.emplace_back(std::make_unique<http::auth::AuthRequiredResource>("/api/resetPacketStatistics", this->auth, http::auth::User::RESET_PACKET_STATISTICS, [this](const auto& req, auto& rep) { s.resources.emplace_back(std::make_unique<http::auth::AuthRequiredResource>("/api/resetPacketStatistics", this->auth, http::auth::User::RESET_PACKET_STATISTICS, [this](const auto& req, auto& rep) {
if (req.method != "POST") { if (req.method != "POST") {
http::server::stockReply(http::server::bad_request, rep); http::server::stockReply(http::server::bad_request, rep);
return;
} }
api->resetPacketStatistics(); api->resetPacketStatistics();
rep.status = http::server::ok; rep.status = http::server::ok;
rep.headers.clear();
rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)}); rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)});
const std::string result = R"({"status":"ok")"; const std::string result = R"({"status":"ok")";
rep.content.insert(rep.content.end(), result.c_str(), result.c_str() + result.size()); rep.content.insert(rep.content.end(), result.c_str(), result.c_str() + result.size());
@ -261,10 +261,10 @@ public:
s.resources.emplace_back(std::make_unique<http::auth::AuthRequiredResource>("/api/set/qos", this->auth, http::auth::User::SETUP_QOS, [this](const auto& req, auto& rep) { s.resources.emplace_back(std::make_unique<http::auth::AuthRequiredResource>("/api/set/qos", this->auth, http::auth::User::SETUP_QOS, [this](const auto& req, auto& rep) {
if (req.method != "POST") { if (req.method != "POST") {
http::server::stockReply(http::server::bad_request, rep); http::server::stockReply(http::server::bad_request, rep);
return;
} }
rep.status = http::server::ok; rep.status = http::server::ok;
rep.headers.clear();
rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)}); rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)});
try { try {
@ -289,10 +289,10 @@ public:
s.resources.emplace_back(std::make_unique<http::auth::AuthRequiredResource>("/api/set/buclnb", this->auth, http::auth::User::SUPERUSER, [this](const auto& req, auto& rep) { s.resources.emplace_back(std::make_unique<http::auth::AuthRequiredResource>("/api/set/buclnb", this->auth, http::auth::User::SUPERUSER, [this](const auto& req, auto& rep) {
if (req.method != "POST") { if (req.method != "POST") {
http::server::stockReply(http::server::bad_request, rep); http::server::stockReply(http::server::bad_request, rep);
return;
} }
rep.status = http::server::ok; rep.status = http::server::ok;
rep.headers.clear();
rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)}); rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)});
try { try {
@ -317,10 +317,10 @@ public:
s.resources.emplace_back(std::make_unique<http::auth::AuthRequiredResource>("/api/set/cinc", this->auth, http::auth::User::EDIT_SETTINGS, [this](const auto& req, auto& rep) { s.resources.emplace_back(std::make_unique<http::auth::AuthRequiredResource>("/api/set/cinc", this->auth, http::auth::User::EDIT_SETTINGS, [this](const auto& req, auto& rep) {
if (req.method != "POST") { if (req.method != "POST") {
http::server::stockReply(http::server::bad_request, rep); http::server::stockReply(http::server::bad_request, rep);
return;
} }
rep.status = http::server::ok; rep.status = http::server::ok;
rep.headers.clear();
rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)}); rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)});
try { try {
@ -345,10 +345,10 @@ public:
s.resources.emplace_back(std::make_unique<http::auth::AuthRequiredResource>("/api/set/rxtx", this->auth, http::auth::User::EDIT_SETTINGS, [this](const auto& req, auto& rep) { s.resources.emplace_back(std::make_unique<http::auth::AuthRequiredResource>("/api/set/rxtx", this->auth, http::auth::User::EDIT_SETTINGS, [this](const auto& req, auto& rep) {
if (req.method != "POST") { if (req.method != "POST") {
http::server::stockReply(http::server::bad_request, rep); http::server::stockReply(http::server::bad_request, rep);
return;
} }
rep.status = http::server::ok; rep.status = http::server::ok;
rep.headers.clear();
rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)}); rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)});
try { try {
@ -373,10 +373,10 @@ public:
s.resources.emplace_back(std::make_unique<http::auth::AuthRequiredResource>("/api/set/network", this->auth, http::auth::User::EDIT_SETTINGS, [this](const auto& req, auto& rep) { s.resources.emplace_back(std::make_unique<http::auth::AuthRequiredResource>("/api/set/network", this->auth, http::auth::User::EDIT_SETTINGS, [this](const auto& req, auto& rep) {
if (req.method != "POST") { if (req.method != "POST") {
http::server::stockReply(http::server::bad_request, rep); http::server::stockReply(http::server::bad_request, rep);
return;
} }
rep.status = http::server::ok; rep.status = http::server::ok;
rep.headers.clear();
rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)}); rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)});
try { try {
@ -401,9 +401,9 @@ public:
s.resources.emplace_back(std::make_unique<http::auth::AuthRequiredResource>("/api/reboot", this->auth, 0, [this](const auto& req, auto& rep) { s.resources.emplace_back(std::make_unique<http::auth::AuthRequiredResource>("/api/reboot", this->auth, 0, [this](const auto& req, auto& rep) {
if (req.method != "POST") { if (req.method != "POST") {
http::server::stockReply(http::server::bad_request, rep); http::server::stockReply(http::server::bad_request, rep);
return;
} }
rep.status = http::server::ok; rep.status = http::server::ok;
rep.headers.clear();
rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)}); rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)});
const std::string result = R"({"status":"ok"})"; const std::string result = R"({"status":"ok"})";
rep.content.insert(rep.content.end(), result.c_str(), result.c_str() + result.size()); rep.content.insert(rep.content.end(), result.c_str(), result.c_str() + result.size());
@ -413,9 +413,9 @@ public:
s.resources.emplace_back(std::make_unique<http::auth::AuthRequiredResource>("/api/resetSettings", this->auth, http::auth::User::SUPERUSER, [this](const auto& req, auto& rep) { s.resources.emplace_back(std::make_unique<http::auth::AuthRequiredResource>("/api/resetSettings", this->auth, http::auth::User::SUPERUSER, [this](const auto& req, auto& rep) {
if (req.method != "POST") { if (req.method != "POST") {
http::server::stockReply(http::server::bad_request, rep); http::server::stockReply(http::server::bad_request, rep);
return;
} }
rep.status = http::server::ok; rep.status = http::server::ok;
rep.headers.clear();
rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)}); rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)});
const std::string result = R"({"status":"ok"})"; const std::string result = R"({"status":"ok"})";
rep.content.insert(rep.content.end(), result.c_str(), result.c_str() + result.size()); rep.content.insert(rep.content.end(), result.c_str(), result.c_str() + result.size());
@ -431,7 +431,6 @@ public:
onUploadFirmware(req); onUploadFirmware(req);
rep.status = http::server::ok; rep.status = http::server::ok;
rep.headers.clear();
rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)}); rep.headers.push_back({.name = "Content-Type", .value = toString(mime_types::json)});
std::string result = R"({"status":"ok","fwsize":)"; std::string result = R"({"status":"ok","fwsize":)";
result += std::to_string(req.payload.size()); result += std::to_string(req.payload.size());

View File

@ -6,10 +6,14 @@
namespace http::server { namespace http::server {
const char* SERVER_HEADER_VALUE = "TerminalWebServer v0.1"; const char* SERVER_HEADER_VALUE = "TerminalWebServer"
#ifdef PROJECT_GIT_REVISION
" " PROJECT_GIT_REVISION
#endif
;
Connection::Connection(boost::asio::ip::tcp::socket socket, ConnectionManager &manager, request_handler handler) Connection::Connection(boost::asio::ip::tcp::socket socket, ConnectionManager &manager, request_handler handler)
: socket_(std::move(socket)), connection_manager_(manager), request_handler_(std::move(handler)), request_(), reply_() { : socket_(std::move(socket)), connection_manager_(manager), request_handler_(std::move(handler)), request_(false), reply_() {
} }
void Connection::start() { void Connection::start() {
@ -51,7 +55,7 @@ namespace http::server {
void Connection::doWrite() { void Connection::doWrite() {
reply_.headers.push_back({.name = "Server", .value = SERVER_HEADER_VALUE}); reply_.headers.push_back({.name = "Server", .value = SERVER_HEADER_VALUE});
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) { if (request_.httpVersionMajor == 1) {
reply_.headers.push_back({.name = "Connection", .value = "keep-alive"}); reply_.headers.push_back({.name = "Connection", .value = "keep-alive"});
} }
@ -71,7 +75,7 @@ namespace http::server {
} }
SslConnection::SslConnection(boost::asio::ip::tcp::socket socket, ConnectionManager &manager, request_handler handler, const std::shared_ptr<boost::asio::ssl::context>& ctx): SslConnection::SslConnection(boost::asio::ip::tcp::socket socket, ConnectionManager &manager, request_handler handler, const std::shared_ptr<boost::asio::ssl::context>& ctx):
stream_(std::move(socket), *ctx), connection_manager_(manager), request_handler_(std::move(handler)), request_(), reply_() { stream_(std::move(socket), *ctx), connection_manager_(manager), request_handler_(std::move(handler)), request_(true), reply_() {
} }
void SslConnection::start() { void SslConnection::start() {
@ -125,7 +129,7 @@ namespace http::server {
void SslConnection::doWrite() { void SslConnection::doWrite() {
reply_.headers.push_back({.name = "Server", .value = SERVER_HEADER_VALUE}); reply_.headers.push_back({.name = "Server", .value = SERVER_HEADER_VALUE});
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) { if (request_.httpVersionMajor == 1) {
reply_.headers.push_back({.name = "Connection", .value = "keep-alive"}); reply_.headers.push_back({.name = "Connection", .value = "keep-alive"});
} }

View File

@ -21,7 +21,7 @@ namespace http::server {
/// A request received from a client. /// A request received from a client.
class Request { class Request {
public: public:
Request(); Request(bool secure);
void reset(); void reset();
std::string getHeaderValue(const std::string& headerName) const; std::string getHeaderValue(const std::string& headerName) const;
@ -29,9 +29,10 @@ namespace http::server {
std::string method; std::string method;
std::string queryUri; std::string queryUri;
std::unique_ptr<Url> url; std::unique_ptr<Url> url;
bool is_keep_alive{}; bool isKeepAlive{};
int http_version_major{}; const bool isSecure;
int http_version_minor{}; int httpVersionMajor{};
int httpVersionMinor{};
std::vector<header> headers; std::vector<header> headers;
std::vector<char> payload; std::vector<char> payload;

View File

@ -51,7 +51,7 @@ namespace http::server {
Url::~Url() = default; Url::~Url() = default;
Request::Request() = default; Request::Request(bool secure): isSecure(secure) {}
void Request::reset() { void Request::reset() {
method = ""; method = "";
@ -59,9 +59,9 @@ namespace http::server {
if (url != nullptr) { if (url != nullptr) {
url.reset(nullptr); url.reset(nullptr);
} }
is_keep_alive = false; isKeepAlive = false;
http_version_major = 0; httpVersionMajor = 0;
http_version_minor = 0; httpVersionMinor = 0;
headers.clear(); headers.clear();
payload.clear(); payload.clear();
} }
@ -151,8 +151,8 @@ namespace http::server {
} }
case http_version_slash: case http_version_slash:
if (input == '/') { if (input == '/') {
req.http_version_major = 0; req.httpVersionMajor = 0;
req.http_version_minor = 0; req.httpVersionMinor = 0;
state_ = http_version_major_start; state_ = http_version_major_start;
return indeterminate; return indeterminate;
} else { } else {
@ -160,7 +160,7 @@ namespace http::server {
} }
case http_version_major_start: case http_version_major_start:
if (is_digit(input)) { if (is_digit(input)) {
req.http_version_major = req.http_version_major * 10 + input - '0'; req.httpVersionMajor = req.httpVersionMajor * 10 + input - '0';
state_ = http_version_major; state_ = http_version_major;
return indeterminate; return indeterminate;
} else { } else {
@ -171,14 +171,14 @@ namespace http::server {
state_ = http_version_minor_start; state_ = http_version_minor_start;
return indeterminate; return indeterminate;
} else if (is_digit(input)) { } else if (is_digit(input)) {
req.http_version_major = req.http_version_major * 10 + input - '0'; req.httpVersionMajor = req.httpVersionMajor * 10 + input - '0';
return indeterminate; return indeterminate;
} else { } else {
return bad; return bad;
} }
case http_version_minor_start: case http_version_minor_start:
if (is_digit(input)) { if (is_digit(input)) {
req.http_version_minor = req.http_version_minor * 10 + input - '0'; req.httpVersionMinor = req.httpVersionMinor * 10 + input - '0';
state_ = http_version_minor; state_ = http_version_minor;
return indeterminate; return indeterminate;
} else { } else {
@ -189,7 +189,7 @@ namespace http::server {
state_ = expecting_newline_1; state_ = expecting_newline_1;
return indeterminate; return indeterminate;
} else if (is_digit(input)) { } else if (is_digit(input)) {
req.http_version_minor = req.http_version_minor * 10 + input - '0'; req.httpVersionMinor = req.httpVersionMinor * 10 + input - '0';
return indeterminate; return indeterminate;
} else { } else {
return bad; return bad;

View File

@ -269,7 +269,7 @@
</select> </select>
</label> </label>
<label v-show="paramRxtx.rxAgcEn === false"><span>Усиление, дБ</span><input type="number" v-model="paramRxtx.rxManualGain" min="-40" max="40" step="0.01"/></label> <label v-show="paramRxtx.rxAgcEn === false"><span>Усиление, дБ</span><input type="number" v-model="paramRxtx.rxManualGain" min="-40" max="40" step="0.01"/></label>
<label v-show="paramRxtx.rxAgcEn === true"><span>Текущее усиление</span><input type="text" readonly v-model="rxManualGain"/></label> <label v-show="paramRxtx.rxAgcEn === true"><span>Текущее усиление</span><input type="text" readonly v-model="paramRxtx.rxManualGain"/></label>
<label> <label>
<span>Инверсия спектра</span> <span>Инверсия спектра</span>
<span class="toggle-input"><input type="checkbox" v-model="paramRxtx.rxSpectrumInversion" /><span class="slider"></span></span> <span class="toggle-input"><input type="checkbox" v-model="paramRxtx.rxSpectrumInversion" /><span class="slider"></span></span>
@ -774,7 +774,7 @@
} }
this.submitStatus.rxtx = true this.submitStatus.rxtx = true
fetch('/api/set/rxtx', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(query) }) fetch('/api/set/rxtx', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(query), credentials: 'same-origin' })
.then(async (resp) => { let vals = await resp.json(); if (vals['status'] !== 'ok') { throw new Error(vals['error'] ? vals['error'] : "Server returns undefined error") } this.updateRxtxSettings(vals) }) .then(async (resp) => { let vals = await resp.json(); if (vals['status'] !== 'ok') { throw new Error(vals['error'] ? vals['error'] : "Server returns undefined error") } this.updateRxtxSettings(vals) })
.catch((reason) => { alert(`Ошибка при применении настроек: ${reason}`) }) .catch((reason) => { alert(`Ошибка при применении настроек: ${reason}`) })
.finally(() => { this.submitStatus.rxtx = false }) .finally(() => { this.submitStatus.rxtx = false })
@ -793,7 +793,7 @@
} }
this.submitStatus.cinc = true this.submitStatus.cinc = true
fetch('/api/set/cinc', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(query) }) fetch('/api/set/cinc', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(query), credentials: 'same-origin' })
.then(async (resp) => { let vals = await resp.json(); if (vals['status'] !== 'ok') { throw new Error(vals['error'] ? vals['error'] : "Server returns undefined error") } this.updateCincSettings(vals) }) .then(async (resp) => { let vals = await resp.json(); if (vals['status'] !== 'ok') { throw new Error(vals['error'] ? vals['error'] : "Server returns undefined error") } this.updateCincSettings(vals) })
.catch((reason) => { alert(`Ошибка при применении настроек: ${reason}`) }) .catch((reason) => { alert(`Ошибка при применении настроек: ${reason}`) })
.finally(() => { this.submitStatus.cinc = false }) .finally(() => { this.submitStatus.cinc = false })
@ -812,7 +812,7 @@
} }
this.submitStatus.buclnb = true this.submitStatus.buclnb = true
fetch('/api/set/buclnb', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(query) }) fetch('/api/set/buclnb', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(query), credentials: 'same-origin' })
.then(async (resp) => { let vals = await resp.json(); if (vals['status'] !== 'ok') { throw new Error(vals['error'] ? vals['error'] : "Server returns undefined error") } this.updateBuclnbSettings(vals) }) .then(async (resp) => { let vals = await resp.json(); if (vals['status'] !== 'ok') { throw new Error(vals['error'] ? vals['error'] : "Server returns undefined error") } this.updateBuclnbSettings(vals) })
.catch((reason) => { alert(`Ошибка при применении настроек: ${reason}`) }) .catch((reason) => { alert(`Ошибка при применении настроек: ${reason}`) })
.finally(() => { this.submitStatus.buclnb = false }) .finally(() => { this.submitStatus.buclnb = false })
@ -826,7 +826,7 @@
} }
this.submitStatus.tcpaccel = true this.submitStatus.tcpaccel = true
fetch('/api/set/tcpaccel', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(query) }) fetch('/api/set/tcpaccel', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(query), credentials: 'same-origin' })
.then(async (resp) => { let vals = await resp.json(); if (vals['status'] !== 'ok') { throw new Error(vals['error'] ? vals['error'] : "Server returns undefined error") } this.updateTcpaccelSettings(vals) }) .then(async (resp) => { let vals = await resp.json(); if (vals['status'] !== 'ok') { throw new Error(vals['error'] ? vals['error'] : "Server returns undefined error") } this.updateTcpaccelSettings(vals) })
.catch((reason) => { alert(`Ошибка при применении настроек: ${reason}`) }) .catch((reason) => { alert(`Ошибка при применении настроек: ${reason}`) })
.finally(() => { this.submitStatus.tcpaccel = false }) .finally(() => { this.submitStatus.tcpaccel = false })
@ -843,7 +843,7 @@
} }
this.submitStatus.network = true this.submitStatus.network = true
fetch('/api/set/network', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(query) }) fetch('/api/set/network', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(query), credentials: 'same-origin' })
.then(async (resp) => { let vals = await resp.json(); if (vals['status'] !== 'ok') { throw new Error(vals['error'] ? vals['error'] : "Server returns undefined error") } this.updateNetworkSettings(vals) }) .then(async (resp) => { let vals = await resp.json(); if (vals['status'] !== 'ok') { throw new Error(vals['error'] ? vals['error'] : "Server returns undefined error") } this.updateNetworkSettings(vals) })
.catch((reason) => { alert(`Ошибка при применении настроек: ${reason}`) }) .catch((reason) => { alert(`Ошибка при применении настроек: ${reason}`) })
.finally(() => { this.submitStatus.network = false }) .finally(() => { this.submitStatus.network = false })
@ -1001,7 +1001,7 @@
resetPacketsStatistics() { resetPacketsStatistics() {
fetch('/api/resetPacketStatistics', { fetch('/api/resetPacketStatistics', {
method: 'POST' method: 'POST', credentials: 'same-origin'
}).then(() => { }).then(() => {
this.statRx.packetsOk = 0 this.statRx.packetsOk = 0
this.statRx.packetsBad = 0 this.statRx.packetsBad = 0
@ -1075,7 +1075,7 @@
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify(query) body: JSON.stringify(query), credentials: 'same-origin'
}).then(async (resp) => { }).then(async (resp) => {
this.submitStatusQos = false this.submitStatusQos = false
if (resp['error']) { throw new Error(resp['error']) } if (resp['error']) { throw new Error(resp['error']) }
@ -1327,7 +1327,7 @@
} }
} else { } else {
try { try {
let d = await fetch("/api/get/statistics") let d = await fetch("/api/get/statistics", { credentials: 'same-origin' })
this.updateStatistics(await d.json()) this.updateStatistics(await d.json())
} catch (e) { } catch (e) {
this.initState = "Ошибка обновления статистики" this.initState = "Ошибка обновления статистики"

View File

@ -435,7 +435,7 @@
} }
this.submitStatus.rxtx = true this.submitStatus.rxtx = true
fetch('/api/set/rxtx', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(query) }) fetch('/api/set/rxtx', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(query), credentials: 'same-origin' })
.then(async (resp) => { let vals = await resp.json(); if (vals['status'] !== 'ok') { throw new Error(vals['error'] ? vals['error'] : "Server returns undefined error") } this.updateRxtxSettings(vals) }) .then(async (resp) => { let vals = await resp.json(); if (vals['status'] !== 'ok') { throw new Error(vals['error'] ? vals['error'] : "Server returns undefined error") } this.updateRxtxSettings(vals) })
.catch((reason) => { alert(`Ошибка при применении настроек: ${reason}`) }) .catch((reason) => { alert(`Ошибка при применении настроек: ${reason}`) })
.finally(() => { this.submitStatus.rxtx = false }) .finally(() => { this.submitStatus.rxtx = false })
@ -454,7 +454,7 @@
} }
this.submitStatus.buclnb = true this.submitStatus.buclnb = true
fetch('/api/set/buclnb', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(query) }) fetch('/api/set/buclnb', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(query), credentials: 'same-origin' })
.then(async (resp) => { let vals = await resp.json(); if (vals['status'] !== 'ok') { throw new Error(vals['error'] ? vals['error'] : "Server returns undefined error") } this.updateBuclnbSettings(vals) }) .then(async (resp) => { let vals = await resp.json(); if (vals['status'] !== 'ok') { throw new Error(vals['error'] ? vals['error'] : "Server returns undefined error") } this.updateBuclnbSettings(vals) })
.catch((reason) => { alert(`Ошибка при применении настроек: ${reason}`) }) .catch((reason) => { alert(`Ошибка при применении настроек: ${reason}`) })
.finally(() => { this.submitStatus.buclnb = false }) .finally(() => { this.submitStatus.buclnb = false })
@ -471,7 +471,7 @@
} }
this.submitStatus.network = true this.submitStatus.network = true
fetch('/api/set/network', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(query) }) fetch('/api/set/network', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(query), credentials: 'same-origin' })
.then(async (resp) => { let vals = await resp.json(); if (vals['status'] !== 'ok') { throw new Error(vals['error'] ? vals['error'] : "Server returns undefined error") } this.updateNetworkSettings(vals) }) .then(async (resp) => { let vals = await resp.json(); if (vals['status'] !== 'ok') { throw new Error(vals['error'] ? vals['error'] : "Server returns undefined error") } this.updateNetworkSettings(vals) })
.catch((reason) => { alert(`Ошибка при применении настроек: ${reason}`) }) .catch((reason) => { alert(`Ошибка при применении настроек: ${reason}`) })
.finally(() => { this.submitStatus.network = false }) .finally(() => { this.submitStatus.network = false })
@ -585,7 +585,7 @@
resetPacketsStatistics() { resetPacketsStatistics() {
fetch('/api/resetPacketStatistics', { fetch('/api/resetPacketStatistics', {
method: 'POST' method: 'POST', credentials: 'same-origin'
}).then(() => { }).then(() => {
this.statRx.packetsOk = 0 this.statRx.packetsOk = 0
this.statRx.packetsBad = 0 this.statRx.packetsBad = 0
@ -684,7 +684,7 @@
} }
} else { } else {
try { try {
let d = await fetch("/api/get/statistics") let d = await fetch("/api/get/statistics", { credentials: 'same-origin' })
this.updateStatistics(await d.json()) this.updateStatistics(await d.json())
} catch (e) { } catch (e) {
this.initState = "Ошибка обновления статистики" this.initState = "Ошибка обновления статистики"