From f3842b49f30da3266513434bdf4e8baefc78fee9 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Tue, 29 Jan 2019 00:17:04 -0800 Subject: [PATCH] add interactive shell --- License.cc | 59 +++++++++++++-- License.hh | 5 ++ Main.cc | 22 ++++-- Makefile | 4 +- README.md | 3 +- ServerState.cc | 3 +- ServerState.hh | 1 + Shell.cc | 182 +++++++++++++++++++++++++++++++++++++++++++++ Shell.hh | 7 ++ system/config.json | 3 + 10 files changed, 274 insertions(+), 15 deletions(-) create mode 100644 Shell.cc create mode 100644 Shell.hh diff --git a/License.cc b/License.cc index 61f2b766..b08d0283 100644 --- a/License.cc +++ b/License.cc @@ -1,3 +1,4 @@ +#include #include #include @@ -10,13 +11,44 @@ using namespace std; -LicenseManager::LicenseManager(const std::string& filename) : filename(filename) { - auto licenses = load_vector_file(this->filename); +string License::str() const { + string ret = string_printf("License(serial_number=%" PRIu32, this->serial_number); + if (this->username[0]) { + ret += ", username="; + ret += this->username; + } + if (this->bb_password[0]) { + ret += ", bb-password="; + ret += this->bb_password; + } + if (this->access_key[0]) { + ret += ", access-key="; + ret += this->access_key; + } + if (this->gc_password[0]) { + ret += ", gc-password="; + ret += this->gc_password; + } + ret += string_printf(", privileges=%" PRIu32, this->privileges); + if (this->ban_end_time) { + ret += string_printf(", banned-until=%" PRIu64, this->ban_end_time); + } + return ret + ")"; +} - for (const auto& read_license : licenses) { - shared_ptr license(new License(read_license)); - this->bb_username_to_license.emplace(license->username, license); - this->serial_number_to_license.emplace(license->serial_number, license); + + +LicenseManager::LicenseManager(const std::string& filename) : filename(filename) { + try { + auto licenses = load_vector_file(this->filename); + for (const auto& read_license : licenses) { + shared_ptr license(new License(read_license)); + this->bb_username_to_license.emplace(license->username, license); + this->serial_number_to_license.emplace(license->serial_number, license); + } + + } catch (const cannot_open_file&) { + log(WARNING, "%s does not exist; no licenses are registered", this->filename.c_str()); } } @@ -78,6 +110,11 @@ shared_ptr LicenseManager::verify_bb(const char* username, return license; } +size_t LicenseManager::count() const { + rw_guard g(this->lock, false); + return this->serial_number_to_license.size(); +} + void LicenseManager::ban_until(uint32_t serial_number, uint64_t end_time) { rw_guard g(this->lock, false); this->serial_number_to_license.at(serial_number)->ban_end_time = end_time; @@ -110,3 +147,13 @@ void LicenseManager::remove(uint32_t serial_number) { rw_guard g(this->lock, false); this->save_locked(); } + +vector LicenseManager::snapshot() const { + vector ret; + + rw_guard g(this->lock, false); + for (auto it : this->serial_number_to_license) { + ret.emplace_back(*it.second); + } + return ret; +} diff --git a/License.hh b/License.hh index 2fe091a0..5f72d25d 100644 --- a/License.hh +++ b/License.hh @@ -34,6 +34,8 @@ struct License { char gc_password[12]; // GC password uint32_t privileges; // privilege level uint64_t ban_end_time; // end time of ban (zero = not banned) + + std::string str() const; } __attribute__((packed)); class LicenseManager { @@ -49,8 +51,11 @@ public: const char* password) const; void ban_until(uint32_t serial_number, uint64_t seconds); + size_t count() const; + void add(std::shared_ptr l); void remove(uint32_t serial_number); + std::vector snapshot() const; protected: void save_locked() const; diff --git a/Main.cc b/Main.cc index c47fff9b..31d9191b 100644 --- a/Main.cc +++ b/Main.cc @@ -15,6 +15,7 @@ #include "Server.hh" #include "FileContentsCache.hh" #include "Text.hh" +#include "Shell.hh" using namespace std; @@ -112,6 +113,12 @@ void populate_state_from_config(shared_ptr s, } catch (const JSONObject::key_error&) { s->run_dns_server = true; } + + try { + s->run_interactive_shell = d.at("RunInteractiveShell")->as_bool(); + } catch (const JSONObject::key_error&) { + s->run_interactive_shell = true; + } } @@ -165,10 +172,16 @@ int main(int argc, char* argv[]) { } game_server->start(); - for (;;) { - sigset_t s; - sigemptyset(&s); - sigsuspend(&s); + if (state->run_interactive_shell) { + log(INFO, "starting interactive shell"); + run_shell(state); + + } else { + for (;;) { + sigset_t s; + sigemptyset(&s); + sigsuspend(&s); + } } log(INFO, "waiting for servers to terminate"); @@ -179,4 +192,3 @@ int main(int argc, char* argv[]) { return 0; } - diff --git a/Makefile b/Makefile index 55e05e68..f6143283 100644 --- a/Makefile +++ b/Makefile @@ -2,10 +2,10 @@ OBJECTS=FileContentsCache.o Menu.o PSOProtocol.o Client.o Lobby.o \ ServerState.o Server.o License.o PSOEncryption.o Player.o SendCommands.o \ ChatCommands.o ReceiveSubcommands.o ReceiveCommands.o Version.o Items.o \ LevelTable.o Compression.o Quest.o RareItemSet.o Map.o NetworkAddresses.o \ - Text.o DNSServer.o Main.o + Text.o DNSServer.o Shell.o Main.o CXX=g++ CXXFLAGS=-I/opt/local/include -I/usr/local/include -std=c++14 -g -DHAVE_INTTYPES_H -DHAVE_NETINET_IN_H -Wall -Werror -LDFLAGS=-L/opt/local/lib -L/usr/local/lib -std=c++14 -levent -levent_pthreads -lphosg -lpthread +LDFLAGS=-L/opt/local/lib -L/usr/local/lib -std=c++14 -levent -levent_pthreads -lphosg -lpthread -lreadline EXECUTABLE=newserv all: $(EXECUTABLE) diff --git a/README.md b/README.md index 3c17812b..b135186e 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,8 @@ So, you've read all of the above and you want to try it out? Here's what you do: - Build and install phosg (https://github.com/fuzziqersoftware/phosg). - Run `make`. - Edit system/config.json to your liking. -- Run `./newserv` in the newserv directory. +- Run `./newserv` in the newserv directory. This will start the game server and run the interactive shell. (You can disable the interactive shell later by editing config.json.) +- Use the interactive shell to add a license. Run `help` in the shell to see how to do this. If you're running PSO on a real GameCube, you can make PSO connect to newserv by changing its default gateway and DNS server addresses to newserv's address. diff --git a/ServerState.cc b/ServerState.cc index 28946340..dc47cde6 100644 --- a/ServerState.cc +++ b/ServerState.cc @@ -10,7 +10,8 @@ using namespace std; -ServerState::ServerState() : run_dns_server(true), next_lobby_id(1) { +ServerState::ServerState() : run_dns_server(true), run_interactive_shell(true), + next_lobby_id(1) { this->main_menu.emplace_back(MAIN_MENU_GO_TO_LOBBY, u"Go to lobby", u"Join the lobby.", 0); this->main_menu.emplace_back(MAIN_MENU_INFORMATION, u"Information", diff --git a/ServerState.hh b/ServerState.hh index de2e5435..b268c0d8 100644 --- a/ServerState.hh +++ b/ServerState.hh @@ -29,6 +29,7 @@ struct ServerState { std::u16string name; std::unordered_map port_configuration; bool run_dns_server; + bool run_interactive_shell; std::shared_ptr quest_index; std::shared_ptr level_table; std::shared_ptr battle_params; diff --git a/Shell.cc b/Shell.cc new file mode 100644 index 00000000..e5ebf16f --- /dev/null +++ b/Shell.cc @@ -0,0 +1,182 @@ +#include "Shell.hh" + +#include +#include +#include + +#include + +using namespace std; + + + +class exit_shell : public runtime_error { +public: + exit_shell() : runtime_error("shell exited") { } + ~exit_shell() = default; +}; + + + +void execute_command(shared_ptr state, const string& command) { + // find the entry in the command table and run the command + size_t command_end = skip_non_whitespace(command, 0); + size_t args_begin = skip_whitespace(command, command_end); + string command_name = command.substr(0, command_end); + string command_args = command.substr(args_begin); + + if (command_name == "exit") { + throw exit_shell(); + + } else if (command_name == "help") { + fprintf(stderr, "\ +commands:\n\ + help\n\ + you\'re reading it now\n\ + exit (or ctrl+d)\n\ + shut down the server\n\ + reload ...\n\ + reload data. can be licenses, battle-params, level-table, or quests.\n\ + add-license \n\ + add a license to the server. is some subset of the following:\n\ + username= (bb username)\n\ + bb-password= (bb password)\n\ + gc-password= (gc password)\n\ + access-key= (gc/pc access key)\n\ + serial= (gc/pc serial number; required for all licenses)\n\ + privileges= (can be normal, mod, admin, root, or numeric)\n\ + delete-license \n\ + delete a license from the server\n\ + list-licenses\n\ + list all licenses registered on the server\n\ +"); + + } else if (command_name == "reload") { + auto types = split(command_args, ' '); + for (const string& type : types) { + if (type == "licenses") { + shared_ptr lm(new LicenseManager("system/licenses.nsi")); + state->license_manager = lm; + } else if (type == "battle-params") { + shared_ptr bpt(new BattleParamTable("system/blueburst/BattleParamEntry")); + state->battle_params = bpt; + } else if (type == "level-table") { + shared_ptr lt(new LevelTable("system/blueburst/PlyLevelTbl.prs", true)); + state->level_table = lt; + } else if (type == "quests") { + shared_ptr qi(new QuestIndex("system/quests")); + state->quest_index = qi; + } else { + throw invalid_argument("incorrect data type"); + } + } + + } else if (command_name == "add-license") { + shared_ptr l(new License()); + memset(l.get(), 0, sizeof(License)); + + for (const string& token : split(command_args, ' ')) { + if (starts_with(token, "username=")) { + if (token.size() >= 29) { + throw invalid_argument("username too long"); + } + strcpy(l->username, token.c_str() + 9); + + } else if (starts_with(token, "bb-password=")) { + if (token.size() >= 32) { + throw invalid_argument("bb-password too long"); + } + strcpy(l->bb_password, token.c_str() + 12); + + } else if (starts_with(token, "gc-password=")) { + if (token.size() > 20) { + throw invalid_argument("gc-password too long"); + } + strcpy(l->gc_password, token.c_str() + 12); + + } else if (starts_with(token, "access-key=")) { + if (token.size() > 23) { + throw invalid_argument("access-key is too long bytes"); + } + strcpy(l->gc_password, token.c_str() + 11); + + } else if (starts_with(token, "serial=")) { + l->serial_number = stoul(token.substr(7)); + + } else if (starts_with(token, "privileges=")) { + string mask = token.substr(11); + if (mask == "normal") { + l->privileges = 0; + } else if (mask == "mod") { + l->privileges = Privilege::Moderator; + } else if (mask == "admin") { + l->privileges = Privilege::Administrator; + } else if (mask == "root") { + l->privileges = Privilege::Root; + } else { + l->privileges = stoul(mask); + } + + } else { + throw invalid_argument("incorrect field"); + } + } + + if (!l->serial_number) { + throw invalid_argument("license does not contain serial number"); + } + + state->license_manager->add(l); + fprintf(stderr, "license added\n"); + + } else if (command_name == "delete-license") { + uint32_t serial_number = stoul(command_args); + state->license_manager->remove(serial_number); + fprintf(stderr, "license deleted\n"); + + } else if (command_name == "list-licenses") { + for (const auto& l : state->license_manager->snapshot()) { + string s = l.str(); + fprintf(stderr, "%s\n", s.c_str()); + } + + } else { + throw invalid_argument("unknown command; try \'help\'"); + } +} + +void run_shell(shared_ptr state) { + // initialize history + using_history(); + // string history_filename = get_user_home_directory() + "/.newserv_history"; + // read_history(history_filename.c_str()); + // stifle_history(HISTORY_FILE_LENGTH); + + // read and execute commands + bool should_continue = true; + while (should_continue) { + + // read the command + char* command = readline("newserv> "); + if (!command) { + fprintf(stderr, " -- exit\n"); + break; + } + + // if there's a command, add it to the history + const char* command_to_execute = command + skip_whitespace(command, 0); + if (command_to_execute && *command_to_execute) { + add_history(command); + } + + // dispatch the command + try { + execute_command(state, command_to_execute); + } catch (const exit_shell&) { + should_continue = false; + } catch (const exception& e) { + fprintf(stderr, "FAILED: %s\n", e.what()); + } + free(command); + } +} diff --git a/Shell.hh b/Shell.hh new file mode 100644 index 00000000..884c94eb --- /dev/null +++ b/Shell.hh @@ -0,0 +1,7 @@ +#pragma once + +#include + +#include "ServerState.hh" + +void run_shell(std::shared_ptr state); diff --git a/system/config.json b/system/config.json index 62b2e6db..fba35c23 100755 --- a/system/config.json +++ b/system/config.json @@ -18,6 +18,9 @@ "Threads": 1, // Set to false to disable the DNS server "RunDNSServer": true, + // Set to false to disable the interactive shell (do this if stdin is + // /dev/null, for example) + "RunInteractiveShell": true, // **************** // INFORMATION MENU