yuzu_room.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. // SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
  2. // SPDX-License-Identifier: GPL-2.0-or-later
  3. #include <chrono>
  4. #include <fstream>
  5. #include <iostream>
  6. #include <memory>
  7. #include <regex>
  8. #include <string>
  9. #include <thread>
  10. #ifdef _WIN32
  11. // windows.h needs to be included before shellapi.h
  12. #include <windows.h>
  13. #include <shellapi.h>
  14. #endif
  15. #include <mbedtls/base64.h>
  16. #include "common/common_types.h"
  17. #include "common/detached_tasks.h"
  18. #include "common/fs/file.h"
  19. #include "common/fs/fs.h"
  20. #include "common/fs/path_util.h"
  21. #include "common/logging/backend.h"
  22. #include "common/logging/log.h"
  23. #include "common/scm_rev.h"
  24. #include "common/settings.h"
  25. #include "common/string_util.h"
  26. #include "core/core.h"
  27. #include "network/announce_multiplayer_session.h"
  28. #include "network/network.h"
  29. #include "network/room.h"
  30. #include "network/verify_user.h"
  31. #ifdef ENABLE_WEB_SERVICE
  32. #include "web_service/verify_user_jwt.h"
  33. #endif
  34. #undef _UNICODE
  35. #include <getopt.h>
  36. #ifndef _MSC_VER
  37. #include <unistd.h>
  38. #endif
  39. static void PrintHelp(const char* argv0) {
  40. LOG_INFO(Network,
  41. "Usage: {}"
  42. " [options] <filename>\n"
  43. "--room-name The name of the room\n"
  44. "--room-description The room description\n"
  45. "--bind-address The bind address for the room\n"
  46. "--port The port used for the room\n"
  47. "--max_members The maximum number of players for this room\n"
  48. "--password The password for the room\n"
  49. "--preferred-game The preferred game for this room\n"
  50. "--preferred-game-id The preferred game-id for this room\n"
  51. "--username The username used for announce\n"
  52. "--token The token used for announce\n"
  53. "--web-api-url yuzu Web API url\n"
  54. "--ban-list-file The file for storing the room ban list\n"
  55. "--log-file The file for storing the room log\n"
  56. "--enable-yuzu-mods Allow yuzu Community Moderators to moderate on your room\n"
  57. "-h, --help Display this help and exit\n"
  58. "-v, --version Output version information and exit\n",
  59. argv0);
  60. }
  61. static void PrintVersion() {
  62. LOG_INFO(Network, "yuzu dedicated room {} {} Libnetwork: {}", Common::g_scm_branch,
  63. Common::g_scm_desc, Network::network_version);
  64. }
  65. /// The magic text at the beginning of a yuzu-room ban list file.
  66. static constexpr char BanListMagic[] = "YuzuRoom-BanList-1";
  67. static constexpr char token_delimiter{':'};
  68. static void PadToken(std::string& token) {
  69. std::size_t outlen = 0;
  70. std::array<unsigned char, 512> output{};
  71. std::array<unsigned char, 2048> roundtrip{};
  72. for (size_t i = 0; i < 3; i++) {
  73. mbedtls_base64_decode(output.data(), output.size(), &outlen,
  74. reinterpret_cast<const unsigned char*>(token.c_str()),
  75. token.length());
  76. mbedtls_base64_encode(roundtrip.data(), roundtrip.size(), &outlen, output.data(), outlen);
  77. if (memcmp(roundtrip.data(), token.data(), token.size()) == 0) {
  78. break;
  79. }
  80. token.push_back('=');
  81. }
  82. }
  83. static std::string UsernameFromDisplayToken(const std::string& display_token) {
  84. std::size_t outlen;
  85. std::array<unsigned char, 512> output{};
  86. mbedtls_base64_decode(output.data(), output.size(), &outlen,
  87. reinterpret_cast<const unsigned char*>(display_token.c_str()),
  88. display_token.length());
  89. std::string decoded_display_token(reinterpret_cast<char*>(&output), outlen);
  90. return decoded_display_token.substr(0, decoded_display_token.find(token_delimiter));
  91. }
  92. static std::string TokenFromDisplayToken(const std::string& display_token) {
  93. std::size_t outlen;
  94. std::array<unsigned char, 512> output{};
  95. mbedtls_base64_decode(output.data(), output.size(), &outlen,
  96. reinterpret_cast<const unsigned char*>(display_token.c_str()),
  97. display_token.length());
  98. std::string decoded_display_token(reinterpret_cast<char*>(&output), outlen);
  99. return decoded_display_token.substr(decoded_display_token.find(token_delimiter) + 1);
  100. }
  101. static Network::Room::BanList LoadBanList(const std::string& path) {
  102. std::ifstream file;
  103. Common::FS::OpenFileStream(file, path, std::ios_base::in);
  104. if (!file || file.eof()) {
  105. LOG_ERROR(Network, "Could not open ban list!");
  106. return {};
  107. }
  108. std::string magic;
  109. std::getline(file, magic);
  110. if (magic != BanListMagic) {
  111. LOG_ERROR(Network, "Ban list is not valid!");
  112. return {};
  113. }
  114. // false = username ban list, true = ip ban list
  115. bool ban_list_type = false;
  116. Network::Room::UsernameBanList username_ban_list;
  117. Network::Room::IPBanList ip_ban_list;
  118. while (!file.eof()) {
  119. std::string line;
  120. std::getline(file, line);
  121. line.erase(std::remove(line.begin(), line.end(), '\0'), line.end());
  122. line = Common::StripSpaces(line);
  123. if (line.empty()) {
  124. // An empty line marks start of the IP ban list
  125. ban_list_type = true;
  126. continue;
  127. }
  128. if (ban_list_type) {
  129. ip_ban_list.emplace_back(line);
  130. } else {
  131. username_ban_list.emplace_back(line);
  132. }
  133. }
  134. return {username_ban_list, ip_ban_list};
  135. }
  136. static void SaveBanList(const Network::Room::BanList& ban_list, const std::string& path) {
  137. std::ofstream file;
  138. Common::FS::OpenFileStream(file, path, std::ios_base::out);
  139. if (!file) {
  140. LOG_ERROR(Network, "Could not save ban list!");
  141. return;
  142. }
  143. file << BanListMagic << "\n";
  144. // Username ban list
  145. for (const auto& username : ban_list.first) {
  146. file << username << "\n";
  147. }
  148. file << "\n";
  149. // IP ban list
  150. for (const auto& ip : ban_list.second) {
  151. file << ip << "\n";
  152. }
  153. }
  154. static void InitializeLogging(const std::string& log_file) {
  155. Common::Log::Initialize();
  156. Common::Log::SetColorConsoleBackendEnabled(true);
  157. Common::Log::Start();
  158. }
  159. /// Application entry point
  160. int main(int argc, char** argv) {
  161. Common::DetachedTasks detached_tasks;
  162. int option_index = 0;
  163. char* endarg;
  164. std::string room_name;
  165. std::string room_description;
  166. std::string password;
  167. std::string preferred_game;
  168. std::string username;
  169. std::string token;
  170. std::string web_api_url;
  171. std::string ban_list_file;
  172. std::string log_file = "yuzu-room.log";
  173. std::string bind_address;
  174. u64 preferred_game_id = 0;
  175. u32 port = Network::DefaultRoomPort;
  176. u32 max_members = 16;
  177. bool enable_yuzu_mods = false;
  178. static struct option long_options[] = {
  179. {"room-name", required_argument, 0, 'n'},
  180. {"room-description", required_argument, 0, 'd'},
  181. {"bind-address", required_argument, 0, 's'},
  182. {"port", required_argument, 0, 'p'},
  183. {"max_members", required_argument, 0, 'm'},
  184. {"password", required_argument, 0, 'w'},
  185. {"preferred-game", required_argument, 0, 'g'},
  186. {"preferred-game-id", required_argument, 0, 'i'},
  187. {"username", optional_argument, 0, 'u'},
  188. {"token", required_argument, 0, 't'},
  189. {"web-api-url", required_argument, 0, 'a'},
  190. {"ban-list-file", required_argument, 0, 'b'},
  191. {"log-file", required_argument, 0, 'l'},
  192. {"enable-yuzu-mods", no_argument, 0, 'e'},
  193. {"help", no_argument, 0, 'h'},
  194. {"version", no_argument, 0, 'v'},
  195. {0, 0, 0, 0},
  196. };
  197. InitializeLogging(log_file);
  198. while (optind < argc) {
  199. int arg =
  200. getopt_long(argc, argv, "n:d:s:p:m:w:g:u:t:a:i:l:hv", long_options, &option_index);
  201. if (arg != -1) {
  202. switch (static_cast<char>(arg)) {
  203. case 'n':
  204. room_name.assign(optarg);
  205. break;
  206. case 'd':
  207. room_description.assign(optarg);
  208. break;
  209. case 's':
  210. bind_address.assign(optarg);
  211. break;
  212. case 'p':
  213. port = strtoul(optarg, &endarg, 0);
  214. break;
  215. case 'm':
  216. max_members = strtoul(optarg, &endarg, 0);
  217. break;
  218. case 'w':
  219. password.assign(optarg);
  220. break;
  221. case 'g':
  222. preferred_game.assign(optarg);
  223. break;
  224. case 'i':
  225. preferred_game_id = strtoull(optarg, &endarg, 16);
  226. break;
  227. case 'u':
  228. username.assign(optarg);
  229. break;
  230. case 't':
  231. token.assign(optarg);
  232. break;
  233. case 'a':
  234. web_api_url.assign(optarg);
  235. break;
  236. case 'b':
  237. ban_list_file.assign(optarg);
  238. break;
  239. case 'l':
  240. log_file.assign(optarg);
  241. break;
  242. case 'e':
  243. enable_yuzu_mods = true;
  244. break;
  245. case 'h':
  246. PrintHelp(argv[0]);
  247. return 0;
  248. case 'v':
  249. PrintVersion();
  250. return 0;
  251. }
  252. }
  253. }
  254. if (room_name.empty()) {
  255. LOG_ERROR(Network, "Room name is empty!");
  256. PrintHelp(argv[0]);
  257. return -1;
  258. }
  259. if (preferred_game.empty()) {
  260. LOG_ERROR(Network, "Preferred game is empty!");
  261. PrintHelp(argv[0]);
  262. return -1;
  263. }
  264. if (preferred_game_id == 0) {
  265. LOG_ERROR(Network,
  266. "preferred-game-id not set!\nThis should get set to allow users to find your "
  267. "room.\nSet with --preferred-game-id id");
  268. }
  269. if (max_members > Network::MaxConcurrentConnections || max_members < 2) {
  270. LOG_ERROR(Network, "max_members needs to be in the range 2 - {}!",
  271. Network::MaxConcurrentConnections);
  272. PrintHelp(argv[0]);
  273. return -1;
  274. }
  275. if (bind_address.empty()) {
  276. LOG_INFO(Network, "Bind address is empty: defaulting to 0.0.0.0");
  277. }
  278. if (port > UINT16_MAX) {
  279. LOG_ERROR(Network, "Port needs to be in the range 0 - 65535!");
  280. PrintHelp(argv[0]);
  281. return -1;
  282. }
  283. if (ban_list_file.empty()) {
  284. LOG_ERROR(Network, "Ban list file not set!\nThis should get set to load and save room ban "
  285. "list.\nSet with --ban-list-file <file>");
  286. }
  287. bool announce = true;
  288. if (token.empty() && announce) {
  289. announce = false;
  290. LOG_INFO(Network, "Token is empty: Hosting a private room");
  291. }
  292. if (web_api_url.empty() && announce) {
  293. announce = false;
  294. LOG_INFO(Network, "Endpoint url is empty: Hosting a private room");
  295. }
  296. if (announce) {
  297. if (username.empty()) {
  298. LOG_INFO(Network, "Hosting a public room");
  299. Settings::values.web_api_url = web_api_url;
  300. PadToken(token);
  301. Settings::values.yuzu_username = UsernameFromDisplayToken(token);
  302. username = Settings::values.yuzu_username.GetValue();
  303. Settings::values.yuzu_token = TokenFromDisplayToken(token);
  304. } else {
  305. LOG_INFO(Network, "Hosting a public room");
  306. Settings::values.web_api_url = web_api_url;
  307. Settings::values.yuzu_username = username;
  308. Settings::values.yuzu_token = token;
  309. }
  310. }
  311. if (!announce && enable_yuzu_mods) {
  312. enable_yuzu_mods = false;
  313. LOG_INFO(Network, "Can not enable yuzu Moderators for private rooms");
  314. }
  315. // Load the ban list
  316. Network::Room::BanList ban_list;
  317. if (!ban_list_file.empty()) {
  318. ban_list = LoadBanList(ban_list_file);
  319. }
  320. std::unique_ptr<Network::VerifyUser::Backend> verify_backend;
  321. if (announce) {
  322. #ifdef ENABLE_WEB_SERVICE
  323. verify_backend =
  324. std::make_unique<WebService::VerifyUserJWT>(Settings::values.web_api_url.GetValue());
  325. #else
  326. LOG_INFO(Network,
  327. "yuzu Web Services is not available with this build: validation is disabled.");
  328. verify_backend = std::make_unique<Network::VerifyUser::NullBackend>();
  329. #endif
  330. } else {
  331. verify_backend = std::make_unique<Network::VerifyUser::NullBackend>();
  332. }
  333. Network::RoomNetwork network{};
  334. network.Init();
  335. if (auto room = network.GetRoom().lock()) {
  336. AnnounceMultiplayerRoom::GameInfo preferred_game_info{.name = preferred_game,
  337. .id = preferred_game_id};
  338. if (!room->Create(room_name, room_description, bind_address, static_cast<u16>(port),
  339. password, max_members, username, preferred_game_info,
  340. std::move(verify_backend), ban_list, enable_yuzu_mods)) {
  341. LOG_INFO(Network, "Failed to create room: ");
  342. return -1;
  343. }
  344. LOG_INFO(Network, "Room is open. Close with Q+Enter...");
  345. auto announce_session = std::make_unique<Core::AnnounceMultiplayerSession>(network);
  346. if (announce) {
  347. announce_session->Start();
  348. }
  349. while (room->GetState() == Network::Room::State::Open) {
  350. std::string in;
  351. std::cin >> in;
  352. if (in.size() > 0) {
  353. break;
  354. }
  355. std::this_thread::sleep_for(std::chrono::milliseconds(100));
  356. }
  357. if (announce) {
  358. announce_session->Stop();
  359. }
  360. announce_session.reset();
  361. // Save the ban list
  362. if (!ban_list_file.empty()) {
  363. SaveBanList(room->GetBanList(), ban_list_file);
  364. }
  365. room->Destroy();
  366. }
  367. network.Shutdown();
  368. detached_tasks.WaitForAllTasks();
  369. return 0;
  370. }