matrix_coro.cpp (18519B)
1 #include "matrix_coro.hpp" 2 3 #include <regex> 4 5 #include "spdlog/spdlog.h" 6 #include <json/json.h> 7 8 static size_t WriteCallback(void* contents, const size_t size, const size_t nmemb, void* userp) 9 { 10 static_cast<std::string*>(userp)->append(static_cast<char*>(contents), size * nmemb); 11 return size * nmemb; 12 } 13 14 cppcoro::task<> setCommonCurlOptions(CURL* curl, const std::string& user_agent, const std::string& url, 15 std::string& str_buffer) 16 { 17 curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); 18 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); 19 curl_easy_setopt(curl, CURLOPT_WRITEDATA, &str_buffer); 20 // Set User-Agent 21 curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent.c_str()); 22 23 /* only allow redirects to HTTP and HTTPS URLs */ 24 curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS_STR, 25 "http,https"); 26 27 /* enable all supported built-in compressions */ 28 curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); 29 30 //curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); 31 co_return; 32 } 33 34 cppcoro::task<Json::Value> LoggedInClient::get(const std::string& url) const 35 { 36 const auto curl = curl_easy_init(); 37 if (!curl) 38 { 39 throw std::runtime_error("http client is not initialized"); 40 } 41 42 if (token_data.access_token.empty()) 43 { 44 throw std::runtime_error("access token is empty"); 45 } 46 47 spdlog::debug("Fetching url: {}", url); 48 49 std::string str_buffer; 50 co_await setCommonCurlOptions(curl, user_agent, url, str_buffer); 51 52 // Add Authorization header 53 const auto access_token = token_data.access_token; 54 curl_easy_setopt(curl, CURLOPT_XOAUTH2_BEARER, access_token.c_str()); 55 curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_BEARER); 56 57 //curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); 58 59 if (const CURLcode res = curl_easy_perform(curl); res != CURLE_OK) 60 { 61 throw std::runtime_error("failed to fetch \"" + url + "\": " + std::string(curl_easy_strerror(res))); 62 } 63 64 Json::Value root; 65 Json::Reader reader; 66 if (const bool parse_status = reader.parse(str_buffer, root); !parse_status) 67 { 68 throw std::runtime_error("failed to parse json"); 69 } 70 71 curl_easy_cleanup(curl); 72 co_return root; 73 } 74 75 cppcoro::task<WhoamiResponse> LoggedInClient::whoami() const 76 { 77 auto homeserver = well_known.homeserver; 78 79 // Add https as needed to the homeserver address 80 std::string homeserver_https = homeserver; 81 if (homeserver_https.find("https://") == std::string::npos) 82 { 83 homeserver_https = "https://" + homeserver_https; 84 } 85 spdlog::info("Fetching whoami from homeserver: {}", homeserver); 86 auto endpoint = homeserver_https + "_matrix/client/v3/account/whoami"; 87 88 // Remove double slashes and make them single 89 const std::regex re("([^:])(//+)"); 90 endpoint = std::regex_replace(endpoint, re, "$1/"); 91 spdlog::debug("Whoami endpoint: {}", endpoint); 92 93 const auto json = co_await get(endpoint); 94 WhoamiResponse response; 95 response.user_id = json["user_id"].asString(); 96 response.device_id = json["device_id"].asString(); 97 response.is_guest = json["is_guest"].asBool(); 98 99 co_return response; 100 } 101 102 cppcoro::task<std::string> Client::get_auth_url(std::string homeserver, std::string redirect_url, 103 std::string state, std::string code_verifier, 104 const ClientRegistrationData& registration_data) 105 { 106 // Get the well-known configuration 107 const auto well_known = co_await fetch_wellknown(homeserver); 108 spdlog::debug("Fetched well-known from homeserver: {}", well_known.homeserver); 109 110 // Get the auth issuer information 111 const auto [issuer] = co_await fetch_auth_issuer(well_known.homeserver); 112 spdlog::debug("Fetched auth issuer from homeserver: {}", issuer); 113 114 // Get the openid configuration 115 const auto openid_configuration = co_await fetch_openid_configuration(issuer); 116 117 // Register the client 118 const auto client_registration = co_await register_client(openid_configuration.registration_endpoint, 119 registration_data); 120 121 spdlog::info("Registered client with client_id: {}", client_registration.client_id); 122 123 this->code_verifier = code_verifier; 124 125 // Build the auth URL 126 co_return this->generate_authorize_url(openid_configuration.authorization_endpoint, client_registration, 127 redirect_url, state, 128 code_verifier); 129 } 130 131 cppcoro::task<LoggedInClient> Client::exchange_token(const std::string& code, const std::string& redirect_url) const 132 { 133 const auto token_resp = co_await exchange_code_for_token(this->openid_configuration.token_endpoint, code, 134 this->code_verifier, 135 this->client_registration.client_id, redirect_url); 136 137 if (token_resp.access_token.empty()) 138 { 139 throw std::runtime_error("access token is empty after exchange"); 140 } 141 142 spdlog::info("Successfully exchanged code for token"); 143 144 const auto logged_in_client = LoggedInClient(token_resp, this->well_known); 145 co_return logged_in_client; 146 } 147 148 cppcoro::task<Json::Value> Client::get(const std::string& url) const 149 { 150 const auto curl = curl_easy_init(); 151 if (!curl) 152 { 153 throw std::runtime_error("http client is not initialized"); 154 } 155 156 spdlog::debug("Fetching url: {}", url); 157 158 std::string str_buffer; 159 co_await setCommonCurlOptions(curl, user_agent, url, str_buffer); 160 161 if (const CURLcode res = curl_easy_perform(curl); res != CURLE_OK) 162 { 163 throw std::runtime_error("failed to fetch \"" + url + "\": " + std::string(curl_easy_strerror(res))); 164 } 165 166 Json::Value json; 167 Json::Reader reader; 168 if (const bool parse_status = reader.parse(str_buffer, json); !parse_status) 169 { 170 throw std::runtime_error("failed to parse json"); 171 } 172 173 if (json.isMember("error")) 174 { 175 throw std::runtime_error("error: " + json["error"].asString() + ", error_description: " + 176 json["error_description"].asString()); 177 } 178 179 curl_easy_cleanup(curl); 180 co_return json; 181 } 182 183 cppcoro::task<Json::Value> Client::post(const std::string& url, const std::string& data, const bool form_data) const 184 { 185 const auto curl = curl_easy_init(); 186 if (!curl) 187 { 188 throw std::runtime_error("http client is not initialized"); 189 } 190 191 spdlog::debug("Fetching url: {}", url); 192 193 std::string str_buffer; 194 co_await setCommonCurlOptions(curl, user_agent, url, str_buffer); 195 196 // Add data to body and set to POST request type 197 curl_easy_setopt(curl, CURLOPT_POST, 1L); 198 curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str()); 199 200 // Set the Content-Type header 201 curl_slist* headers = nullptr; 202 if (form_data) 203 { 204 headers = curl_slist_append(headers, "Content-Type: application/x-www-form-urlencoded"); 205 } 206 else 207 { 208 headers = curl_slist_append(headers, "Content-Type: application/json"); 209 } 210 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); 211 212 /* enable all supported built-in compressions */ 213 curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); 214 215 //curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); 216 217 if (const CURLcode res = curl_easy_perform(curl); res != CURLE_OK) 218 { 219 throw std::runtime_error("failed to fetch \"" + url + "\": " + std::string(curl_easy_strerror(res))); 220 } 221 222 Json::Value json; 223 Json::Reader reader; 224 if (const bool parse_status = reader.parse(str_buffer, json); !parse_status) 225 { 226 throw std::runtime_error("failed to parse json"); 227 } 228 229 if (json.isMember("error")) 230 { 231 throw std::runtime_error("error: " + json["error"].asString() + ", error_description: " + 232 json["error_description"].asString()); 233 } 234 235 curl_easy_cleanup(curl); 236 co_return json; 237 } 238 239 cppcoro::task<WellKnownResponse> Client::fetch_wellknown(std::string homeserver) 240 { 241 spdlog::info("Fetching well-known from homeserver: {}", homeserver); 242 243 // Add https as needed to the homeserver address 244 std::string homeserver_https = homeserver; 245 if (homeserver_https.find("https://") == std::string::npos) 246 { 247 homeserver_https = "https://" + homeserver_https; 248 } 249 auto endpoint = homeserver_https + "/.well-known/matrix/client"; 250 251 // Remove double slashes and make them single 252 const std::regex re("([^:])(//+)"); 253 endpoint = std::regex_replace(endpoint, re, "$1/"); 254 255 const auto json = co_await get(endpoint); 256 257 WellKnownResponse response; 258 response.homeserver = json["m.homeserver"]["base_url"].asString(); 259 response.identity_server = json["m.identity_server"]["base_url"].asString(); 260 this->well_known = response; 261 co_return response; 262 } 263 264 cppcoro::task<AuthIssuerResponse> Client::fetch_auth_issuer(std::string cs_endpoint) 265 { 266 if (cs_endpoint.find("https://") == std::string::npos || cs_endpoint.find("_matrix/client") != std::string::npos) 267 { 268 throw std::runtime_error("invalid cs_endpoint"); 269 } 270 271 auto endpoint = cs_endpoint + "/_matrix/client/unstable/org.matrix.msc2965/auth_issuer"; 272 273 // Remove double slashes and make them single 274 const std::regex re("([^:])(//+)"); 275 endpoint = std::regex_replace(endpoint, re, "$1/"); 276 277 const auto json = co_await get(endpoint); 278 279 AuthIssuerResponse response; 280 response.issuer = json["issuer"].asString(); 281 this->auth_issuer = response; 282 co_return response; 283 } 284 285 cppcoro::task<ClientRegistrationResponse> Client::register_client(std::string registration_endpoint, 286 const ClientRegistrationData& registration_data) 287 { 288 if (registration_endpoint.find("https://") == std::string::npos) 289 { 290 throw std::runtime_error("invalid registration endpoint"); 291 } 292 293 // Convert the registration data to a JSON string 294 Json::Value root; 295 root["application_type"] = registration_data.application_type; 296 root["client_name"] = registration_data.client_name; 297 root["redirect_uris"] = Json::arrayValue; 298 for (const auto& uri : registration_data.redirect_uris) 299 { 300 root["redirect_uris"].append(uri); 301 } 302 root["response_types"] = Json::arrayValue; 303 for (const auto& response_type : registration_data.response_types) 304 { 305 root["response_types"].append(response_type); 306 } 307 root["token_endpoint_auth_method"] = registration_data.token_endpoint_auth_method; 308 root["client_uri"] = registration_data.client_uri; 309 root["contacts"] = Json::arrayValue; 310 for (const auto& contact : registration_data.contacts) 311 { 312 root["contacts"].append(contact); 313 } 314 315 // Convert the JSON to a string 316 Json::StreamWriterBuilder writer; 317 const std::string json_str = Json::writeString(writer, root); 318 319 const auto json = co_await post(registration_endpoint, json_str); 320 ClientRegistrationResponse response; 321 response.client_id = json["client_id"].asString(); 322 response.client_id_issued_at = json["client_id_issued_at"].asInt(); 323 this->client_registration = response; 324 co_return response; 325 } 326 327 cppcoro::task<TokenResponse> Client::exchange_code_for_token(std::string token_endpoint, 328 const std::string& code, 329 const std::string& code_verifier, 330 const std::string& client_id, 331 const std::string& redirect_url) const 332 { 333 if (token_endpoint.find("https://") == std::string::npos) 334 { 335 throw std::runtime_error("invalid token_endpoint"); 336 } 337 338 // Url encode the redirect URL 339 const auto url_encoded_redirect_url = url_encode(redirect_url); 340 341 // Build the request body 342 const std::string post_fields = "grant_type=authorization_code&code=" + code + "&redirect_uri=" + 343 url_encoded_redirect_url + 344 "&client_id=" + client_id + "&code_verifier=" + code_verifier; 345 346 const auto json = co_await post(token_endpoint, post_fields, true); 347 348 if (json.isMember("error")) 349 { 350 throw std::runtime_error("error: " + json["error"].asString() + ", error_description: " + 351 json["error_description"].asString()); 352 } 353 354 TokenResponse response; 355 response.access_token = json["access_token"].asString(); 356 response.expires_in = json["expires_in"].asInt(); 357 response.refresh_token = json["refresh_token"].asString(); 358 response.token_type = json["token_type"].asString(); 359 response.scope = json["scope"].asString(); 360 361 co_return response; 362 } 363 364 cppcoro::task<OpenIDConfiguration> Client::fetch_openid_configuration(std::string auth_endpoint) 365 { 366 spdlog::info("Fetching openid configuration from auth_endpoint: {}", auth_endpoint); 367 368 if (auth_endpoint.find("https://") == std::string::npos || auth_endpoint.find("_matrix/client") != 369 std::string::npos) 370 { 371 throw std::runtime_error("invalid auth_endpoint"); 372 } 373 374 auto endpoint = auth_endpoint + "/.well-known/openid-configuration"; 375 376 // Remove double slashes and make them single 377 std::regex re("([^:])(//+)"); 378 endpoint = std::regex_replace(endpoint, re, "$1/"); 379 380 const auto json = co_await get(endpoint); 381 382 OpenIDConfiguration response; 383 response.issuer = json["issuer"].asString(); 384 response.authorization_endpoint = json["authorization_endpoint"].asString(); 385 response.token_endpoint = json["token_endpoint"].asString(); 386 response.jwks_uri = json["jwks_uri"].asString(); 387 response.registration_endpoint = json["registration_endpoint"].asString(); 388 389 for (const auto& scope : json["scopes_supported"]) 390 { 391 response.scopes_supported.push_back(scope.asString()); 392 } 393 for (const auto& response_type : json["response_types_supported"]) 394 { 395 response.response_types_supported.push_back(response_type.asString()); 396 } 397 for (const auto& response_mode : json["response_modes_supported"]) 398 { 399 response.response_modes_supported.push_back(response_mode.asString()); 400 } 401 for (const auto& grant_type : json["grant_types_supported"]) 402 { 403 response.grant_types_supported.push_back(grant_type.asString()); 404 } 405 for (const auto& token_endpoint_auth_method : json["token_endpoint_auth_methods_supported"]) 406 { 407 response.token_endpoint_auth_methods_supported.push_back(token_endpoint_auth_method.asString()); 408 } 409 for (const auto& token_endpoint_auth_signing_alg : json["token_endpoint_auth_signing_alg_values_supported"]) 410 { 411 response.token_endpoint_auth_signing_alg_values_supported.push_back( 412 token_endpoint_auth_signing_alg.asString()); 413 } 414 response.revocation_endpoint = json["revocation_endpoint"].asString(); 415 for (const auto& revocation_endpoint_auth_method : json["revocation_endpoint_auth_methods_supported"]) 416 { 417 response.revocation_endpoint_auth_methods_supported.push_back(revocation_endpoint_auth_method.asString()); 418 } 419 for (const auto& revocation_endpoint_auth_signing_alg : json[ 420 "revocation_endpoint_auth_signing_alg_values_supported"]) 421 { 422 response.revocation_endpoint_auth_signing_alg_values_supported.push_back( 423 revocation_endpoint_auth_signing_alg.asString()); 424 } 425 response.introspection_endpoint = json["introspection_endpoint"].asString(); 426 for (const auto& introspection_endpoint_auth_method : json["introspection_endpoint_auth_methods_supported"]) 427 { 428 response.introspection_endpoint_auth_methods_supported.push_back( 429 introspection_endpoint_auth_method.asString()); 430 } 431 for (const auto& introspection_endpoint_auth_signing_alg : json[ 432 "introspection_endpoint_auth_signing_alg_values_supported"]) 433 { 434 response.introspection_endpoint_auth_signing_alg_values_supported.push_back( 435 introspection_endpoint_auth_signing_alg.asString()); 436 } 437 for (const auto& code_challenge_method : json["code_challenge_methods_supported"]) 438 { 439 response.code_challenge_methods_supported.push_back(code_challenge_method.asString()); 440 } 441 response.userinfo_endpoint = json["userinfo_endpoint"].asString(); 442 for (const auto& subject_type : json["subject_types_supported"]) 443 { 444 response.subject_types_supported.push_back(subject_type.asString()); 445 } 446 for (const auto& id_token_signing_alg : json["id_token_signing_alg_values_supported"]) 447 { 448 response.id_token_signing_alg_values_supported.push_back(id_token_signing_alg.asString()); 449 } 450 for (const auto& userinfo_signing_alg : json["userinfo_signing_alg_values_supported"]) 451 { 452 response.userinfo_signing_alg_values_supported.push_back(userinfo_signing_alg.asString()); 453 } 454 for (const auto& display_value : json["display_values_supported"]) 455 { 456 response.display_values_supported.push_back(display_value.asString()); 457 } 458 for (const auto& claim_type : json["claim_types_supported"]) 459 { 460 response.claim_types_supported.push_back(claim_type.asString()); 461 } 462 for (const auto& claim : json["claims_supported"]) 463 { 464 response.claims_supported.push_back(claim.asString()); 465 } 466 response.claims_parameter_supported = json["claims_parameter_supported"].asBool(); 467 response.request_parameter_supported = json["request_parameter_supported"].asBool(); 468 response.request_uri_parameter_supported = json["request_uri_parameter_supported"].asBool(); 469 for (const auto& prompt_value : json["prompt_values_supported"]) 470 { 471 response.prompt_values_supported.push_back(prompt_value.asString()); 472 } 473 response.device_authorization_endpoint = json["device_authorization_endpoint"].asString(); 474 response.org_matrix_matrix_authentication_service_graphql_endpoint = json[ 475 "org.matrix.matrix_authentication_service_graphql_endpoint"].asString(); 476 response.account_management_uri = json["account_management_uri"].asString(); 477 for (const auto& account_management_action : json["account_management_actions_supported"]) 478 { 479 response.account_management_actions_supported.push_back(account_management_action.asString()); 480 } 481 482 this->openid_configuration = response; 483 484 co_return response; 485 }