web_backend.cpp
1 // Copyright 2017 Citra Emulator Project 2 // Licensed under GPLv2 or any later version 3 // Refer to the license.txt file included. 4 5 #include <array> 6 #include <cstdlib> 7 #include <mutex> 8 #include <string> 9 #include <fmt/format.h> 10 #include <httplib.h> 11 #include "common/common_types.h" 12 #include "common/logging/log.h" 13 #include "common/web_result.h" 14 #include "web_service/web_backend.h" 15 16 namespace WebService { 17 18 constexpr std::array<const char, 1> API_VERSION{'1'}; 19 20 constexpr std::size_t TIMEOUT_SECONDS = 30; 21 22 struct Client::Impl { 23 Impl(std::string host, std::string username, std::string token) 24 : host{std::move(host)}, username{std::move(username)}, token{std::move(token)} { 25 std::lock_guard lock{jwt_cache.mutex}; 26 if (this->username == jwt_cache.username && this->token == jwt_cache.token) { 27 jwt = jwt_cache.jwt; 28 } 29 // normalize host expression 30 if (!this->host.empty() && this->host.back() == '/') { 31 static_cast<void>(this->host.pop_back()); 32 } 33 } 34 35 /// A generic function handles POST, GET and DELETE request together 36 Common::WebResult GenericRequest(const std::string& method, const std::string& path, 37 const std::string& data, bool allow_anonymous, 38 const std::string& accept) { 39 if (jwt.empty()) { 40 UpdateJWT(); 41 } 42 43 if (jwt.empty() && !allow_anonymous) { 44 LOG_ERROR(WebService, "Credentials must be provided for authenticated requests"); 45 return Common::WebResult{Common::WebResult::Code::CredentialsMissing, 46 "Credentials needed"}; 47 } 48 49 auto result = GenericRequest(method, path, data, accept, jwt); 50 if (result.result_string == "401") { 51 // Try again with new JWT 52 UpdateJWT(); 53 result = GenericRequest(method, path, data, accept, jwt); 54 } 55 56 return result; 57 } 58 59 /** 60 * A generic function with explicit authentication method specified 61 * JWT is used if the jwt parameter is not empty 62 * username + token is used if jwt is empty but username and token are 63 * not empty anonymous if all of jwt, username and token are empty 64 */ 65 Common::WebResult GenericRequest(const std::string& method, const std::string& path, 66 const std::string& data, const std::string& accept, 67 const std::string& jwt = "", const std::string& username = "", 68 const std::string& token = "") { 69 if (cli == nullptr) { 70 cli = std::make_unique<httplib::Client>(host.c_str()); 71 cli->set_connection_timeout(TIMEOUT_SECONDS); 72 cli->set_read_timeout(TIMEOUT_SECONDS); 73 cli->set_write_timeout(TIMEOUT_SECONDS); 74 } 75 if (!cli->is_valid()) { 76 LOG_ERROR(WebService, "Invalid URL {}", host + path); 77 return Common::WebResult{Common::WebResult::Code::InvalidURL, "Invalid URL"}; 78 } 79 80 httplib::Headers params; 81 if (!jwt.empty()) { 82 params = { 83 {std::string("Authorization"), fmt::format("Bearer {}", jwt)}, 84 }; 85 } else if (!username.empty()) { 86 params = { 87 {std::string("x-username"), username}, 88 {std::string("x-token"), token}, 89 }; 90 } 91 92 params.emplace(std::string("api-version"), 93 std::string(API_VERSION.begin(), API_VERSION.end())); 94 if (method != "GET") { 95 params.emplace(std::string("Content-Type"), std::string("application/json")); 96 }; 97 98 httplib::Request request; 99 request.method = method; 100 request.path = path; 101 request.headers = params; 102 request.body = data; 103 104 httplib::Result result = cli->send(request); 105 106 if (!result) { 107 LOG_ERROR(WebService, "{} to {} returned null", method, host + path); 108 return Common::WebResult{Common::WebResult::Code::LibError, "Null response"}; 109 } 110 111 httplib::Response response = result.value(); 112 113 if (response.status >= 400) { 114 LOG_ERROR(WebService, "{} to {} returned error status code: {}", method, host + path, 115 response.status); 116 return Common::WebResult{Common::WebResult::Code::HttpError, 117 std::to_string(response.status)}; 118 } 119 120 auto content_type = response.headers.find("content-type"); 121 122 if (content_type == response.headers.end()) { 123 LOG_ERROR(WebService, "{} to {} returned no content", method, host + path); 124 return Common::WebResult{Common::WebResult::Code::WrongContent, ""}; 125 } 126 127 if (content_type->second.find(accept) == std::string::npos) { 128 LOG_ERROR(WebService, "{} to {} returned wrong content: {}", method, host + path, 129 content_type->second); 130 return Common::WebResult{Common::WebResult::Code::WrongContent, "Wrong content"}; 131 } 132 return Common::WebResult{Common::WebResult::Code::Success, "", response.body}; 133 } 134 135 // Retrieve a new JWT from given username and token 136 void UpdateJWT() { 137 if (username.empty() || token.empty()) { 138 return; 139 } 140 141 auto result = GenericRequest("POST", "/jwt/internal", "", "text/html", "", username, token); 142 if (result.result_code != Common::WebResult::Code::Success) { 143 LOG_ERROR(WebService, "UpdateJWT failed"); 144 } else { 145 std::lock_guard lock{jwt_cache.mutex}; 146 jwt_cache.username = username; 147 jwt_cache.token = token; 148 jwt_cache.jwt = jwt = result.returned_data; 149 } 150 } 151 152 std::string host; 153 std::string username; 154 std::string token; 155 std::string jwt; 156 std::unique_ptr<httplib::Client> cli; 157 158 struct JWTCache { 159 std::mutex mutex; 160 std::string username; 161 std::string token; 162 std::string jwt; 163 }; 164 static inline JWTCache jwt_cache; 165 }; 166 167 Client::Client(std::string host, std::string username, std::string token) 168 : impl{std::make_unique<Impl>(std::move(host), std::move(username), std::move(token))} {} 169 170 Client::~Client() = default; 171 172 Common::WebResult Client::PostJson(const std::string& path, const std::string& data, 173 bool allow_anonymous) { 174 return impl->GenericRequest("POST", path, data, allow_anonymous, "application/json"); 175 } 176 177 Common::WebResult Client::GetJson(const std::string& path, bool allow_anonymous) { 178 return impl->GenericRequest("GET", path, "", allow_anonymous, "application/json"); 179 } 180 181 Common::WebResult Client::DeleteJson(const std::string& path, const std::string& data, 182 bool allow_anonymous) { 183 return impl->GenericRequest("DELETE", path, data, allow_anonymous, "application/json"); 184 } 185 186 Common::WebResult Client::GetPlain(const std::string& path, bool allow_anonymous) { 187 return impl->GenericRequest("GET", path, "", allow_anonymous, "text/plain"); 188 } 189 190 Common::WebResult Client::GetImage(const std::string& path, bool allow_anonymous) { 191 return impl->GenericRequest("GET", path, "", allow_anonymous, "image/png"); 192 } 193 194 Common::WebResult Client::GetExternalJWT(const std::string& audience) { 195 return impl->GenericRequest("POST", fmt::format("/jwt/external/{}", audience), "", false, 196 "text/html"); 197 } 198 199 } // namespace WebService