Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ option(HTTP "Enable HTTP support" ON)
if (HTTP)
list(APPEND VCPKG_MANIFEST_FEATURES "http")
set(BOOST_REQUIRED_VERSION 1.75.0)
add_compile_definitions(HTTP)
endif()

option(BUILD_TESTING "Build unit tests" OFF)
Expand Down
1 change: 1 addition & 0 deletions config.lua.dist
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ gameProtocolPort = 7172
statusProtocolPort = 7171
httpPort = 8080
httpWorkers = 1
sessionSecret = "c2VjcmV0" -- CHANGE THIS VALUE!
maxPlayers = 0
onePlayerOnlinePerAccount = true
allowClones = false
Expand Down
89 changes: 71 additions & 18 deletions src/base64.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,87 @@

#include "base64.h"

#include <openssl/evp.h>
static constexpr std::string_view alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";

std::string tfs::base64::encode(std::string_view input)
{
std::unique_ptr<BIO, decltype(&BIO_free_all)> b64(BIO_new(BIO_f_base64()), BIO_free_all);
BIO_set_flags(b64.get(), BIO_FLAGS_BASE64_NO_NL);
std::string output;
output.reserve((input.size() * 4 + 2) / 3); // Estimated size

BIO* sink = BIO_new(BIO_s_mem());
BIO_push(b64.get(), sink);
BIO_write(b64.get(), input.data(), input.size());
BIO_flush(b64.get());
size_t i = 0;
while (i + 3 <= input.size()) {
uint32_t chunk = (static_cast<uint8_t>(input[i]) << 16) | (static_cast<uint8_t>(input[i + 1]) << 8) |
static_cast<uint8_t>(input[i + 2]);

const char* encoded;
auto len = BIO_get_mem_data(sink, &encoded);
return {encoded, static_cast<size_t>(len)};
output.push_back(alphabet[(chunk >> 18) & 0x3F]);
output.push_back(alphabet[(chunk >> 12) & 0x3F]);
output.push_back(alphabet[(chunk >> 6) & 0x3F]);
output.push_back(alphabet[chunk & 0x3F]);
i += 3;
}

if (i < input.size()) {
uint32_t chunk = static_cast<uint8_t>(input[i]) << 16;
if (i + 1 < input.size()) {
chunk |= static_cast<uint8_t>(input[i + 1]) << 8;
}

output.push_back(alphabet[(chunk >> 18) & 0x3F]);
output.push_back(alphabet[(chunk >> 12) & 0x3F]);
if (i + 1 < input.size()) {
output.push_back(alphabet[(chunk >> 6) & 0x3F]);
}
}

return output;
}

std::string tfs::base64::decode(std::string_view input)
Comment thread
ranisalt marked this conversation as resolved.
{
std::unique_ptr<BIO, decltype(&BIO_free_all)> b64(BIO_new(BIO_f_base64()), BIO_free_all);
BIO_set_flags(b64.get(), BIO_FLAGS_BASE64_NO_NL);
static constexpr auto reverse_table = [] {
std::array<uint32_t, 256> table{};
table.fill(-1);
for (size_t i = 0; i < alphabet.size(); ++i) {
table[static_cast<uint8_t>(alphabet[i])] = static_cast<uint8_t>(i);
}
return table;
}();

size_t len = input.size();
assert(len % 4 != 1);

std::string output;
output.reserve((len * 3) / 4); // Estimated

size_t i = 0;
while (i + 4 <= len) {
uint32_t chunk = (reverse_table[static_cast<uint8_t>(input[i])] << 18) |
(reverse_table[static_cast<uint8_t>(input[i + 1])] << 12) |
(reverse_table[static_cast<uint8_t>(input[i + 2])] << 6) |
reverse_table[static_cast<uint8_t>(input[i + 3])];

output.push_back((chunk >> 16) & 0xFF);
output.push_back((chunk >> 8) & 0xFF);
output.push_back(chunk & 0xFF);
i += 4;
}

if (i < len) {
uint32_t chunk = 0;
int padding = 0;

chunk |= reverse_table[static_cast<uint8_t>(input[i])] << 18;
chunk |= reverse_table[static_cast<uint8_t>(input[i + 1])] << 12;

BIO* source = BIO_new_mem_buf(input.data(), input.size()); // read-only source
BIO_push(b64.get(), source);
if (i + 2 < len) {
chunk |= reverse_table[static_cast<uint8_t>(input[i + 2])] << 6;
} else {
++padding;
}

size_t decodedLength = 3 * input.size() / 4;
auto decoded = std::string(decodedLength, '\0');
output.push_back((chunk >> 16) & 0xFF);
if (padding < 1) output.push_back((chunk >> 8) & 0xFF);
}

const int len = BIO_read(b64.get(), decoded.data(), decodedLength);
return decoded.substr(0, len);
return output;
}
2 changes: 2 additions & 0 deletions src/configmanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include "configmanager.h"

