mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-30 21:30:04 +00:00 
			
		
		
		
	web_service: Change authentication system to use JWT (#4041)
* Change authentication system to JWT * Address review comments * Get rid of global variable, fix some documentations, fix a bug when verificating * Refactor PostJson to avoid code duplication * Rename jwt_token, add functionality to request a new JWT when getting a 401 * Take bools by value instead of const reference * Send request again when JWT is invalid and use forward declarations * Omit brackets
This commit is contained in:
		
							parent
							
								
									b49d042200
								
							
						
					
					
						commit
						604c1b5fc3
					
				
					 6 changed files with 202 additions and 85 deletions
				
			
		|  | @ -84,7 +84,7 @@ void RoomJson::AddPlayer(const std::string& nickname, | |||
| 
 | ||||
| std::future<Common::WebResult> RoomJson::Announce() { | ||||
|     nlohmann::json json = room; | ||||
|     return PostJson(endpoint_url, json.dump(), false, username, token); | ||||
|     return PostJson(endpoint_url, json.dump(), false); | ||||
| } | ||||
| 
 | ||||
| void RoomJson::ClearPlayers() { | ||||
|  | @ -99,14 +99,13 @@ std::future<AnnounceMultiplayerRoom::RoomList> RoomJson::GetRoomList(std::functi | |||
|         func(); | ||||
|         return room_list; | ||||
|     }; | ||||
|     return GetJson<AnnounceMultiplayerRoom::RoomList>(DeSerialize, endpoint_url, true, username, | ||||
|                                                       token); | ||||
|     return GetJson<AnnounceMultiplayerRoom::RoomList>(DeSerialize, endpoint_url, true); | ||||
| } | ||||
| 
 | ||||
| void RoomJson::Delete() { | ||||
|     nlohmann::json json; | ||||
|     json["id"] = room.UID; | ||||
|     DeleteJson(endpoint_url, json.dump(), username, token); | ||||
|     DeleteJson(endpoint_url, json.dump()); | ||||
| } | ||||
| 
 | ||||
| } // namespace WebService
 | ||||
|  |  | |||
|  | @ -82,7 +82,7 @@ void TelemetryJson::Complete() { | |||
|     SerializeSection(Telemetry::FieldType::UserSystem, "UserSystem"); | ||||
| 
 | ||||
|     // Send the telemetry async but don't handle the errors since they were written to the log
 | ||||
|     future = PostJson(endpoint_url, TopSection().dump(), true, username, token); | ||||
|     future = PostJson(endpoint_url, TopSection().dump(), true); | ||||
| } | ||||
| 
 | ||||
| } // namespace WebService
 | ||||
|  |  | |||
|  | @ -26,7 +26,8 @@ std::future<bool> VerifyLogin(std::string& username, std::string& token, | |||
| 
 | ||||
|         return username == *iter; | ||||
|     }; | ||||
|     return GetJson<bool>(get_func, endpoint_url, false, username, token); | ||||
|     UpdateCoreJWT(true, username, token); | ||||
|     return GetJson<bool>(get_func, endpoint_url, false); | ||||
| } | ||||
| 
 | ||||
| } // namespace WebService
 | ||||
|  |  | |||
|  | @ -6,9 +6,9 @@ | |||
| #include <string> | ||||
| #include <thread> | ||||
| #include <LUrlParser.h> | ||||
| #include <httplib.h> | ||||
| #include "common/announce_multiplayer_room.h" | ||||
| #include "common/logging/log.h" | ||||
| #include "core/settings.h" | ||||
| #include "web_service/web_backend.h" | ||||
| 
 | ||||
