allow multiple licenses per account

This commit is contained in:
Martin Michelsen
2024-04-12 18:35:48 -07:00
parent 40d5c6ee64
commit 34751f99e9
35 changed files with 2351 additions and 1723 deletions
+305 -167
View File
@@ -178,10 +178,10 @@ CommandDefinition c_on(
Run a command on a specific game server client or proxy server session.\n\
Without this prefix, commands that affect a single client or session will\n\
work only if there's exactly one connected client or open session. SESSION\n\
may be a client ID (e.g. C-3), a player name, a license serial number, or\n\
a BB account username. For proxy commands, SESSION should be the session\n\
ID, which generally is the same as the player\'s serial number and appears\n\
after \"LinkedSession:\" in the log output.",
may be a client ID (e.g. C-3), a player name, an account ID, an Xbox\n\
gamertag, or a BB account username. For proxy commands, SESSION should be\n\
the session ID, which generally is the same as the player\'s account ID\n\
and appears after \"LinkedSession:\" in the log output.",
false,
+[](CommandArgs& args) {
size_t session_name_end = skip_non_whitespace(args.args, 0);
@@ -197,6 +197,7 @@ CommandDefinition c_on(
CommandDefinition c_reload(
"reload", "reload ITEM [ITEM...]\n\
Reload various parts of the server configuration. The items are:\n\
accounts - reindex user accounts\n\
battle-params - reload the BB enemy stats files\n\
bb-keys - reload BB private keys\n\
config - reload most fields from config.json\n\
@@ -209,7 +210,6 @@ CommandDefinition c_reload(
item-definitions - reload item definitions files\n\
item-name-index - regenerate item name list\n\
level-table - reload the level-up tables\n\
licenses - reindex user licenses\n\
patch-files - reindex the PC and BB patch directories\n\
quests - reindex all quests (including Episode3 download quests)\n\
set-tables - reload set data tables\n\
@@ -228,8 +228,8 @@ CommandDefinition c_reload(
for (const auto& type : types) {
if (type == "bb-keys") {
args.s->load_bb_private_keys(true);
} else if (type == "licenses") {
args.s->load_licenses(true);
} else if (type == "accounts") {
args.s->load_accounts(true);
} else if (type == "patch-files") {
args.s->load_patch_indexes(true);
} else if (type == "ep3-cards") {
@@ -278,188 +278,326 @@ CommandDefinition c_reload(
}
});
CommandDefinition c_add_license(
"add-license", "add-license PARAMETERS...\n\
Add a license to the server. <parameters> is some subset of the following:\n\
bb-username=<username> (BB username)\n\
bb-password=<password> (BB password)\n\
xb-gamertag=<gamertag> (Xbox gamertag)\n\
xb-user-id=<user-id> (Xbox user ID)\n\
xb-account-id=<account-id> (Xbox account ID)\n\
gc-password=<password> (GC password)\n\
dc-nte-serial-number=<serial-number> (DC NTE serial number)\n\
dc-nte-access-key=<access-key> (DC NTE access key)\n\
access-key=<access-key> (DC/GC/PC access key)\n\
serial=<serial-number> (decimal serial number; required for all licenses)\n\
flags=<privilege-mask> (see below)\n\
If flags is specified in hex, the meanings of bits are:\n\
00000001 = Can kick other users offline\n\
00000002 = Can ban other users\n\
00000004 = Can silence other users\n\
00000010 = Can change lobby events\n\
00000020 = Can make server-wide announcements\n\
00000040 = Ignores game join restrictions (e.g. level/quest requirements)\n\
01000000 = Can use debugging commands\n\
02000000 = Can use cheat commands even if cheat mode is disabled\n\
04000000 = Can play any quest without progression/flags restrictions\n\
08000000 = Can use chat commands even if disabled in config.json\n\
80000000 = License is a shared serial (disables Access Key and password\n\
checks; players will get Guild Cards based on their player names)\n\
There are also shorthands for some general privilege levels:\n\
flags=moderator = 00000007\n\
flags=admin = 000000FF\n\
flags=root = 7FFFFFFF",
CommandDefinition c_list_accounts(
"list-accounts", "list-accounts\n\
List all accounts registered on the server.",
true,
+[](CommandArgs& args) {
auto l = args.s->license_index->create_license();
for (const string& token : split(args.args, ' ')) {
if (starts_with(token, "bb-username=")) {
l->bb_username = token.substr(12);
} else if (starts_with(token, "bb-password=")) {
l->bb_password = token.substr(12);
} else if (starts_with(token, "xb-gamertag=")) {
l->xb_gamertag = token.substr(12);
} else if (starts_with(token, "xb-user-id=")) {
l->xb_user_id = stoull(token.substr(11), nullptr, 16);
} else if (starts_with(token, "xb-account-id=")) {
l->xb_account_id = stoull(token.substr(14), nullptr, 16);
} else if (starts_with(token, "gc-password=")) {
l->gc_password = token.substr(12);
} else if (starts_with(token, "dc-nte-serial-number=")) {
l->dc_nte_serial_number = token.substr(21);
} else if (starts_with(token, "dc-nte-access-key=")) {
l->dc_nte_access_key = token.substr(18);
} else if (starts_with(token, "access-key=")) {
l->access_key = token.substr(11);
} else if (starts_with(token, "serial=")) {
l->serial_number = stoul(token.substr(7), nullptr, 0);
} else if (starts_with(token, "flags=")) {
string mask = token.substr(6);
if (mask == "normal") {
l->flags = 0;
} else if (mask == "mod") {
l->replace_all_flags(License::Flag::MODERATOR);
} else if (mask == "admin") {
l->replace_all_flags(License::Flag::ADMINISTRATOR);
} else if (mask == "root") {
l->replace_all_flags(License::Flag::ROOT);
} else {
l->flags = stoul(mask, nullptr, 16);
}
} else {
throw invalid_argument("incorrect field: " + token);
auto accounts = args.s->account_index->all();
if (accounts.empty()) {
fprintf(stderr, "No accounts registered\n");
} else {
for (const auto& a : accounts) {
a->print(stderr);
}
}
if (!l->serial_number) {
throw invalid_argument("license does not contain serial number");
}
l->save();
args.s->license_index->add(l);
fprintf(stderr, "License added\n");
});
CommandDefinition c_update_license(
"update-license", "update-license SERIAL-NUMBER PARAMETERS...\n\
Update an existing license. <serial-number> specifies which license to\n\
update. The options in <parameters> are the same as for the add-license\n\
command.",
uint32_t parse_account_flags(const string& flags_str) {
try {
size_t end_pos = 0;
uint32_t ret = stoul(flags_str, &end_pos, 16);
if (end_pos == flags_str.size()) {
return ret;
}
} catch (const exception&) {
}
uint32_t ret = 0;
auto tokens = split(flags_str, ',');
for (const auto& token : tokens) {
string token_upper = toupper(token);
if (token_upper == "NONE") {
// Nothing to do
} else if (token_upper == "KICK_USER") {
ret |= static_cast<uint32_t>(Account::Flag::KICK_USER);
} else if (token_upper == "BAN_USER") {
ret |= static_cast<uint32_t>(Account::Flag::BAN_USER);
} else if (token_upper == "SILENCE_USER") {
ret |= static_cast<uint32_t>(Account::Flag::SILENCE_USER);
} else if (token_upper == "MODERATOR") {
ret |= static_cast<uint32_t>(Account::Flag::MODERATOR);
} else if (token_upper == "CHANGE_EVENT") {
ret |= static_cast<uint32_t>(Account::Flag::CHANGE_EVENT);
} else if (token_upper == "ANNOUNCE") {
ret |= static_cast<uint32_t>(Account::Flag::ANNOUNCE);
} else if (token_upper == "FREE_JOIN_GAMES") {
ret |= static_cast<uint32_t>(Account::Flag::FREE_JOIN_GAMES);
} else if (token_upper == "ADMINISTRATOR") {
ret |= static_cast<uint32_t>(Account::Flag::ADMINISTRATOR);
} else if (token_upper == "DEBUG") {
ret |= static_cast<uint32_t>(Account::Flag::DEBUG);
} else if (token_upper == "CHEAT_ANYWHERE") {
ret |= static_cast<uint32_t>(Account::Flag::CHEAT_ANYWHERE);
} else if (token_upper == "DISABLE_QUEST_REQUIREMENTS") {
ret |= static_cast<uint32_t>(Account::Flag::DISABLE_QUEST_REQUIREMENTS);
} else if (token_upper == "ALWAYS_ENABLE_CHAT_COMMANDS") {
ret |= static_cast<uint32_t>(Account::Flag::ALWAYS_ENABLE_CHAT_COMMANDS);
} else if (token_upper == "ROOT") {
ret |= static_cast<uint32_t>(Account::Flag::ROOT);
} else if (token_upper == "IS_SHARED_ACCOUNT") {
ret |= static_cast<uint32_t>(Account::Flag::IS_SHARED_ACCOUNT);
} else {
throw runtime_error("invalid flag name: " + token_upper);
}
}
return ret;
}
CommandDefinition c_add_account(
"add-account", "add-account [PARAMETERS...]\n\
Add an account to the server. <parameters> is some subset of:\n\
id=ACCOUNT-ID: preferred account ID in hex (optional)\n\
flags=FLAGS: behaviors and permissions for the account (see below)\n\
ep3-current-meseta=MESETA: Episode 3 Meseta value\n\
ep3-total-meseta=MESETA: Episode 3 total Meseta ever earned\n\
temporary: marks the account as temporary; it is not saved to disk and\n\
therefore will be deleted when the server shuts down\n\
If given, FLAGS is a comma-separated list of zero or more the following:\n\
NONE: Placeholder if no other flags are specified\n\
KICK_USER: Can kick other users offline\n\
BAN_USER: Can ban other users\n\
SILENCE_USER: Can silence other users\n\
MODERATOR: Alias for all of the above flags\n\
CHANGE_EVENT: Can change lobby events\n\
ANNOUNCE: Can make server-wide announcements\n\
FREE_JOIN_GAMES: Ignores game restrictions (level/quest requirements)\n\
ADMINISTRATOR: Alias for all of the above flags (including MODERATOR)\n\
DEBUG: Can use debugging commands\n\
CHEAT_ANYWHERE: Can use cheat commands even if cheat mode is disabled\n\
DISABLE_QUEST_REQUIREMENTS: Can play any quest without progression\n\
restrictions\n\
ALWAYS_ENABLE_CHAT_COMMANDS: Can use chat commands even if they are\n\
disabled in config.json\n\
ROOT: Alias for all of the above flags (including ADMINISTRATOR)\n\
IS_SHARED_ACCOUNT: Account is a shared serial (disables Access Key and\n\
password checks; players will get Guild Cards based on their player\n\
names)",
true,
+[](CommandArgs& args) {
auto account = make_shared<Account>();
for (const string& token : split(args.args, ' ')) {
if (starts_with(token, "id=")) {
account->account_id = stoul(token.substr(3), nullptr, 16);
} else if (starts_with(token, "ep3-current-meseta=")) {
account->ep3_current_meseta = stoul(token.substr(19), nullptr, 0);
} else if (starts_with(token, "ep3-total-meseta=")) {
account->ep3_total_meseta_earned = stoul(token.substr(17), nullptr, 0);
} else if (token == "temporary") {
account->is_temporary = true;
} else if (starts_with(token, "flags=")) {
account->flags = parse_account_flags(token.substr(6));
} else {
throw invalid_argument("invalid account field: " + token);
}
}
args.s->account_index->add(account);
account->save();
fprintf(stderr, "Account %08" PRIX32 " added\n", account->account_id);
});
CommandDefinition c_update_account(
"update-account", "update-account ACCOUNT-ID PARAMETERS...\n\
Update an existing license. ACCOUNT-ID (8 hex digits) specifies which\n\
account to update. The options are similar to the add-account command:\n\
flags=FLAGS: behaviors and permissions for the account (same as with\n\
add-account)\n\
ep3-current-meseta=MESETA: Episode 3 Meseta value\n\
ep3-total-meseta=MESETA: Episode 3 total Meseta ever earned\n\
temporary: marks the account as temporary; it is not saved to disk and\n\
therefore will be deleted when the server shuts down\n\
permanent: if the account was temporary, makes it non-temporary",
true,
+[](CommandArgs& args) {
auto tokens = split(args.args, ' ');
if (tokens.size() < 2) {
throw runtime_error("not enough arguments");
}
uint32_t serial_number = stoul(tokens[0]);
auto account = args.s->account_index->from_account_id(stoul(tokens[0], nullptr, 16));
tokens.erase(tokens.begin());
auto orig_l = args.s->license_index->get(serial_number);
auto l = args.s->license_index->create_license();
*l = *orig_l;
args.s->license_index->remove(orig_l->serial_number);
try {
for (const string& token : tokens) {
if (starts_with(token, "bb-username=")) {
l->bb_username = token.substr(12);
} else if (starts_with(token, "bb-password=")) {
l->bb_password = token.substr(12);
} else if (starts_with(token, "xb-gamertag=")) {
l->xb_gamertag = token.substr(12);
} else if (starts_with(token, "xb-user-id=")) {
l->xb_user_id = stoull(token.substr(11), nullptr, 16);
} else if (starts_with(token, "xb-account-id=")) {
l->xb_account_id = stoull(token.substr(14), nullptr, 16);
} else if (starts_with(token, "gc-password=")) {
l->gc_password = token.substr(12);
} else if (starts_with(token, "dc-nte-serial-number=")) {
l->dc_nte_serial_number = token.substr(21);
} else if (starts_with(token, "dc-nte-access-key=")) {
l->dc_nte_access_key = token.substr(18);
} else if (starts_with(token, "access-key=")) {
l->access_key = token.substr(11);
} else if (starts_with(token, "serial=")) {
l->serial_number = stoul(token.substr(7), nullptr, 0);
} else if (starts_with(token, "flags=")) {
string mask = token.substr(6);
if (mask == "normal") {
l->flags = 0;
} else if (mask == "mod") {
l->replace_all_flags(License::Flag::MODERATOR);
} else if (mask == "admin") {
l->replace_all_flags(License::Flag::ADMINISTRATOR);
} else if (mask == "root") {
l->replace_all_flags(License::Flag::ROOT);
} else {
l->flags = stoul(mask, nullptr, 16);
}
} else {
throw invalid_argument("incorrect field: " + token);
}
// Do all the parsing first, then the updates afterward, so we won't
// partially update the account if parsing a later option fails
int64_t new_ep3_current_meseta = -1;
int64_t new_ep3_total_meseta = -1;
int64_t new_flags = -1;
uint8_t new_is_temporary = 0xFF;
for (const string& token : tokens) {
if (starts_with(token, "ep3-current-meseta=")) {
new_ep3_current_meseta = stoul(token.substr(19), nullptr, 0);
} else if (starts_with(token, "ep3-total-meseta=")) {
new_ep3_total_meseta = stoul(token.substr(17), nullptr, 0);
} else if (token == "temporary") {
new_is_temporary = 1;
} else if (token == "permanent") {
new_is_temporary = 0;
} else if (starts_with(token, "flags=")) {
new_flags = parse_account_flags(token.substr(6));
} else {
throw invalid_argument("invalid account field: " + token);
}
if (!l->serial_number) {
throw invalid_argument("license does not contain serial number");
}
} catch (const exception&) {
args.s->license_index->add(orig_l);
throw;
}
l->save();
args.s->license_index->add(l);
fprintf(stderr, "License updated\n");
if (new_ep3_current_meseta > 0) {
account->ep3_current_meseta = new_ep3_current_meseta;
}
if (new_ep3_total_meseta > 0) {
account->ep3_total_meseta_earned = new_ep3_total_meseta;
}
if (new_flags > 0) {
account->flags = new_flags;
}
if (new_is_temporary != 0xFF) {
account->is_temporary = new_is_temporary;
}
account->save();
fprintf(stderr, "Account %08" PRIX32 " updated\n", account->account_id);
});
CommandDefinition c_delete_account(
"delete-account", "delete-account ACCOUNT-ID\n\
Delete an account from the server. If a player is online with the deleted\n\
account, they will not be automatically disconnected.",
true,
+[](CommandArgs& args) {
auto account = args.s->account_index->from_account_id(stoul(args.args, nullptr, 16));
args.s->account_index->remove(account->account_id);
account->is_temporary = true;
account->delete_file();
fprintf(stderr, "Account deleted\n");
});
CommandDefinition c_add_license(
"add-license", "add-license ACCOUNT-ID TYPE CREDENTIALS...\n\
Add a license to an account. Each account may have multiple licenses of\n\
each type. The types are:\n\
DC-NTE: CREDENTIALS is serial number and access key (16 characters each)\n\
DC: CREDENTIALS is serial number and access key (8 characters each)\n\
PC: CREDENTIALS is serial number and access key (8 characters each)\n\
GC: CREDENTIALS is serial number (10 digits), access key (12 digits), and\n\
password (up to 8 characters)\n\
XB: CREDENTIALS is gamertag (up to 16 characters), user ID (16 hex\n\
digits), and account ID (16 hex digits)\n\
BB: CREDENTIALS is username and password (up to 16 characters each)\n\
Examples (adding licenses to account 385A92C4):\n\
add-license 385A92C4 DC 107862F9 d38XTu2p\n\
add-license 385A92C4 GC 0418572923 282949185033 hunter2\n\
add-license 385A92C4 BB user1 trustno1",
true,
+[](CommandArgs& args) {
auto tokens = split(args.args, ' ');
if (tokens.size() < 3) {
throw runtime_error("not enough arguments");
}
auto account = args.s->account_index->from_account_id(stoul(tokens[0], nullptr, 16));
string type_str = toupper(tokens[1]);
if (type_str == "DC-NTE") {
if (tokens.size() != 4) {
throw runtime_error("incorrect number of parameters");
}
auto license = make_shared<DCNTELicense>();
license->serial_number = std::move(tokens[2]);
license->access_key = std::move(tokens[3]);
args.s->account_index->add_dc_nte_license(account, license);
} else if (type_str == "DC") {
if (tokens.size() != 4) {
throw runtime_error("incorrect number of parameters");
}
auto license = make_shared<V1V2License>();
license->serial_number = stoul(tokens[2], nullptr, 16);
license->access_key = std::move(tokens[3]);
args.s->account_index->add_dc_license(account, license);
} else if (type_str == "PC") {
if (tokens.size() != 4) {
throw runtime_error("incorrect number of parameters");
}
auto license = make_shared<V1V2License>();
license->serial_number = stoul(tokens[2], nullptr, 16);
license->access_key = std::move(tokens[3]);
args.s->account_index->add_pc_license(account, license);
} else if (type_str == "GC") {
if (tokens.size() != 5) {
throw runtime_error("incorrect number of parameters");
}
auto license = make_shared<GCLicense>();
license->serial_number = stoul(tokens[2], nullptr, 10);
license->access_key = std::move(tokens[3]);
license->password = std::move(tokens[4]);
args.s->account_index->add_gc_license(account, license);
} else if (type_str == "XB") {
if (tokens.size() != 5) {
throw runtime_error("incorrect number of parameters");
}
auto license = make_shared<XBLicense>();
license->gamertag = std::move(tokens[2]);
license->user_id = stoull(tokens[3], nullptr, 16);
license->account_id = stoull(tokens[4], nullptr, 16);
args.s->account_index->add_xb_license(account, license);
} else if (type_str == "BB") {
if (tokens.size() != 4) {
throw runtime_error("incorrect number of parameters");
}
auto license = make_shared<BBLicense>();
license->username = std::move(tokens[2]);
license->password = std::move(tokens[3]);
args.s->account_index->add_bb_license(account, license);
} else {
throw runtime_error("invalid license type");
}
account->save();
fprintf(stderr, "Account %08" PRIX32 " updated\n", account->account_id);
});
CommandDefinition c_delete_license(
"delete-license", "delete-license SERIAL-NUMBER\n\
Delete a license from the server.",
"delete-license", "delete-license ACCOUNT-ID TYPE PRIMARY-CREDENTIAL\n\
Delete a license from an account. ACCOUNT-ID and TYPE have the same\n\
meanings as for add-license. PRIMARY-CREDENTIAL is the first credential\n\
for the license type; specifically:\n\
DC-NTE: PRIMARY-CREDENTIAL is the serial number\n\
DC: PRIMARY-CREDENTIAL is the serial number (hex)\n\
PC: PRIMARY-CREDENTIAL is the serial number (hex)\n\
GC: PRIMARY-CREDENTIAL is the serial number (decimal)\n\
XB: PRIMARY-CREDENTIAL is the gamertag\n\
BB: PRIMARY-CREDENTIAL is the username\n\
Examples (deleting licenses from account 385A92C4):\n\
delete-license 385A92C4 DC 107862F9\n\
delete-license 385A92C4 GC 0418572923\n\
delete-license 385A92C4 BB user1",
true,
+[](CommandArgs& args) {
uint32_t serial_number = stoul(args.args);
auto l = args.s->license_index->get(serial_number);
l->delete_file();
args.s->license_index->remove(l->serial_number);
fprintf(stderr, "License deleted\n");
});
CommandDefinition c_list_licenses(
"list-licenses", "list-licenses\n\
List all licenses registered on the server.",
true,
+[](CommandArgs& args) {
auto licenses = args.s->license_index->all();
if (licenses.empty()) {
fprintf(stderr, "No licenses registered\n");
} else {
for (const auto& l : licenses) {
string s = l->str();
fprintf(stderr, "%s\n", s.c_str());
}
auto tokens = split(args.args, ' ');
if (tokens.size() != 3) {
throw runtime_error("incorrect argument count");
}
auto account = args.s->account_index->from_account_id(stoul(tokens[0], nullptr, 16));
string type_str = toupper(tokens[1]);
if (type_str == "DC-NTE") {
args.s->account_index->remove_dc_nte_license(account, tokens[2]);
} else if (type_str == "DC") {
args.s->account_index->remove_dc_license(account, stoul(tokens[2], nullptr, 16));
} else if (type_str == "PC") {
args.s->account_index->remove_pc_license(account, stoul(tokens[2], nullptr, 16));
} else if (type_str == "GC") {
args.s->account_index->remove_gc_license(account, stoul(tokens[2], nullptr, 0));
} else if (type_str == "XB") {
args.s->account_index->remove_xb_license(account, tokens[2]);
} else if (type_str == "BB") {
args.s->account_index->remove_bb_license(account, tokens[2]);
} else {
throw runtime_error("invalid license type");
}
account->save();
fprintf(stderr, "Account %08" PRIX32 " updated\n", account->account_id);
});
CommandDefinition c_announce(