#include "base64.h"
#include "game.h"
#include "monster.h"
#include "pugicast.h"
Expand Down Expand Up @@ -253,6 +254,7 @@ bool ConfigManager::load()
string[URL] = getGlobalString(L, "url", "");
string[LOCATION] = getGlobalString(L, "location", "");
string[WORLD_TYPE] = getGlobalString(L, "worldType", "pvp");
string[SESSION_SECRET] = tfs::base64::decode(getGlobalString(L, "sessionSecret", ""));

integer[MAX_PLAYERS] = getGlobalNumber(L, "maxPlayers");
integer[PZ_LOCKED] = getGlobalNumber(L, "pzLocked", 60000);
Expand Down
1 change: 1 addition & 0 deletions src/configmanager.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ enum string_config_t
LOCATION,
IP,
WORLD_TYPE,
SESSION_SECRET,
MYSQL_HOST,
MYSQL_USER,
MYSQL_PASS,
Expand Down
2 changes: 1 addition & 1 deletion src/http/cacheinfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace beast = boost::beast;
namespace json = boost::json;
using boost::beast::http::status;

std::pair<status, json::value> tfs::http::handle_cacheinfo(const json::object&, std::string_view)
std::pair<status, json::value> tfs::http::handle_cacheinfo(const json::object&)
{
thread_local auto& db = Database::getInstance();
auto result = db.storeQuery("SELECT COUNT(*) AS `count` FROM `players_online`");
Expand Down
3 changes: 1 addition & 2 deletions src/http/cacheinfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

namespace tfs::http {

std::pair<boost::beast::http::status, boost::json::value> handle_cacheinfo(const boost::json::object& body,
std::string_view ip);
std::pair<boost::beast::http::status, boost::json::value> handle_cacheinfo(const boost::json::object& body);

}
31 changes: 19 additions & 12 deletions src/http/login.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
#include "../game.h"
#include "error.h"

#include <fmt/format.h>

extern Game g_game;
extern Vocations g_vocations;

Expand All @@ -17,6 +15,18 @@ using boost::beast::http::status;

namespace {

[[noreturn]] inline void unreachable()
{
// Uses compiler specific extensions if possible.
// Even if no extension is used, undefined behavior is still raised by
// an empty function body and the noreturn attribute.
#if defined(_MSC_VER) && !defined(__clang__) // MSVC
__assume(false);
#else // GCC, Clang
__builtin_unreachable();
#endif
}

int getPvpType()
{
switch (g_game.getWorldType()) {
Expand All @@ -28,12 +38,12 @@ int getPvpType()
return 2;
}

tfs::unreachable();
unreachable();
}

} // namespace

std::pair<status, json::value> tfs::http::handle_login(const json::object& body, std::string_view ip)
std::pair<status, json::value> tfs::http::handle_login(const json::object& body)
{
using namespace std::chrono;

Expand All @@ -50,7 +60,7 @@ std::pair<status, json::value> tfs::http::handle_login(const json::object& body,
}

thread_local auto& db = Database::getInstance();
auto result = db.storeQuery(fmt::format(
auto result = db.storeQuery(std::format(
"SELECT `id`, UNHEX(`password`) AS `password`, `secret`, `premium_ends_at` FROM `accounts` WHERE `email` = {:s}",
db.escapeString(emailField->get_string())));
if (!result) {
Expand Down Expand Up @@ -84,14 +94,11 @@ std::pair<status, json::value> tfs::http::handle_login(const json::object& body,
auto accountId = result->getNumber<uint64_t>("id");
auto premiumEndsAt = result->getNumber<int64_t>("premium_ends_at");

std::string sessionKey = randomBytes(16);
if (!db.executeQuery(
fmt::format("INSERT INTO `sessions` (`token`, `account_id`, `ip`) VALUES ({:s}, {:d}, INET6_ATON({:s}))",
db.escapeString(sessionKey), accountId, db.escapeString(ip)))) {
return make_error_response();
}
const auto& payload = std::format("{:s}{:s}", tfs::to_bytes(accountId), tfs::to_bytes(now));
const auto& signature = hmac("SHA256", getString(ConfigManager::SESSION_SECRET), payload);
const auto& sessionKey = std::format("{:s}{:s}", payload, signature);

result = db.storeQuery(fmt::format(
result = db.storeQuery(std::format(
"SELECT `id`, `name`, `level`, `vocation`, `lastlogin`, `sex`, `looktype`, `lookhead`, `lookbody`, `looklegs`, `lookfeet`, `lookaddons` FROM `players` WHERE `account_id` = {:d}",
accountId));

Expand Down
3 changes: 1 addition & 2 deletions src/http/login.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

namespace tfs::http {

std::pair<boost::beast::http::status, boost::json::value> handle_login(const boost::json::object& body,
std::string_view ip);
std::pair<boost::beast::http::status, boost::json::value> handle_login(const boost::json::object& body);

}
8 changes: 4 additions & 4 deletions src/http/router.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ namespace json = boost::json;

namespace {

auto router(std::string_view type, const json::object& body, std::string_view ip)
auto router(std::string_view type, const json::object& body)
{
using namespace tfs::http;

if (type == "cacheinfo") {
return handle_cacheinfo(body, ip);
return handle_cacheinfo(body);
}
if (type == "login") {
return handle_login(body, ip);
return handle_login(body);
}

return make_error_response();
Expand All @@ -47,7 +47,7 @@ beast::http::message_generator tfs::http::handle_request(const beast::http::requ
return make_error_response({.code = 2, .message = "Invalid request type."});
}

return router(typeField->get_string(), requestBodyObj, ip);
return router(typeField->get_string(), requestBodyObj);
}();

beast::http::response<beast::http::string_body> res{status, req.version()};
Expand Down
2 changes: 1 addition & 1 deletion src/http/tests/test_cacheinfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ BOOST_FIXTURE_TEST_CASE(test_login_success_with_token, CacheInfoFixture)
BOOST_TEST(db.executeQuery(fmt::format(
"INSERT INTO `players_online` (`player_id`) SELECT `id` FROM `players` WHERE `account_id` = {:d}", id)));

auto&& [status, body] = tfs::http::handle_cacheinfo({{"type", "cacheinfo"}}, ip);
auto&& [status, body] = tfs::http::handle_cacheinfo({{"type", "cacheinfo"}});

BOOST_TEST(status == status::ok);
BOOST_TEST(body.at("playersonline").as_uint64() == 3);
Expand Down
42 changes: 19 additions & 23 deletions src/http/tests/test_login.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ using status = boost::beast::http::status;

BOOST_FIXTURE_TEST_CASE(test_login_missing_email, LoginFixture)
{
auto&& [status, body] = tfs::http::handle_login({{"type", "login"}, {"password", "bar"}}, ip);
auto&& [status, body] = tfs::http::handle_login({{"type", "login"}, {"password", "bar"}});

BOOST_TEST(status == status::ok);
BOOST_TEST(body.at("errorCode").as_int64() == 3);
Expand All @@ -160,15 +160,15 @@ BOOST_FIXTURE_TEST_CASE(test_login_missing_email, LoginFixture)
BOOST_FIXTURE_TEST_CASE(test_login_account_does_not_exist, LoginFixture)
{
auto&& [status, body] =
tfs::http::handle_login({{"type", "login"}, {"email", "k@example.com"}, {"password", "bar"}}, ip);
tfs::http::handle_login({{"type", "login"}, {"email", "k@example.com"}, {"password", "bar"}});

BOOST_TEST(status == status::ok);
BOOST_TEST(body.at("errorCode").as_int64() == 3);
}

BOOST_FIXTURE_TEST_CASE(test_login_missing_password, LoginFixture)
{
auto&& [status, body] = tfs::http::handle_login({{"type", "login"}, {"email", "foo@example.com"}}, ip);
auto&& [status, body] = tfs::http::handle_login({{"type", "login"}, {"email", "foo@example.com"}});

BOOST_TEST(status == status::ok);
BOOST_TEST(body.at("errorCode").as_int64() == 3);
Expand All @@ -180,7 +180,7 @@ BOOST_FIXTURE_TEST_CASE(test_login_invalid_password, LoginFixture)
"INSERT INTO `accounts` (`name`, `email`, `password`) VALUES ('abc', 'foo@example.com', SHA1('bar'))"));

auto&& [status, body] =
tfs::http::handle_login({{"type", "login"}, {"email", "foo@example.com"}, {"password", "baz"}}, ip);
tfs::http::handle_login({{"type", "login"}, {"email", "foo@example.com"}, {"password", "baz"}});

BOOST_TEST(status == status::ok);
BOOST_TEST(body.at("errorCode").as_int64() == 3);
Expand All @@ -193,13 +193,11 @@ BOOST_FIXTURE_TEST_CASE(test_login_missing_token, LoginFixture)

auto now = duration_cast<seconds>(system_clock::now().time_since_epoch());

auto&& [status, body] = tfs::http::handle_login(
{
{"type", "login"},
{"email", "fooba@example.com"},
{"password", "bar"},
},
ip);
auto&& [status, body] = tfs::http::handle_login({
{"type", "login"},
{"email", "fooba@example.com"},
{"password", "bar"},
});

BOOST_TEST(status == status::ok);
BOOST_TEST(body.at("errorCode").as_int64() == 6);
Expand All @@ -211,7 +209,7 @@ BOOST_FIXTURE_TEST_CASE(test_login_success_no_players, LoginFixture)
"INSERT INTO `accounts` (`name`, `email`, `password`) VALUES ('defg', 'foobar@example.com', SHA1('bar'))"));

auto&& [status, body] =
tfs::http::handle_login({{"type", "login"}, {"email", "foobar@example.com"}, {"password", "bar"}}, ip);
tfs::http::handle_login({{"type", "login"}, {"email", "foobar@example.com"}, {"password", "bar"}});

BOOST_TEST(status == status::ok);
auto& characters = body.at("playdata").at("characters").as_array();
Expand All @@ -229,12 +227,12 @@ BOOST_FIXTURE_TEST_CASE(test_login_success, LoginFixture)

DBInsert insert(
"INSERT INTO `players` (`account_id`, `name`, `level`, `vocation`, `lastlogin`, `sex`, `looktype`, `lookhead`, `lookbody`, `looklegs`, `lookfeet`, `lookaddons`) VALUES");
insert.addRow(fmt::format("{:d}, \"{:s}\", {:d}, {:d}, {:d}, {:d}, {:d}, {:d}, {:d}, {:d}, {:d}, {:d}", id,
"Test", 2597, 6, 1715719401, 1, 1094, 78, 132, 114, 0, 1));
insert.addRow(fmt::format("{:d}, \"{:s}\", {:d}, {:d}, {:d}, {:d}, {:d}, {:d}, {:d}, {:d}, {:d}, {:d}", id, "Test",
2597, 6, 1715719401, 1, 1094, 78, 132, 114, 0, 1));
BOOST_TEST(insert.execute());

auto&& [status, body] =
tfs::http::handle_login({{"type", "login"}, {"email", "ghij@example.com"}, {"password", "bar"}}, ip);
tfs::http::handle_login({{"type", "login"}, {"email", "ghij@example.com"}, {"password", "bar"}});

BOOST_TEST(status == status::ok);

Expand Down Expand Up @@ -285,14 +283,12 @@ BOOST_FIXTURE_TEST_CASE(test_login_success_with_token, LoginFixture)
insert.addRow(fmt::format("{:d}, \"{:s}\", {:d}, {:d}, {:d}", id, "Testtoken", 2597, 6, 1715719401));
BOOST_TEST(insert.execute());

auto&& [status, body] = tfs::http::handle_login(
{
{"type", "login"},
{"email", "nbdj@example.com"},
{"password", "bar"},
{"token", generateToken("", now.count() / AUTHENTICATOR_PERIOD)},
},
ip);
auto&& [status, body] = tfs::http::handle_login({
{"type", "login"},
{"email", "nbdj@example.com"},
{"password", "bar"},
{"token", generateToken("", now.count() / AUTHENTICATOR_PERIOD)},
});

BOOST_TEST(status == status::ok);
}
Loading