core: use fingerprints instead of ips for db
This commit is contained in:
@@ -7,15 +7,30 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
constexpr const char* DB_FILE = "data.db";
|
constexpr const char* DB_FILE = "data.db";
|
||||||
constexpr const uint64_t DB_TIME_BEFORE_CLEANUP_MS = 1000 * 60 * 10; // 10 mins
|
constexpr const uint64_t DB_TIME_BEFORE_CLEANUP_MS = 1000 * 60 * 10; // 10 mins
|
||||||
constexpr const uint64_t DB_TOKEN_LIFE_LENGTH_S = 60 * 60; // 1hr
|
constexpr const uint64_t DB_TOKEN_LIFE_LENGTH_S = 60 * 60; // 1hr
|
||||||
constexpr const uint64_t DB_CHALLENGE_LIFE_LENGTH_S = 60 * 10; // 10 mins
|
constexpr const uint64_t DB_CHALLENGE_LIFE_LENGTH_S = 60 * 10; // 10 mins
|
||||||
|
constexpr const uint64_t DB_SCHEMA_VERSION = 2;
|
||||||
|
|
||||||
//
|
//
|
||||||
|
static std::string readFileAsText(const std::string& path) {
|
||||||
|
std::ifstream ifs(path);
|
||||||
|
auto res = std::string((std::istreambuf_iterator<char>(ifs)), (std::istreambuf_iterator<char>()));
|
||||||
|
if (res.back() == '\n')
|
||||||
|
res.pop_back();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string dbDir() {
|
||||||
|
static const std::string dir = std::filesystem::canonical(g_pGlobalState->cwd + "/" + g_pConfig->m_config.data_dir).string();
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
static std::string dbPath() {
|
static std::string dbPath() {
|
||||||
static const std::string path = std::filesystem::canonical(g_pGlobalState->cwd + "/" + g_pConfig->m_config.data_dir).string() + "/" + DB_FILE;
|
static const std::string path = dbDir() + "/" + DB_FILE;
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,31 +38,39 @@ static bool isHashValid(const std::string_view sv) {
|
|||||||
return std::all_of(sv.begin(), sv.end(), [](const char& c) { return (c >= 'a' && c <= 'f') || std::isdigit(c); });
|
return std::all_of(sv.begin(), sv.end(), [](const char& c) { return (c >= 'a' && c <= 'f') || std::isdigit(c); });
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool isIpValid(const std::string_view sv) {
|
|
||||||
return std::all_of(sv.begin(), sv.end(), [](const char& c) { return c == '.' || c == ':' || std::isdigit(c); });
|
|
||||||
}
|
|
||||||
|
|
||||||
CDatabase::CDatabase() {
|
CDatabase::CDatabase() {
|
||||||
if (std::filesystem::exists(dbPath())) {
|
if (std::filesystem::exists(dbPath())) {
|
||||||
|
if (std::filesystem::exists(dbDir() + "/schema")) {
|
||||||
|
int schema = std::stoi(readFileAsText(dbDir() + "/schema"));
|
||||||
|
if (schema == DB_SCHEMA_VERSION) {
|
||||||
if (sqlite3_open(dbPath().c_str(), &m_db) != SQLITE_OK)
|
if (sqlite3_open(dbPath().c_str(), &m_db) != SQLITE_OK)
|
||||||
throw std::runtime_error("failed to open sqlite3 db");
|
throw std::runtime_error("failed to open sqlite3 db");
|
||||||
|
|
||||||
cleanupDb();
|
cleanupDb();
|
||||||
return;
|
return;
|
||||||
}
|
} else
|
||||||
|
Debug::log(LOG, "Database outdated, recreating db");
|
||||||
|
} else
|
||||||
|
Debug::log(LOG, "Database schema not present, recreating db");
|
||||||
|
} else
|
||||||
Debug::log(LOG, "Database not present, creating one");
|
Debug::log(LOG, "Database not present, creating one");
|
||||||
|
|
||||||
|
std::filesystem::remove(dbPath());
|
||||||
|
|
||||||
if (sqlite3_open(dbPath().c_str(), &m_db) != SQLITE_OK)
|
if (sqlite3_open(dbPath().c_str(), &m_db) != SQLITE_OK)
|
||||||
throw std::runtime_error("failed to open sqlite3 db");
|
throw std::runtime_error("failed to open sqlite3 db");
|
||||||
|
|
||||||
|
std::ofstream of(dbDir() + "/schema", std::ios::trunc);
|
||||||
|
of << DB_SCHEMA_VERSION;
|
||||||
|
of.close();
|
||||||
|
|
||||||
// create db layout
|
// create db layout
|
||||||
char* errmsg = nullptr;
|
char* errmsg = nullptr;
|
||||||
|
|
||||||
const char* CHALLENGE_TABLE = R"#(
|
const char* CHALLENGE_TABLE = R"#(
|
||||||
CREATE TABLE challenges (
|
CREATE TABLE challenges (
|
||||||
nonce TEXT NOT NULL,
|
nonce TEXT NOT NULL,
|
||||||
ip TEXT NOT NULL,
|
fingerprint TEXT NOT NULL,
|
||||||
difficulty INTEGER NOT NULL,
|
difficulty INTEGER NOT NULL,
|
||||||
epoch INTEGER NOT NULL,
|
epoch INTEGER NOT NULL,
|
||||||
CONSTRAINT PK PRIMARY KEY (nonce)
|
CONSTRAINT PK PRIMARY KEY (nonce)
|
||||||
@@ -58,7 +81,7 @@ CREATE TABLE challenges (
|
|||||||
const char* TOKENS_TABLE = R"#(
|
const char* TOKENS_TABLE = R"#(
|
||||||
CREATE TABLE tokens (
|
CREATE TABLE tokens (
|
||||||
token TEXT NOT NULL,
|
token TEXT NOT NULL,
|
||||||
ip TEXT NOT NULL,
|
fingerprint TEXT NOT NULL,
|
||||||
epoch INTEGER NOT NULL,
|
epoch INTEGER NOT NULL,
|
||||||
CONSTRAINT PK PRIMARY KEY (token)
|
CONSTRAINT PK PRIMARY KEY (token)
|
||||||
);)#";
|
);)#";
|
||||||
@@ -75,14 +98,14 @@ void CDatabase::addChallenge(const SDatabaseChallengeEntry& entry) {
|
|||||||
if (!isHashValid(entry.nonce))
|
if (!isHashValid(entry.nonce))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!isIpValid(entry.ip))
|
if (!isHashValid(entry.fingerprint))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const std::string CMD = fmt::format(R"#(
|
const std::string CMD = fmt::format(R"#(
|
||||||
INSERT INTO challenges VALUES (
|
INSERT INTO challenges VALUES (
|
||||||
"{}", "{}", {}, {}
|
"{}", "{}", {}, {}
|
||||||
);)#",
|
);)#",
|
||||||
entry.nonce, entry.ip, entry.difficulty, entry.epoch);
|
entry.nonce, entry.fingerprint, entry.difficulty, entry.epoch);
|
||||||
|
|
||||||
char* errmsg = nullptr;
|
char* errmsg = nullptr;
|
||||||
sqlite3_exec(m_db, CMD.c_str(), nullptr, nullptr, &errmsg);
|
sqlite3_exec(m_db, CMD.c_str(), nullptr, nullptr, &errmsg);
|
||||||
@@ -120,7 +143,7 @@ SELECT * FROM challenges WHERE nonce = "{}";
|
|||||||
if (errmsg || result.result.size() < 4)
|
if (errmsg || result.result.size() < 4)
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|
||||||
return SDatabaseChallengeEntry{.nonce = nonce, .difficulty = std::stoi(result.result.at(2)), .epoch = std::stoull(result.result.at(3)), .ip = result.result.at(1)};
|
return SDatabaseChallengeEntry{.nonce = nonce, .difficulty = std::stoi(result.result.at(2)), .epoch = std::stoull(result.result.at(3)), .fingerprint = result.result.at(1)};
|
||||||
}
|
}
|
||||||
|
|
||||||
void CDatabase::dropChallenge(const std::string& nonce) {
|
void CDatabase::dropChallenge(const std::string& nonce) {
|
||||||
@@ -143,14 +166,14 @@ void CDatabase::addToken(const SDatabaseTokenEntry& entry) {
|
|||||||
if (!isHashValid(entry.token))
|
if (!isHashValid(entry.token))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!isIpValid(entry.ip))
|
if (!isHashValid(entry.fingerprint))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const std::string CMD = fmt::format(R"#(
|
const std::string CMD = fmt::format(R"#(
|
||||||
INSERT INTO tokens VALUES (
|
INSERT INTO tokens VALUES (
|
||||||
"{}", "{}", {}
|
"{}", "{}", {}
|
||||||
);)#",
|
);)#",
|
||||||
entry.token, entry.ip, entry.epoch);
|
entry.token, entry.fingerprint, entry.epoch);
|
||||||
|
|
||||||
char* errmsg = nullptr;
|
char* errmsg = nullptr;
|
||||||
sqlite3_exec(m_db, CMD.c_str(), nullptr, nullptr, &errmsg);
|
sqlite3_exec(m_db, CMD.c_str(), nullptr, nullptr, &errmsg);
|
||||||
@@ -207,7 +230,7 @@ SELECT * FROM tokens WHERE token = "{}";
|
|||||||
if (errmsg || result.result.size() < 3)
|
if (errmsg || result.result.size() < 3)
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|
||||||
return SDatabaseTokenEntry{.token = token, .epoch = std::stoull(result.result.at(2)), .ip = result.result.at(1)};
|
return SDatabaseTokenEntry{.token = token, .epoch = std::stoull(result.result.at(2)), .fingerprint = result.result.at(1)};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CDatabase::shouldCleanupDb() {
|
bool CDatabase::shouldCleanupDb() {
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ struct SDatabaseChallengeEntry {
|
|||||||
std::string nonce = "";
|
std::string nonce = "";
|
||||||
int difficulty = 0;
|
int difficulty = 0;
|
||||||
unsigned long int epoch = std::chrono::system_clock::now().time_since_epoch() / std::chrono::seconds(1);
|
unsigned long int epoch = std::chrono::system_clock::now().time_since_epoch() / std::chrono::seconds(1);
|
||||||
std::string ip = "";
|
std::string fingerprint = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SDatabaseTokenEntry {
|
struct SDatabaseTokenEntry {
|
||||||
std::string token = "";
|
std::string token = "";
|
||||||
unsigned long int epoch = std::chrono::system_clock::now().time_since_epoch() / std::chrono::seconds(1);
|
unsigned long int epoch = std::chrono::system_clock::now().time_since_epoch() / std::chrono::seconds(1);
|
||||||
std::string ip = "";
|
std::string fingerprint = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
class CDatabase {
|
class CDatabase {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "../headers/cfHeader.hpp"
|
#include "../headers/cfHeader.hpp"
|
||||||
#include "../headers/xforwardfor.hpp"
|
#include "../headers/xforwardfor.hpp"
|
||||||
#include "../headers/gitProtocolHeader.hpp"
|
#include "../headers/gitProtocolHeader.hpp"
|
||||||
|
#include "../headers/acceptLanguageHeader.hpp"
|
||||||
#include "../debug/log.hpp"
|
#include "../debug/log.hpp"
|
||||||
#include "../GlobalState.hpp"
|
#include "../GlobalState.hpp"
|
||||||
#include "../config/Config.hpp"
|
#include "../config/Config.hpp"
|
||||||
@@ -96,6 +97,54 @@ void CServerHandler::finish() {
|
|||||||
m_client = nullptr;
|
m_client = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string CServerHandler::fingerprintForRequest(const Pistache::Http::Request& req) {
|
||||||
|
const auto HEADERS = req.headers();
|
||||||
|
std::shared_ptr<const Pistache::Http::Header::AcceptEncoding> acceptEncodingHeader;
|
||||||
|
std::shared_ptr<const Pistache::Http::Header::UserAgent> userAgentHeader;
|
||||||
|
std::shared_ptr<const CFConnectingIPHeader> cfHeader;
|
||||||
|
std::shared_ptr<const AcceptLanguageHeader> languageHeader;
|
||||||
|
|
||||||
|
std::string input = "checkpoint-";
|
||||||
|
|
||||||
|
try {
|
||||||
|
cfHeader = Pistache::Http::Header::header_cast<CFConnectingIPHeader>(HEADERS.get("cf-connecting-ip"));
|
||||||
|
} catch (std::exception& e) {
|
||||||
|
; // silent ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
acceptEncodingHeader = Pistache::Http::Header::header_cast<Pistache::Http::Header::AcceptEncoding>(HEADERS.get("Accept-Encoding"));
|
||||||
|
} catch (std::exception& e) {
|
||||||
|
; // silent ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
languageHeader = Pistache::Http::Header::header_cast<AcceptLanguageHeader>(HEADERS.get("Accept-Language"));
|
||||||
|
} catch (std::exception& e) {
|
||||||
|
; // silent ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
userAgentHeader = Pistache::Http::Header::header_cast<Pistache::Http::Header::UserAgent>(HEADERS.get("User-Agent"));
|
||||||
|
} catch (std::exception& e) {
|
||||||
|
; // silent ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfHeader)
|
||||||
|
input += cfHeader->ip();
|
||||||
|
// TODO: those seem to change. Find better things to hash.
|
||||||
|
// if (acceptEncodingHeader)
|
||||||
|
// input += HEADERS.getRaw("Accept-Encoding").value();
|
||||||
|
// if (languageHeader)
|
||||||
|
// input += languageHeader->language();
|
||||||
|
if (userAgentHeader)
|
||||||
|
input += userAgentHeader->agent();
|
||||||
|
|
||||||
|
input += req.address().host();
|
||||||
|
|
||||||
|
return sha256(input);
|
||||||
|
}
|
||||||
|
|
||||||
void CServerHandler::onRequest(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter response) {
|
void CServerHandler::onRequest(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter response) {
|
||||||
const auto HEADERS = req.headers();
|
const auto HEADERS = req.headers();
|
||||||
std::shared_ptr<const Pistache::Http::Header::Host> hostHeader;
|
std::shared_ptr<const Pistache::Http::Header::Host> hostHeader;
|
||||||
@@ -203,16 +252,22 @@ void CServerHandler::onRequest(const Pistache::Http::Request& req, Pistache::Htt
|
|||||||
const auto TOKEN = g_pDB->getToken(req.cookies().get("CheckpointToken").value);
|
const auto TOKEN = g_pDB->getToken(req.cookies().get("CheckpointToken").value);
|
||||||
if (TOKEN) {
|
if (TOKEN) {
|
||||||
const auto AGE = std::chrono::milliseconds(std::time(nullptr)).count() - TOKEN->epoch;
|
const auto AGE = std::chrono::milliseconds(std::time(nullptr)).count() - TOKEN->epoch;
|
||||||
if (AGE <= TOKEN_MAX_AGE_MS && TOKEN->ip == (cfHeader ? cfHeader->ip() : req.address().host())) {
|
if (AGE <= TOKEN_MAX_AGE_MS && TOKEN->fingerprint == fingerprintForRequest(req)) {
|
||||||
Debug::log(LOG, " | Action: PASS (token)");
|
Debug::log(LOG, " | Action: PASS (token)");
|
||||||
proxyPass(req, response);
|
proxyPass(req, response);
|
||||||
return;
|
return;
|
||||||
} else // token has been used from a different IP or is expired. Nuke it.
|
} else { // token has been used from a different IP or is expired. Nuke it.
|
||||||
g_pDB->dropToken(TOKEN->token);
|
g_pDB->dropToken(TOKEN->token);
|
||||||
|
if (AGE > TOKEN_MAX_AGE_MS)
|
||||||
|
Debug::log(LOG, " | Action: CHALLENGE (token expired)");
|
||||||
|
else
|
||||||
|
Debug::log(LOG, " | Action: CHALLENGE (token fingerprint mismatch)");
|
||||||
}
|
}
|
||||||
}
|
} else
|
||||||
|
Debug::log(LOG, " | Action: CHALLENGE (token not found in db)");
|
||||||
|
} else
|
||||||
|
Debug::log(LOG, " | Action: CHALLENGE (no token)");
|
||||||
|
|
||||||
Debug::log(LOG, " | Action: CHALLENGE");
|
|
||||||
serveStop(req, response);
|
serveStop(req, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,6 +277,7 @@ void CServerHandler::onTimeout(const Pistache::Http::Request& request, Pistache:
|
|||||||
|
|
||||||
void CServerHandler::challengeSubmitted(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response) {
|
void CServerHandler::challengeSubmitted(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response) {
|
||||||
const auto JSON = req.body();
|
const auto JSON = req.body();
|
||||||
|
const auto FINGERPRINT = fingerprintForRequest(req);
|
||||||
|
|
||||||
std::shared_ptr<const CFConnectingIPHeader> cfHeader;
|
std::shared_ptr<const CFConnectingIPHeader> cfHeader;
|
||||||
try {
|
try {
|
||||||
@@ -249,7 +305,7 @@ void CServerHandler::challengeSubmitted(const Pistache::Http::Request& req, Pist
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CHALLENGE->ip != req.address().host()) {
|
if (CHALLENGE->fingerprint != FINGERPRINT) {
|
||||||
resp.error = "bad challenge";
|
resp.error = "bad challenge";
|
||||||
response.send(Pistache::Http::Code::Bad_Request, glz::write_json(resp).value());
|
response.send(Pistache::Http::Code::Bad_Request, glz::write_json(resp).value());
|
||||||
return;
|
return;
|
||||||
@@ -273,7 +329,7 @@ void CServerHandler::challengeSubmitted(const Pistache::Http::Request& req, Pist
|
|||||||
|
|
||||||
const auto TOKEN = generateToken();
|
const auto TOKEN = generateToken();
|
||||||
|
|
||||||
g_pDB->addToken(SDatabaseTokenEntry{.token = TOKEN, .ip = (cfHeader ? cfHeader->ip() : req.address().host())});
|
g_pDB->addToken(SDatabaseTokenEntry{.token = TOKEN, .fingerprint = FINGERPRINT});
|
||||||
|
|
||||||
resp.success = true;
|
resp.success = true;
|
||||||
resp.token = TOKEN;
|
resp.token = TOKEN;
|
||||||
@@ -290,7 +346,7 @@ void CServerHandler::serveStop(const Pistache::Http::Request& req, Pistache::Htt
|
|||||||
const auto NONCE = generateNonce();
|
const auto NONCE = generateNonce();
|
||||||
const auto DIFFICULTY = 4;
|
const auto DIFFICULTY = 4;
|
||||||
|
|
||||||
g_pDB->addChallenge(SDatabaseChallengeEntry{.nonce = NONCE, .difficulty = DIFFICULTY, .ip = req.address().host()});
|
g_pDB->addChallenge(SDatabaseChallengeEntry{.nonce = NONCE, .difficulty = DIFFICULTY, .fingerprint = fingerprintForRequest(req)});
|
||||||
|
|
||||||
page.add("challengeDifficulty", CTinylatesProp(std::to_string(DIFFICULTY)));
|
page.add("challengeDifficulty", CTinylatesProp(std::to_string(DIFFICULTY)));
|
||||||
page.add("challengeNonce", CTinylatesProp(NONCE));
|
page.add("challengeNonce", CTinylatesProp(NONCE));
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class CServerHandler : public Pistache::Http::Handler {
|
|||||||
void serveStop(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response);
|
void serveStop(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response);
|
||||||
void proxyPass(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response);
|
void proxyPass(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response);
|
||||||
void challengeSubmitted(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response);
|
void challengeSubmitted(const Pistache::Http::Request& req, Pistache::Http::ResponseWriter& response);
|
||||||
|
std::string fingerprintForRequest(const Pistache::Http::Request& req);
|
||||||
|
|
||||||
struct SChallengeResponse {
|
struct SChallengeResponse {
|
||||||
std::string challenge;
|
std::string challenge;
|
||||||
|
|||||||
24
src/headers/acceptLanguageHeader.hpp
Normal file
24
src/headers/acceptLanguageHeader.hpp
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#include <pistache/http_headers.h>
|
||||||
|
#include <pistache/net.h>
|
||||||
|
|
||||||
|
class AcceptLanguageHeader : public Pistache::Http::Header::Header {
|
||||||
|
public:
|
||||||
|
NAME("Accept-Language");
|
||||||
|
|
||||||
|
AcceptLanguageHeader() = default;
|
||||||
|
|
||||||
|
void parse(const std::string& str) override {
|
||||||
|
m_language = str;
|
||||||
|
}
|
||||||
|
|
||||||
|
void write(std::ostream& os) const override {
|
||||||
|
os << m_language;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string language() const {
|
||||||
|
return m_language;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string m_language = "";
|
||||||
|
};
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "headers/xforwardfor.hpp"
|
#include "headers/xforwardfor.hpp"
|
||||||
#include "headers/cfHeader.hpp"
|
#include "headers/cfHeader.hpp"
|
||||||
#include "headers/gitProtocolHeader.hpp"
|
#include "headers/gitProtocolHeader.hpp"
|
||||||
|
#include "headers/acceptLanguageHeader.hpp"
|
||||||
|
|
||||||
#include "debug/log.hpp"
|
#include "debug/log.hpp"
|
||||||
|
|
||||||
@@ -74,6 +75,7 @@ int main(int argc, char** argv, char** envp) {
|
|||||||
Pistache::Http::Header::Registry::instance().registerHeader<CFConnectingIPHeader>();
|
Pistache::Http::Header::Registry::instance().registerHeader<CFConnectingIPHeader>();
|
||||||
Pistache::Http::Header::Registry::instance().registerHeader<XForwardedForHeader>();
|
Pistache::Http::Header::Registry::instance().registerHeader<XForwardedForHeader>();
|
||||||
Pistache::Http::Header::Registry::instance().registerHeader<GitProtocolHeader>();
|
Pistache::Http::Header::Registry::instance().registerHeader<GitProtocolHeader>();
|
||||||
|
Pistache::Http::Header::Registry::instance().registerHeader<AcceptLanguageHeader>();
|
||||||
|
|
||||||
g_pDB = std::make_unique<CDatabase>();
|
g_pDB = std::make_unique<CDatabase>();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user