| namespace WebService { | ||||
|  | @ -20,6 +20,19 @@ constexpr int HTTPS_PORT = 443; | |||
| 
 | ||||
| constexpr int TIMEOUT_SECONDS = 30; | ||||
| 
 | ||||
| std::string UpdateCoreJWT(bool force_new_token, const std::string& username, | ||||
|                           const std::string& token) { | ||||
|     static std::string jwt; | ||||
|     if (jwt.empty() || force_new_token) { | ||||
|         if (!username.empty() && !token.empty()) { | ||||
|             std::future<Common::WebResult> future = | ||||
|                 PostJson("https://api.citra-emu.org/jwt/internal", username, token); | ||||
|             jwt = future.get().returned_data; | ||||
|         } | ||||
|     } | ||||
|     return jwt; | ||||
| } | ||||
| 
 | ||||
| std::unique_ptr<httplib::Client> GetClientFor(const LUrlParser::clParseURL& parsedUrl) { | ||||
|     namespace hl = httplib; | ||||
| 
 | ||||
|  | @ -43,8 +56,102 @@ std::unique_ptr<httplib::Client> GetClientFor(const LUrlParser::clParseURL& pars | |||
|     } | ||||
| } | ||||
| 
 | ||||
| static Common::WebResult PostJsonAsyncFn(const std::string& url, | ||||
|                                          const LUrlParser::clParseURL& parsed_url, | ||||
|                                          const httplib::Headers& params, const std::string& data, | ||||
|                                          bool is_jwt_requested) { | ||||
|     static bool is_first_attempt = true; | ||||
| 
 | ||||
|     namespace hl = httplib; | ||||
|     std::unique_ptr<hl::Client> cli = GetClientFor(parsed_url); | ||||
| 
 | ||||
|     if (cli == nullptr) { | ||||
|         return Common::WebResult{Common::WebResult::Code::InvalidURL, "URL is invalid"}; | ||||
|     } | ||||
| 
 | ||||
|     hl::Request request; | ||||
|     request.method = "POST"; | ||||
|     request.path = "/" + parsed_url.m_Path; | ||||
|     request.headers = params; | ||||
|     request.body = data; | ||||
| 
 | ||||
|     hl::Response response; | ||||
| 
 | ||||
|     if (!cli->send(request, response)) { | ||||
|         LOG_ERROR(WebService, "POST to {} returned null", url); | ||||
|         return Common::WebResult{Common::WebResult::Code::LibError, "Null response"}; | ||||
|     } | ||||
| 
 | ||||
|     if (response.status >= 400) { | ||||
|         LOG_ERROR(WebService, "POST to {} returned error status code: {}", url, response.status); | ||||
|         if (response.status == 401 && !is_jwt_requested && is_first_attempt) { | ||||
|             LOG_WARNING(WebService, "Requesting new JWT"); | ||||
|             UpdateCoreJWT(true, Settings::values.citra_username, Settings::values.citra_token); | ||||
|             is_first_attempt = false; | ||||
|             PostJsonAsyncFn(url, parsed_url, params, data, is_jwt_requested); | ||||
|             is_first_attempt = true; | ||||
|         } | ||||
|         return Common::WebResult{Common::WebResult::Code::HttpError, | ||||
|                                  std::to_string(response.status)}; | ||||
|     } | ||||
| 
 | ||||
|     auto content_type = response.headers.find("content-type"); | ||||
| 
 | ||||
|     if (content_type == response.headers.end() || | ||||
|         (content_type->second.find("application/json") == std::string::npos && | ||||
|          content_type->second.find("text/html; charset=utf-8") == std::string::npos)) { | ||||
|         LOG_ERROR(WebService, "POST to {} returned wrong content: {}", url, content_type->second); | ||||
|         return Common::WebResult{Common::WebResult::Code::WrongContent, ""}; | ||||
|     } | ||||
| 
 | ||||
|     return Common::WebResult{Common::WebResult::Code::Success, "", response.body}; | ||||
| } | ||||
| 
 | ||||
| std::future<Common::WebResult> PostJson(const std::string& url, const std::string& data, | ||||
|                                         bool allow_anonymous, const std::string& username, | ||||
|                                         bool allow_anonymous) { | ||||
| 
 | ||||
|     using lup = LUrlParser::clParseURL; | ||||
|     namespace hl = httplib; | ||||
| 
 | ||||
|     lup parsedUrl = lup::ParseURL(url); | ||||
| 
 | ||||
|     if (url.empty() || !parsedUrl.IsValid()) { | ||||
|         LOG_ERROR(WebService, "URL is invalid"); | ||||
|         return std::async(std::launch::deferred, [] { | ||||
|             return Common::WebResult{Common::WebResult::Code::InvalidURL, "URL is invalid"}; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     const std::string jwt = | ||||
|         UpdateCoreJWT(false, Settings::values.citra_username, Settings::values.citra_token); | ||||
| 
 | ||||
|     const bool are_credentials_provided{!jwt.empty()}; | ||||
|     if (!allow_anonymous && !are_credentials_provided) { | ||||
|         LOG_ERROR(WebService, "Credentials must be provided for authenticated requests"); | ||||
|         return std::async(std::launch::deferred, [] { | ||||
|             return Common::WebResult{Common::WebResult::Code::CredentialsMissing, | ||||
|                                      "Credentials needed"}; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     // Built request header
 | ||||
|     hl::Headers params; | ||||
|     if (are_credentials_provided) { | ||||
|         // Authenticated request if credentials are provided
 | ||||
|         params = {{std::string("Authorization"), fmt::format("Bearer {}", jwt)}, | ||||
|                   {std::string("api-version"), std::string(API_VERSION)}, | ||||
|                   {std::string("Content-Type"), std::string("application/json")}}; | ||||
|     } else { | ||||
|         // Otherwise, anonymous request
 | ||||
|         params = {{std::string("api-version"), std::string(API_VERSION)}, | ||||
|                   {std::string("Content-Type"), std::string("application/json")}}; | ||||
|     } | ||||
| 
 | ||||
|     // Post JSON asynchronously
 | ||||
|     return std::async(std::launch::async, PostJsonAsyncFn, url, parsedUrl, params, data, false); | ||||
| } | ||||
| 
 | ||||
| std::future<Common::WebResult> PostJson(const std::string& url, const std::string& username, | ||||
|                                         const std::string& token) { | ||||
|     using lup = LUrlParser::clParseURL; | ||||
|     namespace hl = httplib; | ||||
|  | @ -53,17 +160,16 @@ std::future<Common::WebResult> PostJson(const std::string& url, const std::strin | |||
| 
 | ||||
|     if (url.empty() || !parsedUrl.IsValid()) { | ||||
|         LOG_ERROR(WebService, "URL is invalid"); | ||||
|         return std::async(std::launch::deferred, []() { | ||||
|             return Common::WebResult{Common::WebResult::Code::InvalidURL, "URL is invalid"}; | ||||
|         return std::async(std::launch::deferred, [] { | ||||
|             return Common::WebResult{Common::WebResult::Code::InvalidURL, ""}; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     const bool are_credentials_provided{!token.empty() && !username.empty()}; | ||||
|     if (!allow_anonymous && !are_credentials_provided) { | ||||
|     if (!are_credentials_provided) { | ||||
|         LOG_ERROR(WebService, "Credentials must be provided for authenticated requests"); | ||||
|         return std::async(std::launch::deferred, []() { | ||||
|             return Common::WebResult{Common::WebResult::Code::CredentialsMissing, | ||||
|                                      "Credentials needed"}; | ||||
|         return std::async(std::launch::deferred, [] { | ||||
|             return Common::WebResult{Common::WebResult::Code::CredentialsMissing, ""}; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|  | @ -82,50 +188,14 @@ std::future<Common::WebResult> PostJson(const std::string& url, const std::strin | |||
|     } | ||||
| 
 | ||||
|     // Post JSON asynchronously
 | ||||
|     return std::async(std::launch::async, [url, parsedUrl, params, data] { | ||||
|         std::unique_ptr<hl::Client> cli = GetClientFor(parsedUrl); | ||||
| 
 | ||||
|         if (cli == nullptr) { | ||||
|             return Common::WebResult{Common::WebResult::Code::InvalidURL, "URL is invalid"}; | ||||
|         } | ||||
| 
 | ||||
|         hl::Request request; | ||||
|         request.method = "POST"; | ||||
|         request.path = "/" + parsedUrl.m_Path; | ||||
|         request.headers = params; | ||||
|         request.body = data; | ||||
| 
 | ||||
|         hl::Response response; | ||||
| 
 | ||||
|         if (!cli->send(request, response)) { | ||||
|             LOG_ERROR(WebService, "POST to {} returned null", url); | ||||
|             return Common::WebResult{Common::WebResult::Code::LibError, "Null response"}; | ||||
|         } | ||||
| 
 | ||||
|         if (response.status >= 400) { | ||||
|             LOG_ERROR(WebService, "POST to {} returned error status code: {}", url, | ||||
|                       response.status); | ||||
|             return Common::WebResult{Common::WebResult::Code::HttpError, | ||||
|                                      std::to_string(response.status)}; | ||||
|         } | ||||
| 
 | ||||
|         auto content_type = response.headers.find("content-type"); | ||||
| 
 | ||||
|         if (content_type == response.headers.end() || | ||||
|             content_type->second.find("application/json") == std::string::npos) { | ||||
|             LOG_ERROR(WebService, "POST to {} returned wrong content: {}", url, | ||||
|                       content_type->second); | ||||
|             return Common::WebResult{Common::WebResult::Code::WrongContent, content_type->second}; | ||||
|         } | ||||
| 
 | ||||
|         return Common::WebResult{Common::WebResult::Code::Success, ""}; | ||||
|     }); | ||||
|     return std::async(std::launch::async, PostJsonAsyncFn, url, parsedUrl, params, "", true); | ||||
| } | ||||
| 
 | ||||
| template <typename T> | ||||
| std::future<T> GetJson(std::function<T(const std::string&)> func, const std::string& url, | ||||
|                        bool allow_anonymous, const std::string& username, | ||||
|                        const std::string& token) { | ||||
|                        bool allow_anonymous) { | ||||
|     static bool is_first_attempt = true; | ||||
| 
 | ||||
|     using lup = LUrlParser::clParseURL; | ||||
|     namespace hl = httplib; | ||||
| 
 | ||||
|  | @ -136,7 +206,10 @@ std::future<T> GetJson(std::function<T(const std::string&)> func, const std::str | |||
|         return std::async(std::launch::deferred, [func{std::move(func)}]() { return func(""); }); | ||||
|     } | ||||
| 
 | ||||
|     const bool are_credentials_provided{!token.empty() && !username.empty()}; | ||||
|     const std::string jwt = | ||||
|         UpdateCoreJWT(false, Settings::values.citra_username, Settings::values.citra_token); | ||||
| 
 | ||||
|     const bool are_credentials_provided{!jwt.empty()}; | ||||
|     if (!allow_anonymous && !are_credentials_provided) { | ||||
|         LOG_ERROR(WebService, "Credentials must be provided for authenticated requests"); | ||||
|         return std::async(std::launch::deferred, [func{std::move(func)}]() { return func(""); }); | ||||
|  | @ -145,9 +218,7 @@ std::future<T> GetJson(std::function<T(const std::string&)> func, const std::str | |||
|     // Built request header
 | ||||
|     hl::Headers params; | ||||
|     if (are_credentials_provided) { | ||||
|         // Authenticated request if credentials are provided
 | ||||
|         params = {{std::string("x-username"), username}, | ||||
|                   {std::string("x-token"), token}, | ||||
|         params = {{std::string("Authorization"), fmt::format("Bearer {}", jwt)}, | ||||
|                   {std::string("api-version"), std::string(API_VERSION)}}; | ||||
|     } else { | ||||
|         // Otherwise, anonymous request
 | ||||
|  | @ -155,7 +226,7 @@ std::future<T> GetJson(std::function<T(const std::string&)> func, const std::str | |||
|     } | ||||
| 
 | ||||
|     // Get JSON asynchronously
 | ||||
|     return std::async(std::launch::async, [func, url, parsedUrl, params] { | ||||
|     return std::async(std::launch::async, [func, url, parsedUrl, params, allow_anonymous] { | ||||
|         std::unique_ptr<hl::Client> cli = GetClientFor(parsedUrl); | ||||
| 
 | ||||
|         if (cli == nullptr) { | ||||
|  | @ -176,6 +247,13 @@ std::future<T> GetJson(std::function<T(const std::string&)> func, const std::str | |||
| 
 | ||||
|         if (response.status >= 400) { | ||||
|             LOG_ERROR(WebService, "GET to {} returned error status code: {}", url, response.status); | ||||
|             if (response.status == 401 && is_first_attempt) { | ||||
|                 LOG_WARNING(WebService, "Requesting new JWT"); | ||||
|                 UpdateCoreJWT(true, Settings::values.citra_username, Settings::values.citra_token); | ||||
|                 is_first_attempt = false; | ||||
|                 GetJson(func, url, allow_anonymous); | ||||
|                 is_first_attempt = true; | ||||
|             } | ||||
|             return func(""); | ||||
|         } | ||||
| 
 | ||||
|  | @ -193,15 +271,14 @@ std::future<T> GetJson(std::function<T(const std::string&)> func, const std::str | |||
| } | ||||
| 
 | ||||
| template std::future<bool> GetJson(std::function<bool(const std::string&)> func, | ||||
|                                    const std::string& url, bool allow_anonymous, | ||||
|                                    const std::string& username, const std::string& token); | ||||
|                                    const std::string& url, bool allow_anonymous); | ||||
| template std::future<AnnounceMultiplayerRoom::RoomList> GetJson( | ||||
|     std::function<AnnounceMultiplayerRoom::RoomList(const std::string&)> func, | ||||
|     const std::string& url, bool allow_anonymous, const std::string& username, | ||||
|     const std::string& token); | ||||
|     const std::string& url, bool allow_anonymous); | ||||
| 
 | ||||
| void DeleteJson(const std::string& url, const std::string& data) { | ||||
|     static bool is_first_attempt = true; | ||||
| 
 | ||||
| void DeleteJson(const std::string& url, const std::string& data, const std::string& username, | ||||
|                 const std::string& token) { | ||||
|     using lup = LUrlParser::clParseURL; | ||||
|     namespace hl = httplib; | ||||
| 
 | ||||
|  | @ -212,15 +289,17 @@ void DeleteJson(const std::string& url, const std::string& data, const std::stri | |||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     const bool are_credentials_provided{!token.empty() && !username.empty()}; | ||||
|     const std::string jwt = | ||||
|         UpdateCoreJWT(false, Settings::values.citra_username, Settings::values.citra_token); | ||||
| 
 | ||||
|     const bool are_credentials_provided{!jwt.empty()}; | ||||
|     if (!are_credentials_provided) { | ||||
|         LOG_ERROR(WebService, "Credentials must be provided for authenticated requests"); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     // Built request header
 | ||||
|     hl::Headers params = {{std::string("x-username"), username}, | ||||
|                           {std::string("x-token"), token}, | ||||
|     hl::Headers params = {{std::string("Authorization"), fmt::format("Bearer {}", jwt)}, | ||||
|                           {std::string("api-version"), std::string(API_VERSION)}, | ||||
|                           {std::string("Content-Type"), std::string("application/json")}}; | ||||
| 
 | ||||
|  | @ -248,6 +327,13 @@ void DeleteJson(const std::string& url, const std::string& data, const std::stri | |||
|         if (response.status >= 400) { | ||||
|             LOG_ERROR(WebService, "DELETE to {} returned error status code: {}", url, | ||||
|                       response.status); | ||||
|             if (response.status == 401 && is_first_attempt) { | ||||
|                 LOG_WARNING(WebService, "Requesting new JWT"); | ||||
|                 UpdateCoreJWT(true, Settings::values.citra_username, Settings::values.citra_token); | ||||
|                 is_first_attempt = false; | ||||
|                 DeleteJson(url, data); | ||||
|                 is_first_attempt = true; | ||||
|             } | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -8,46 +8,76 @@ | |||
| #include <future> | ||||
| #include <string> | ||||
| #include <tuple> | ||||
| #include <httplib.h> | ||||
| #include "common/announce_multiplayer_room.h" | ||||
| #include "common/common_types.h" | ||||
| 
 | ||||
| namespace LUrlParser { | ||||
| class clParseURL; | ||||
| } | ||||
| 
 | ||||
| namespace WebService { | ||||
| 
 | ||||
| /**
 | ||||
|  * Posts JSON to services.citra-emu.org. | ||||
|  * @param url URL of the services.citra-emu.org endpoint to post data to. | ||||
|  * Requests a new JWT if necessary | ||||
|  * @param force_new_token If true, force to request a new token from the server. | ||||
|  * @param username Citra username to use for authentication. | ||||
|  * @param token Citra token to use for authentication. | ||||
|  * @return string with the current JWT toke | ||||
|  */ | ||||
| std::string UpdateCoreJWT(bool force_new_token, const std::string& username, | ||||
|                           const std::string& token); | ||||
| 
 | ||||
| /**
 | ||||
|  * Posts JSON to a api.citra-emu.org. | ||||
|  * @param url URL of the api.citra-emu.org endpoint to post data to. | ||||
|  * @param parsed_url Parsed URL used for the POST request. | ||||
|  * @param params Headers sent for the POST request. | ||||
|  * @param data String of JSON data to use for the body of the POST request. | ||||
|  * @param data If true, a JWT is requested in the function | ||||
|  * @return future with the returned value of the POST | ||||
|  */ | ||||
| static Common::WebResult PostJsonAsyncFn(const std::string& url, | ||||
|                                          const LUrlParser::clParseURL& parsed_url, | ||||
|                                          const httplib::Headers& params, const std::string& data, | ||||
|                                          bool is_jwt_requested); | ||||
| 
 | ||||
| /**
 | ||||
|  * Posts JSON to api.citra-emu.org. | ||||
|  * @param url URL of the api.citra-emu.org endpoint to post data to. | ||||
|  * @param data String of JSON data to use for the body of the POST request. | ||||
|  * @param allow_anonymous If true, allow anonymous unauthenticated requests. | ||||
|  * @return future with the returned value of the POST | ||||
|  */ | ||||
| std::future<Common::WebResult> PostJson(const std::string& url, const std::string& data, | ||||
|                                         bool allow_anonymous); | ||||
| 
 | ||||
| /**
 | ||||
|  * Posts JSON to api.citra-emu.org. | ||||
|  * @param url URL of the api.citra-emu.org endpoint to post data to. | ||||
|  * @param username Citra username to use for authentication. | ||||
|  * @param token Citra token to use for authentication. | ||||
|  * @return future with the error or result of the POST | ||||
|  */ | ||||
| std::future<Common::WebResult> PostJson(const std::string& url, const std::string& data, | ||||
|                                         bool allow_anonymous, const std::string& username = {}, | ||||
|                                         const std::string& token = {}); | ||||
| std::future<Common::WebResult> PostJson(const std::string& url, const std::string& username, | ||||
|                                         const std::string& token); | ||||
| 
 | ||||
| /**
 | ||||
|  * Gets JSON from services.citra-emu.org. | ||||
|  * Gets JSON from api.citra-emu.org. | ||||
|  * @param func A function that gets exectued when the json as a string is received | ||||
|  * @param url URL of the services.citra-emu.org endpoint to post data to. | ||||
|  * @param url URL of the api.citra-emu.org endpoint to post data to. | ||||
|  * @param allow_anonymous If true, allow anonymous unauthenticated requests. | ||||
|  * @param username Citra username to use for authentication. | ||||
|  * @param token Citra token to use for authentication. | ||||
|  * @return future that holds the return value T of the func | ||||
|  */ | ||||
| template <typename T> | ||||
| std::future<T> GetJson(std::function<T(const std::string&)> func, const std::string& url, | ||||
|                        bool allow_anonymous, const std::string& username = {}, | ||||
|                        const std::string& token = {}); | ||||
|                        bool allow_anonymous); | ||||
| 
 | ||||
| /**
 | ||||
|  * Delete JSON to services.citra-emu.org. | ||||
|  * @param url URL of the services.citra-emu.org endpoint to post data to. | ||||
|  * Delete JSON to api.citra-emu.org. | ||||
|  * @param url URL of the api.citra-emu.org endpoint to post data to. | ||||
|  * @param data String of JSON data to use for the body of the DELETE request. | ||||
|  * @param username Citra username to use for authentication. | ||||
|  * @param token Citra token to use for authentication. | ||||
|  */ | ||||
| void DeleteJson(const std::string& url, const std::string& data, const std::string& username = {}, | ||||
|                 const std::string& token = {}); | ||||
| void DeleteJson(const std::string& url, const std::string& data); | ||||
| 
 | ||||
| } // namespace WebService
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue