diff --git a/.gitmodules b/.gitmodules index f79819a95..b7f41e5f6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -42,4 +42,7 @@ url = https://github.com/zeromq/libzmq [submodule "externals/cppzmq"] path = externals/cppzmq - url = https://github.com/zeromq/cppzmq \ No newline at end of file + url = https://github.com/zeromq/cppzmq +[submodule "cpp-jwt"] + path = externals/cpp-jwt + url = https://github.com/arun11299/cpp-jwt.git diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index 13b347d89..b96bebb7f 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -82,6 +82,10 @@ if (ENABLE_WEB_SERVICE) target_include_directories(ssl INTERFACE ./libressl/include) target_compile_definitions(ssl PRIVATE -DHAVE_INET_NTOP) + # JSON + add_library(json-headers INTERFACE) + target_include_directories(json-headers INTERFACE ./json) + # lurlparser add_subdirectory(lurlparser EXCLUDE_FROM_ALL) @@ -89,9 +93,9 @@ if (ENABLE_WEB_SERVICE) add_library(httplib INTERFACE) target_include_directories(httplib INTERFACE ./httplib) - # JSON - add_library(json-headers INTERFACE) - target_include_directories(json-headers INTERFACE ./json) + # cpp-jwt + add_library(cpp-jwt INTERFACE) + target_include_directories(cpp-jwt INTERFACE ./cpp-jwt/include) endif() if (ENABLE_SCRIPTING) diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt index f72da9439..b98cf1654 100644 --- a/src/network/CMakeLists.txt +++ b/src/network/CMakeLists.txt @@ -7,8 +7,10 @@ add_library(network STATIC room.h room_member.cpp room_member.h + verify_user.cpp + verify_user.h ) create_target_directory_groups(network) -target_link_libraries(network PRIVATE common enet) +target_link_libraries(network PRIVATE common cpp-jwt enet) diff --git a/src/network/room.cpp b/src/network/room.cpp index c5b960925..cae4a7258 100644 --- a/src/network/room.cpp +++ b/src/network/room.cpp @@ -14,6 +14,7 @@ #include "enet/enet.h" #include "network/packet.h" #include "network/room.h" +#include "network/verify_user.h" namespace Network { @@ -28,6 +29,9 @@ public: std::atomic state{State::Closed}; ///< Current state of the room. RoomInformation room_information; ///< Information about this room. + std::string verify_UID; ///< A GUID which may be used for verfication. + mutable std::mutex verify_UID_mutex; ///< Mutex for verify_UID + std::string password; ///< The password required to connect to this room. struct Member { @@ -35,7 +39,9 @@ public: std::string console_id_hash; ///< A hash of the console ID of the member. GameInfo game_info; ///< The current game of the member MacAddress mac_address; ///< The assigned mac address of the member. - ENetPeer* peer; ///< The remote peer. + /// Data of the user, often including authenticated forum username. + VerifyUser::UserData user_data; + ENetPeer* peer; ///< The remote peer. }; using MemberList = std::vector; MemberList members; ///< Information about the members of this room @@ -48,6 +54,9 @@ public: /// Thread that receives and dispatches network packets std::unique_ptr room_thread; + /// Verification backend of the room + std::unique_ptr verify_backend; + /// Thread function that will receive and dispatch messages until the room is destroyed. void ServerLoop(); void StartLoop(); @@ -165,11 +174,6 @@ public: * to all other clients. */ void HandleClientDisconnection(ENetPeer* client); - - /** - * Creates a random ID in the form 12345678-1234-1234-1234-123456789012 - */ - void CreateUniqueID(); }; // RoomImpl @@ -238,6 +242,9 @@ void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { std::string pass; packet >> pass; + std::string token; + packet >> token; + if (pass != password) { SendWrongPassword(event->peer); return; @@ -276,6 +283,13 @@ void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { member.nickname = nickname; member.peer = event->peer; + std::string uid; + { + std::lock_guard lock(verify_UID_mutex); + uid = verify_UID; + } + member.user_data = verify_backend->LoadUserData(uid, token); + { std::lock_guard lock(member_mutex); members.push_back(std::move(member)); @@ -407,7 +421,6 @@ void Room::RoomImpl::BroadcastRoomInformation() { packet << room_information.name; packet << room_information.description; packet << room_information.member_slots; - packet << room_information.uid; packet << room_information.port; packet << room_information.preferred_game; @@ -419,6 +432,9 @@ void Room::RoomImpl::BroadcastRoomInformation() { packet << member.mac_address; packet << member.game_info.name; packet << member.game_info.id; + packet << member.user_data.username; + packet << member.user_data.display_name; + packet << member.user_data.avatar_url; } } @@ -511,6 +527,7 @@ void Room::RoomImpl::HandleChatPacket(const ENetEvent* event) { Packet out_packet; out_packet << static_cast(IdChatMessage); out_packet << sending_member->nickname; + out_packet << sending_member->user_data.username; out_packet << message; ENetPacket* enet_packet = enet_packet_create(out_packet.GetData(), out_packet.GetDataSize(), @@ -567,20 +584,6 @@ void Room::RoomImpl::HandleClientDisconnection(ENetPeer* client) { BroadcastRoomInformation(); } -void Room::RoomImpl::CreateUniqueID() { - std::uniform_int_distribution<> dis(0, 9999); - std::ostringstream stream; - stream << std::setfill('0') << std::setw(4) << dis(random_gen); - stream << std::setfill('0') << std::setw(4) << dis(random_gen) << "-"; - stream << std::setfill('0') << std::setw(4) << dis(random_gen) << "-"; - stream << std::setfill('0') << std::setw(4) << dis(random_gen) << "-"; - stream << std::setfill('0') << std::setw(4) << dis(random_gen) << "-"; - stream << std::setfill('0') << std::setw(4) << dis(random_gen); - stream << std::setfill('0') << std::setw(4) << dis(random_gen); - stream << std::setfill('0') << std::setw(4) << dis(random_gen); - room_information.uid = stream.str(); -} - // Room Room::Room() : room_impl{std::make_unique()} {} @@ -589,7 +592,7 @@ Room::~Room() = default; bool Room::Create(const std::string& name, const std::string& description, const std::string& server_address, u16 server_port, const std::string& password, const u32 max_connections, const std::string& preferred_game, - u64 preferred_game_id) { + u64 preferred_game_id, std::unique_ptr verify_backend) { ENetAddress address; address.host = ENET_HOST_ANY; if (!server_address.empty()) { @@ -597,8 +600,8 @@ bool Room::Create(const std::string& name, const std::string& description, } address.port = server_port; - // In order to send the room is full message to the connecting client, we need to leave one slot - // open so enet won't reject the incoming connection without telling us + // In order to send the room is full message to the connecting client, we need to leave one + // slot open so enet won't reject the incoming connection without telling us room_impl->server = enet_host_create(&address, max_connections + 1, NumChannels, 0, 0); if (!room_impl->server) { return false; @@ -612,7 +615,7 @@ bool Room::Create(const std::string& name, const std::string& description, room_impl->room_information.preferred_game = preferred_game; room_impl->room_information.preferred_game_id = preferred_game_id; room_impl->password = password; - room_impl->CreateUniqueID(); + room_impl->verify_backend = std::move(verify_backend); room_impl->StartLoop(); return true; @@ -626,12 +629,20 @@ const RoomInformation& Room::GetRoomInformation() const { return room_impl->room_information; } +std::string Room::GetVerifyUID() const { + std::lock_guard lock(room_impl->verify_UID_mutex); + return room_impl->verify_UID; +} + std::vector Room::GetRoomMemberList() const { std::vector member_list; std::lock_guard lock(room_impl->member_mutex); for (const auto& member_impl : room_impl->members) { Member member; member.nickname = member_impl.nickname; + member.username = member_impl.user_data.username; + member.display_name = member_impl.user_data.display_name; + member.avatar_url = member_impl.user_data.avatar_url; member.mac_address = member_impl.mac_address; member.game_info = member_impl.game_info; member_list.push_back(member); @@ -643,6 +654,11 @@ bool Room::HasPassword() const { return !room_impl->password.empty(); } +void Room::SetVerifyUID(const std::string& uid) { + std::lock_guard lock(room_impl->verify_UID_mutex); + room_impl->verify_UID = uid; +} + void Room::Destroy() { room_impl->state = State::Closed; room_impl->room_thread->join(); diff --git a/src/network/room.h b/src/network/room.h index 9b2889a0c..d1a26e62d 100644 --- a/src/network/room.h +++ b/src/network/room.h @@ -9,6 +9,7 @@ #include #include #include "common/common_types.h" +#include "network/verify_user.h" namespace Network { @@ -27,7 +28,6 @@ struct RoomInformation { std::string name; ///< Name of the server std::string description; ///< Server description u32 member_slots; ///< Maximum number of members in this room - std::string uid; ///< The unique ID of the room u16 port; ///< The port of this room std::string preferred_game; ///< Game to advertise that you want to play u64 preferred_game_id; ///< Title ID for the advertised game @@ -72,9 +72,12 @@ public: }; struct Member { - std::string nickname; ///< The nickname of the member. - GameInfo game_info; ///< The current game of the member - MacAddress mac_address; ///< The assigned mac address of the member. + std::string nickname; ///< The nickname of the member. + std::string username; ///< The web services username of the member. Can be empty. + std::string display_name; ///< The web services display name of the member. Can be empty. + std::string avatar_url; ///< Url to the member's avatar. Can be empty. + GameInfo game_info; ///< The current game of the member + MacAddress mac_address; ///< The assigned mac address of the member. }; Room(); @@ -90,6 +93,11 @@ public: */ const RoomInformation& GetRoomInformation() const; + /** + * Gets the verify UID of this room. + */ + std::string GetVerifyUID() const; + /** * Gets a list of the mbmers connected to the room. */ @@ -108,7 +116,13 @@ public: const std::string& server = "", u16 server_port = DefaultRoomPort, const std::string& password = "", const u32 max_connections = MaxConcurrentConnections, - const std::string& preferred_game = "", u64 preferred_game_id = 0); + const std::string& preferred_game = "", u64 preferred_game_id = 0, + std::unique_ptr verify_backend = nullptr); + + /** + * Sets the verification GUID of the room. + */ + void SetVerifyUID(const std::string& uid); /** * Destroys the socket diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp index 14fc201f5..8fe67c086 100644 --- a/src/network/room_member.cpp +++ b/src/network/room_member.cpp @@ -33,7 +33,11 @@ public: void SetState(const State new_state); bool IsConnected() const; - std::string nickname; ///< The nickname of this member. + std::string nickname; ///< The nickname of this member. + + std::string username; ///< The username of this member. + mutable std::mutex username_mutex; ///< Mutex for locking username. + MacAddress mac_address; ///< The mac_address of this member. std::mutex network_mutex; ///< Mutex that controls access to the `client` variable. @@ -80,7 +84,7 @@ public: */ void SendJoinRequest(const std::string& nickname, const std::string& console_id_hash, const MacAddress& preferred_mac = NoPreferredMac, - const std::string& password = ""); + const std::string& password = "", const std::string& token = ""); /** * Extracts a MAC Address from a received ENet packet. @@ -210,7 +214,8 @@ void RoomMember::RoomMemberImpl::Send(Packet&& packet) { void RoomMember::RoomMemberImpl::SendJoinRequest(const std::string& nickname, const std::string& console_id_hash, const MacAddress& preferred_mac, - const std::string& password) { + const std::string& password, + const std::string& token) { Packet packet; packet << static_cast(IdJoinRequest); packet << nickname; @@ -218,6 +223,7 @@ void RoomMember::RoomMemberImpl::SendJoinRequest(const std::string& nickname, packet << preferred_mac; packet << network_version; packet << password; + packet << token; Send(std::move(packet)); } @@ -232,7 +238,6 @@ void RoomMember::RoomMemberImpl::HandleRoomInformationPacket(const ENetEvent* ev packet >> info.name; packet >> info.description; packet >> info.member_slots; - packet >> info.uid; packet >> info.port; packet >> info.preferred_game; room_information.name = info.name; @@ -250,6 +255,16 @@ void RoomMember::RoomMemberImpl::HandleRoomInformationPacket(const ENetEvent* ev packet >> member.mac_address; packet >> member.game_info.name; packet >> member.game_info.id; + packet >> member.username; + packet >> member.display_name; + packet >> member.avatar_url; + + { + std::lock_guard lock(username_mutex); + if (member.nickname == nickname) { + username = member.username; + } + } } Invoke(room_information); } @@ -297,6 +312,7 @@ void RoomMember::RoomMemberImpl::HandleChatPacket(const ENetEvent* event) { ChatEntry chat_entry{}; packet >> chat_entry.nickname; + packet >> chat_entry.username; packet >> chat_entry.message; Invoke(chat_entry); } @@ -391,6 +407,11 @@ const std::string& RoomMember::GetNickname() const { return room_member_impl->nickname; } +const std::string& RoomMember::GetUsername() const { + std::lock_guard lock(room_member_impl->username_mutex); + return room_member_impl->username; +} + const MacAddress& RoomMember::GetMacAddress() const { ASSERT_MSG(IsConnected(), "Tried to get MAC address while not connected"); return room_member_impl->mac_address; @@ -402,7 +423,8 @@ RoomInformation RoomMember::GetRoomInformation() const { void RoomMember::Join(const std::string& nick, const std::string& console_id_hash, const char* server_addr, u16 server_port, u16 client_port, - const MacAddress& preferred_mac, const std::string& password) { + const MacAddress& preferred_mac, const std::string& password, + const std::string& token) { // If the member is connected, kill the connection first if (room_member_impl->loop_thread && room_member_impl->loop_thread->joinable()) { Leave(); @@ -435,7 +457,7 @@ void RoomMember::Join(const std::string& nick, const std::string& console_id_has if (net > 0 && event.type == ENET_EVENT_TYPE_CONNECT) { room_member_impl->nickname = nick; room_member_impl->StartLoop(); - room_member_impl->SendJoinRequest(nick, console_id_hash, preferred_mac, password); + room_member_impl->SendJoinRequest(nick, console_id_hash, preferred_mac, password, token); SendGameInfo(room_member_impl->current_game_info); } else { enet_peer_disconnect(room_member_impl->server, 0); diff --git a/src/network/room_member.h b/src/network/room_member.h index 58fca96b7..4329263aa 100644 --- a/src/network/room_member.h +++ b/src/network/room_member.h @@ -35,7 +35,9 @@ struct WifiPacket { /// Represents a chat message. struct ChatEntry { std::string nickname; ///< Nickname of the client who sent this message. - std::string message; ///< Body of the message. + /// Web services username of the client who sent this message, can be empty. + std::string username; + std::string message; ///< Body of the message. }; /** @@ -64,7 +66,10 @@ public: }; struct MemberInformation { - std::string nickname; ///< Nickname of the member. + std::string nickname; ///< Nickname of the member. + std::string username; ///< The web services username of the member. Can be empty. + std::string display_name; ///< The web services display name of the member. Can be empty. + std::string avatar_url; ///< Url to the member's avatar. Can be empty. GameInfo game_info; ///< Name of the game they're currently playing, or empty if they're /// not playing anything. MacAddress mac_address; ///< MAC address associated with this member. @@ -100,6 +105,11 @@ public: */ const std::string& GetNickname() const; + /** + * Returns the username of the RoomMember. + */ + const std::string& GetUsername() const; + /** * Returns the MAC address of the RoomMember. */ @@ -123,7 +133,7 @@ public: void Join(const std::string& nickname, const std::string& console_id_hash, const char* server_addr = "127.0.0.1", const u16 server_port = DefaultRoomPort, const u16 client_port = 0, const MacAddress& preferred_mac = NoPreferredMac, - const std::string& password = ""); + const std::string& password = "", const std::string& token = ""); /** * Sends a WiFi packet to the room. diff --git a/src/network/verify_user.cpp b/src/network/verify_user.cpp new file mode 100644 index 000000000..d9d98e495 --- /dev/null +++ b/src/network/verify_user.cpp @@ -0,0 +1,18 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "network/verify_user.h" + +namespace Network::VerifyUser { + +Backend::~Backend() = default; + +NullBackend::~NullBackend() = default; + +UserData NullBackend::LoadUserData([[maybe_unused]] const std::string& verify_UID, + [[maybe_unused]] const std::string& token) { + return {}; +} + +} // namespace Network::VerifyUser diff --git a/src/network/verify_user.h b/src/network/verify_user.h new file mode 100644 index 000000000..74e154331 --- /dev/null +++ b/src/network/verify_user.h @@ -0,0 +1,45 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "common/logging/log.h" + +namespace Network::VerifyUser { + +struct UserData { + std::string username; + std::string display_name; + std::string avatar_url; +}; + +/** + * A backend used for verifying users and loading user data. + */ +class Backend { +public: + virtual ~Backend(); + + /** + * Verifies the given token and loads the information into a UserData struct. + * @param verify_UID A GUID that may be used for verification. + * @param token A token that contains user data and verification data. The format and content is + * decided by backends. + */ + virtual UserData LoadUserData(const std::string& verify_UID, const std::string& token) = 0; +}; + +/** + * A null backend where the token is ignored. + * No verification is performed here and the function returns an empty UserData. + */ +class NullBackend final : public Backend { +public: + ~NullBackend(); + + UserData LoadUserData(const std::string& verify_UID, const std::string& token) override; +}; + +} // namespace Network::VerifyUser diff --git a/src/web_service/CMakeLists.txt b/src/web_service/CMakeLists.txt index 71bf8977d..c3d42fe8b 100644 --- a/src/web_service/CMakeLists.txt +++ b/src/web_service/CMakeLists.txt @@ -5,6 +5,8 @@ add_library(web_service STATIC telemetry_json.h verify_login.cpp verify_login.h + verify_user_jwt.cpp + verify_user_jwt.h web_backend.cpp web_backend.h ) @@ -15,4 +17,4 @@ get_directory_property(OPENSSL_LIBS DIRECTORY ${PROJECT_SOURCE_DIR}/externals/libressl DEFINITION OPENSSL_LIBS) target_compile_definitions(web_service PRIVATE -DCPPHTTPLIB_OPENSSL_SUPPORT) -target_link_libraries(web_service PRIVATE common json-headers ${OPENSSL_LIBS} httplib lurlparser) +target_link_libraries(web_service PRIVATE common network json-headers ${OPENSSL_LIBS} httplib lurlparser cpp-jwt) diff --git a/src/web_service/verify_user_jwt.cpp b/src/web_service/verify_user_jwt.cpp new file mode 100644 index 000000000..50eeb8e8e --- /dev/null +++ b/src/web_service/verify_user_jwt.cpp @@ -0,0 +1,56 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "common/logging/log.h" +#include "common/web_result.h" +#include "web_service/verify_user_jwt.h" +#include "web_service/web_backend.h" + +namespace WebService { + +static std::string public_key; +std::string GetPublicKey(const std::string& host) { + if (public_key.empty()) { + Client client(host, "", ""); // no need for credentials here + public_key = client.GetJson("/jwt/external/key.pem", true).returned_data; + if (public_key.empty()) { + LOG_ERROR(WebService, "Could not fetch external JWT public key, verification may fail"); + } else { + LOG_INFO(WebService, "Fetched external JWT public key (size={})", public_key.size()); + } + } + return public_key; +} + +VerifyUserJWT::VerifyUserJWT(const std::string& host) : pub_key(GetPublicKey(host)) {} + +Network::VerifyUser::UserData VerifyUserJWT::LoadUserData(const std::string& verify_UID, + const std::string& token) { + const std::string audience = fmt::format("external-{}", verify_UID); + using namespace jwt::params; + std::error_code error; + auto decoded = + jwt::decode(token, algorithms({"rs256"}), error, secret(pub_key), issuer("citra-core"), + aud(audience), validate_iat(true), validate_jti(true)); + if (error) { + LOG_INFO(WebService, "Verification failed: category={}, code={}, message={}", + error.category().name(), error.value(), error.message()); + return {}; + } + Network::VerifyUser::UserData user_data{}; + if (decoded.payload().has_claim("username")) { + user_data.username = decoded.payload().get_claim_value("username"); + } + if (decoded.payload().has_claim("displayName")) { + user_data.display_name = decoded.payload().get_claim_value("displayName"); + } + if (decoded.payload().has_claim("avatarUrl")) { + user_data.avatar_url = decoded.payload().get_claim_value("avatarUrl"); + } + return user_data; +} + +} // namespace WebService diff --git a/src/web_service/verify_user_jwt.h b/src/web_service/verify_user_jwt.h new file mode 100644 index 000000000..826e01eed --- /dev/null +++ b/src/web_service/verify_user_jwt.h @@ -0,0 +1,25 @@ +// Copyright 2018 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "network/verify_user.h" +#include "web_service/web_backend.h" + +namespace WebService { + +class VerifyUserJWT final : public Network::VerifyUser::Backend { +public: + VerifyUserJWT(const std::string& host); + ~VerifyUserJWT() = default; + + Network::VerifyUser::UserData LoadUserData(const std::string& verify_UID, + const std::string& token) override; + +private: + std::string pub_key; +}; + +} // namespace WebService