| | |
| | |
| |
|
| | #include <chrono> |
| | #include <fstream> |
| | #include <iostream> |
| | #include <memory> |
| | #include <regex> |
| | #include <string> |
| | #include <thread> |
| |
|
| | #ifdef _WIN32 |
| | |
| | #include <windows.h> |
| |
|
| | #include <shellapi.h> |
| | #endif |
| |
|
| | #include <mbedtls/base64.h> |
| | #include "common/common_types.h" |
| | #include "common/detached_tasks.h" |
| | #include "common/fs/file.h" |
| | #include "common/fs/fs.h" |
| | #include "common/fs/path_util.h" |
| | #include "common/logging/backend.h" |
| | #include "common/logging/log.h" |
| | #include "common/scm_rev.h" |
| | #include "common/settings.h" |
| | #include "common/string_util.h" |
| | #include "core/core.h" |
| | #include "network/announce_multiplayer_session.h" |
| | #include "network/network.h" |
| | #include "network/room.h" |
| | #include "network/verify_user.h" |
| |
|
| | #ifdef ENABLE_WEB_SERVICE |
| | #include "web_service/verify_user_jwt.h" |
| | #endif |
| |
|
| | #undef _UNICODE |
| | #include <getopt.h> |
| | #ifndef _MSC_VER |
| | #include <unistd.h> |
| | #endif |
| |
|
| | static void PrintHelp(const char* argv0) { |
| | LOG_INFO(Network, |
| | "Usage: {}" |
| | " [options] <filename>\n" |
| | "--room-name The name of the room\n" |
| | "--room-description The room description\n" |
| | "--bind-address The bind address for the room\n" |
| | "--port The port used for the room\n" |
| | "--max_members The maximum number of players for this room\n" |
| | "--password The password for the room\n" |
| | "--preferred-game The preferred game for this room\n" |
| | "--preferred-game-id The preferred game-id for this room\n" |
| | "--username The username used for announce\n" |
| | "--token The token used for announce\n" |
| | "--web-api-url yuzu Web API url\n" |
| | "--ban-list-file The file for storing the room ban list\n" |
| | "--log-file The file for storing the room log\n" |
| | "--enable-yuzu-mods Allow yuzu Community Moderators to moderate on your room\n" |
| | "-h, --help Display this help and exit\n" |
| | "-v, --version Output version information and exit\n", |
| | argv0); |
| | } |
| |
|
| | static void PrintVersion() { |
| | LOG_INFO(Network, "yuzu dedicated room {} {} Libnetwork: {}", Common::g_scm_branch, |
| | Common::g_scm_desc, Network::network_version); |
| | } |
| |
|
| | |
| | static constexpr char BanListMagic[] = "YuzuRoom-BanList-1"; |
| |
|
| | static constexpr char token_delimiter{':'}; |
| |
|
| | static void PadToken(std::string& token) { |
| | std::size_t outlen = 0; |
| |
|
| | std::array<unsigned char, 512> output{}; |
| | std::array<unsigned char, 2048> roundtrip{}; |
| | for (size_t i = 0; i < 3; i++) { |
| | mbedtls_base64_decode(output.data(), output.size(), &outlen, |
| | reinterpret_cast<const unsigned char*>(token.c_str()), |
| | token.length()); |
| | mbedtls_base64_encode(roundtrip.data(), roundtrip.size(), &outlen, output.data(), outlen); |
| | if (memcmp(roundtrip.data(), token.data(), token.size()) == 0) { |
| | break; |
| | } |
| | token.push_back('='); |
| | } |
| | } |
| |
|
| | static std::string UsernameFromDisplayToken(const std::string& display_token) { |
| | std::size_t outlen; |
| |
|
| | std::array<unsigned char, 512> output{}; |
| | mbedtls_base64_decode(output.data(), output.size(), &outlen, |
| | reinterpret_cast<const unsigned char*>(display_token.c_str()), |
| | display_token.length()); |
| | std::string decoded_display_token(reinterpret_cast<char*>(&output), outlen); |
| | return decoded_display_token.substr(0, decoded_display_token.find(token_delimiter)); |
| | } |
| |
|
| | static std::string TokenFromDisplayToken(const std::string& display_token) { |
| | std::size_t outlen; |
| |
|
| | std::array<unsigned char, 512> output{}; |
| | mbedtls_base64_decode(output.data(), output.size(), &outlen, |
| | reinterpret_cast<const unsigned char*>(display_token.c_str()), |
| | display_token.length()); |
| | std::string decoded_display_token(reinterpret_cast<char*>(&output), outlen); |
| | return decoded_display_token.substr(decoded_display_token.find(token_delimiter) + 1); |
| | } |
| |
|
| | static Network::Room::BanList LoadBanList(const std::string& path) { |
| | std::ifstream file; |
| | Common::FS::OpenFileStream(file, path, std::ios_base::in); |
| | if (!file || file.eof()) { |
| | LOG_ERROR(Network, "Could not open ban list!"); |
| | return {}; |
| | } |
| | std::string magic; |
| | std::getline(file, magic); |
| | if (magic != BanListMagic) { |
| | LOG_ERROR(Network, "Ban list is not valid!"); |
| | return {}; |
| | } |
| |
|
| | |
| | bool ban_list_type = false; |
| | Network::Room::UsernameBanList username_ban_list; |
| | Network::Room::IPBanList ip_ban_list; |
| | while (!file.eof()) { |
| | std::string line; |
| | std::getline(file, line); |
| | line.erase(std::remove(line.begin(), line.end(), '\0'), line.end()); |
| | line = Common::StripSpaces(line); |
| | if (line.empty()) { |
| | |
| | ban_list_type = true; |
| | continue; |
| | } |
| | if (ban_list_type) { |
| | ip_ban_list.emplace_back(line); |
| | } else { |
| | username_ban_list.emplace_back(line); |
| | } |
| | } |
| |
|
| | return {username_ban_list, ip_ban_list}; |
| | } |
| |
|
| | static void SaveBanList(const Network::Room::BanList& ban_list, const std::string& path) { |
| | std::ofstream file; |
| | Common::FS::OpenFileStream(file, path, std::ios_base::out); |
| | if (!file) { |
| | LOG_ERROR(Network, "Could not save ban list!"); |
| | return; |
| | } |
| |
|
| | file << BanListMagic << "\n"; |
| |
|
| | |
| | for (const auto& username : ban_list.first) { |
| | file << username << "\n"; |
| | } |
| | file << "\n"; |
| |
|
| | |
| | for (const auto& ip : ban_list.second) { |
| | file << ip << "\n"; |
| | } |
| | } |
| |
|
| | static void InitializeLogging(const std::string& log_file) { |
| | Common::Log::Initialize(); |
| | Common::Log::SetColorConsoleBackendEnabled(true); |
| | Common::Log::Start(); |
| | } |
| |
|
| | |
| | int main(int argc, char** argv) { |
| | Common::DetachedTasks detached_tasks; |
| | int option_index = 0; |
| | char* endarg; |
| |
|
| | std::string room_name; |
| | std::string room_description; |
| | std::string password; |
| | std::string preferred_game; |
| | std::string username; |
| | std::string token; |
| | std::string web_api_url; |
| | std::string ban_list_file; |
| | std::string log_file = "yuzu-room.log"; |
| | std::string bind_address; |
| | u64 preferred_game_id = 0; |
| | u32 port = Network::DefaultRoomPort; |
| | u32 max_members = 16; |
| | bool enable_yuzu_mods = false; |
| |
|
| | static struct option long_options[] = { |
| | {"room-name", required_argument, 0, 'n'}, |
| | {"room-description", required_argument, 0, 'd'}, |
| | {"bind-address", required_argument, 0, 's'}, |
| | {"port", required_argument, 0, 'p'}, |
| | {"max_members", required_argument, 0, 'm'}, |
| | {"password", required_argument, 0, 'w'}, |
| | {"preferred-game", required_argument, 0, 'g'}, |
| | {"preferred-game-id", required_argument, 0, 'i'}, |
| | {"username", optional_argument, 0, 'u'}, |
| | {"token", required_argument, 0, 't'}, |
| | {"web-api-url", required_argument, 0, 'a'}, |
| | {"ban-list-file", required_argument, 0, 'b'}, |
| | {"log-file", required_argument, 0, 'l'}, |
| | {"enable-yuzu-mods", no_argument, 0, 'e'}, |
| | {"help", no_argument, 0, 'h'}, |
| | {"version", no_argument, 0, 'v'}, |
| | {0, 0, 0, 0}, |
| | }; |
| |
|
| | InitializeLogging(log_file); |
| |
|
| | while (optind < argc) { |
| | int arg = |
| | getopt_long(argc, argv, "n:d:s:p:m:w:g:u:t:a:i:l:hv", long_options, &option_index); |
| | if (arg != -1) { |
| | switch (static_cast<char>(arg)) { |
| | case 'n': |
| | room_name.assign(optarg); |
| | break; |
| | case 'd': |
| | room_description.assign(optarg); |
| | break; |
| | case 's': |
| | bind_address.assign(optarg); |
| | break; |
| | case 'p': |
| | port = strtoul(optarg, &endarg, 0); |
| | break; |
| | case 'm': |
| | max_members = strtoul(optarg, &endarg, 0); |
| | break; |
| | case 'w': |
| | password.assign(optarg); |
| | break; |
| | case 'g': |
| | preferred_game.assign(optarg); |
| | break; |
| | case 'i': |
| | preferred_game_id = strtoull(optarg, &endarg, 16); |
| | break; |
| | case 'u': |
| | username.assign(optarg); |
| | break; |
| | case 't': |
| | token.assign(optarg); |
| | break; |
| | case 'a': |
| | web_api_url.assign(optarg); |
| | break; |
| | case 'b': |
| | ban_list_file.assign(optarg); |
| | break; |
| | case 'l': |
| | log_file.assign(optarg); |
| | break; |
| | case 'e': |
| | enable_yuzu_mods = true; |
| | break; |
| | case 'h': |
| | PrintHelp(argv[0]); |
| | return 0; |
| | case 'v': |
| | PrintVersion(); |
| | return 0; |
| | } |
| | } |
| | } |
| |
|
| | if (room_name.empty()) { |
| | LOG_ERROR(Network, "Room name is empty!"); |
| | PrintHelp(argv[0]); |
| | return -1; |
| | } |
| | if (preferred_game.empty()) { |
| | LOG_ERROR(Network, "Preferred game is empty!"); |
| | PrintHelp(argv[0]); |
| | return -1; |
| | } |
| | if (preferred_game_id == 0) { |
| | LOG_ERROR(Network, |
| | "preferred-game-id not set!\nThis should get set to allow users to find your " |
| | "room.\nSet with --preferred-game-id id"); |
| | } |
| | if (max_members > Network::MaxConcurrentConnections || max_members < 2) { |
| | LOG_ERROR(Network, "max_members needs to be in the range 2 - {}!", |
| | Network::MaxConcurrentConnections); |
| | PrintHelp(argv[0]); |
| | return -1; |
| | } |
| | if (bind_address.empty()) { |
| | LOG_INFO(Network, "Bind address is empty: defaulting to 0.0.0.0"); |
| | } |
| | if (port > UINT16_MAX) { |
| | LOG_ERROR(Network, "Port needs to be in the range 0 - 65535!"); |
| | PrintHelp(argv[0]); |
| | return -1; |
| | } |
| | if (ban_list_file.empty()) { |
| | LOG_ERROR(Network, "Ban list file not set!\nThis should get set to load and save room ban " |
| | "list.\nSet with --ban-list-file <file>"); |
| | } |
| | bool announce = true; |
| | if (token.empty() && announce) { |
| | announce = false; |
| | LOG_INFO(Network, "Token is empty: Hosting a private room"); |
| | } |
| | if (web_api_url.empty() && announce) { |
| | announce = false; |
| | LOG_INFO(Network, "Endpoint url is empty: Hosting a private room"); |
| | } |
| | if (announce) { |
| | if (username.empty()) { |
| | LOG_INFO(Network, "Hosting a public room"); |
| | Settings::values.web_api_url = web_api_url; |
| | PadToken(token); |
| | Settings::values.yuzu_username = UsernameFromDisplayToken(token); |
| | username = Settings::values.yuzu_username.GetValue(); |
| | Settings::values.yuzu_token = TokenFromDisplayToken(token); |
| | } else { |
| | LOG_INFO(Network, "Hosting a public room"); |
| | Settings::values.web_api_url = web_api_url; |
| | Settings::values.yuzu_username = username; |
| | Settings::values.yuzu_token = token; |
| | } |
| | } |
| | if (!announce && enable_yuzu_mods) { |
| | enable_yuzu_mods = false; |
| | LOG_INFO(Network, "Can not enable yuzu Moderators for private rooms"); |
| | } |
| |
|
| | |
| | Network::Room::BanList ban_list; |
| | if (!ban_list_file.empty()) { |
| | ban_list = LoadBanList(ban_list_file); |
| | } |
| |
|
| | std::unique_ptr<Network::VerifyUser::Backend> verify_backend; |
| | if (announce) { |
| | #ifdef ENABLE_WEB_SERVICE |
| | verify_backend = |
| | std::make_unique<WebService::VerifyUserJWT>(Settings::values.web_api_url.GetValue()); |
| | #else |
| | LOG_INFO(Network, |
| | "yuzu Web Services is not available with this build: validation is disabled."); |
| | verify_backend = std::make_unique<Network::VerifyUser::NullBackend>(); |
| | #endif |
| | } else { |
| | verify_backend = std::make_unique<Network::VerifyUser::NullBackend>(); |
| | } |
| |
|
| | Network::RoomNetwork network{}; |
| | network.Init(); |
| | if (auto room = network.GetRoom().lock()) { |
| | AnnounceMultiplayerRoom::GameInfo preferred_game_info{.name = preferred_game, |
| | .id = preferred_game_id}; |
| | if (!room->Create(room_name, room_description, bind_address, static_cast<u16>(port), |
| | password, max_members, username, preferred_game_info, |
| | std::move(verify_backend), ban_list, enable_yuzu_mods)) { |
| | LOG_INFO(Network, "Failed to create room: "); |
| | return -1; |
| | } |
| | LOG_INFO(Network, "Room is open. Close with Q+Enter..."); |
| | auto announce_session = std::make_unique<Core::AnnounceMultiplayerSession>(network); |
| | if (announce) { |
| | announce_session->Start(); |
| | } |
| | while (room->GetState() == Network::Room::State::Open) { |
| | std::string in; |
| | std::cin >> in; |
| | if (in.size() > 0) { |
| | break; |
| | } |
| | std::this_thread::sleep_for(std::chrono::milliseconds(100)); |
| | } |
| | if (announce) { |
| | announce_session->Stop(); |
| | } |
| | announce_session.reset(); |
| | |
| | if (!ban_list_file.empty()) { |
| | SaveBanList(room->GetBanList(), ban_list_file); |
| | } |
| | room->Destroy(); |
| | } |
| | network.Shutdown(); |
| | detached_tasks.WaitForAllTasks(); |
| | return 0; |
| | } |
| |
|