matrix-coro

A WIP highlevel matrix c++ SDK with a MAS first approach
git clone git://archive.git.mtrnord.blog/MTRNord/matrix-coro.git
Log | Files | Refs | README | LICENSE

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 }