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

commit 5f7d4259b61b2ce8f5f2955ae448e9adf7d4b058
parent abe5b14296532739d21d441a1e7ba9cd296221ba
Author: Marcel Radzio <mtrnord@nordgedanken.dev>
Date:   Sat, 21 Sep 2024 12:02:40 +0200

Fix issues and make an example

Diffstat:
M.idea/misc.xml | 1+
MCMakeLists.txt | 3+++
Aexamples/CMakeLists.txt | 12++++++++++++
Aexamples/main.cpp | 40++++++++++++++++++++++++++++++++++++++++
Minclude/matrix_coro.hpp | 28++++++++++++++++++++--------
Msrc/matrix_coro.cpp | 89++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Mtests/test.cpp | 41+++++++++++++++++++++--------------------
7 files changed, 169 insertions(+), 45 deletions(-)

diff --git a/.idea/misc.xml b/.idea/misc.xml @@ -13,6 +13,7 @@ <component name="CMakeWorkspace" PROJECT_DIR="$PROJECT_DIR$" /> <component name="CidrRootsConfiguration"> <sourceRoots> + <file path="$PROJECT_DIR$/examples" /> <file path="$PROJECT_DIR$/include" /> <file path="$PROJECT_DIR$/src" /> <file path="$PROJECT_DIR$/tests" /> diff --git a/CMakeLists.txt b/CMakeLists.txt @@ -65,4 +65,7 @@ if (CMAKE_BUILD_TYPE MATCHES "Debug") #) endif () +# Examples +add_subdirectory(examples) + add_subdirectory(tests) \ No newline at end of file diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt @@ -0,0 +1,11 @@ +add_executable( + matrix_coro_example + main.cpp +) + +target_link_libraries( + matrix_coro_example + matrix_coro + cppcoro + spdlog::spdlog +) +\ No newline at end of file diff --git a/examples/main.cpp b/examples/main.cpp @@ -0,0 +1,40 @@ +#include <spdlog/spdlog.h> + +#include "matrix_coro.hpp" +#include "cppcoro/sync_wait.hpp" + +int main() { + spdlog::set_level(spdlog::level::debug); + spdlog::set_pattern("[%H:%M:%S %z] [%^%L%$] [thread %t] %v"); + + Client client; + + const auto homeserver = "https://synapse-oidc.element.dev"; + auto redirect_url = "https://areweoidcyet.com/client-implementation-guide/callback"; + const auto state = "state"; + const auto code_verifier = "code_verifier"; + ClientRegistrationData registration_data; + registration_data.application_type = "web"; + registration_data.client_name = "Test"; + registration_data.client_uri = "https://areweoidcyet.com/"; + registration_data.token_endpoint_auth_method = "none"; + registration_data.redirect_uris = {redirect_url}; + registration_data.response_types = {"code"}; + registration_data.grant_types = {"authorization_code", "refresh_token"}; + registration_data.contacts = {"mailto:hello@localhost"}; + + auto auth_url_task = client.get_auth_url(homeserver, redirect_url, state, code_verifier, registration_data); + + auto auth_url = sync_wait(auth_url_task); + + spdlog::info("Please login via the Auth URL: {}", auth_url); + + // Wait for the user to paste the code here + std::string code; + spdlog::info("Please paste the code here: "); + std::cin >> code; + + auto logged_in_client = sync_wait(client.exchange_token(code, redirect_url)); + + return 0; +} diff --git a/include/matrix_coro.hpp b/include/matrix_coro.hpp @@ -11,10 +11,10 @@ class BaseClient { }; class LoggedInClient : public BaseClient { - LoginResponse login_data; + TokenResponse token_data; public: - explicit LoggedInClient(LoginResponse login_data) : login_data(std::move(login_data)) { + explicit LoggedInClient(TokenResponse token_data) : token_data(std::move(token_data)) { } }; @@ -26,8 +26,21 @@ public: curl_easy_cleanup(curl); } + [[nodiscard]] cppcoro::task<std::string> get_auth_url(std::string homeserver, std::string redirect_url, + std::string state, std::string code_verifier, + const ClientRegistrationData &registration_data); + + [[nodiscard]] cppcoro::task<LoggedInClient> exchange_token(const std::string &code, + const std::string &redirect_url) const; + private: CURL *curl = curl_easy_init(); + WellKnownResponse well_known; + AuthIssuerResponse auth_issuer; + ClientRegistrationResponse client_registration; + OpenIDConfiguration openid_configuration; + std::string state; + std::string code_verifier; /** * \brief Fetches the well-known configuration from the specified homeserver. @@ -37,7 +50,7 @@ private: * \param homeserver The URL of the homeserver from which to fetch the well-known configuration. * \return A cppcoro::task that resolves to a WellKnownResponse containing the well-known configuration. */ - [[nodiscard]] cppcoro::task<WellKnownResponse> fetch_wellknown(const std::string &homeserver) const; + [[nodiscard]] cppcoro::task<WellKnownResponse> fetch_wellknown(std::string homeserver); /** * \brief Fetches the authentication issuer information from the specified client-server endpoint. @@ -47,7 +60,7 @@ private: * \param cs_endpoint The URL of the client-server endpoint from which to fetch the authentication issuer information. * \return A cppcoro::task that resolves to an AuthIssuerResponse containing the authentication issuer information. */ - [[nodiscard]] cppcoro::task<AuthIssuerResponse> fetch_auth_issuer(std::string cs_endpoint) const; + [[nodiscard]] cppcoro::task<AuthIssuerResponse> fetch_auth_issuer(std::string cs_endpoint); /** * \brief Registers a client with the specified authentication endpoint (MSC2966). @@ -61,7 +74,7 @@ private: */ [[nodiscard]] cppcoro::task<ClientRegistrationResponse> register_client(std::string registration_endpoint, const ClientRegistrationData & - registration_data) const; + registration_data); // ReSharper disable once CppMemberFunctionMayBeStatic @@ -77,8 +90,7 @@ private: // Calculate the code challenge from the code_verifier by doing `BASE64URL(SHA256(code_verifier))` const auto code_challenge = cthash::base64url_encode(cthash::simple<cthash::sha256>(code_verifier)).to_string(); - - return auth_endpoint + "/authorize?response_type=code&response_mode=fragment&client_id=" + + return auth_endpoint + "?response_type=code&response_mode=fragment&client_id=" + auth_data.client_id + "&redirect_uri=" + url_encoded_redirect_url + "&scope=urn%3Amatrix%3Aorg.matrix.msc2967.client%3Aapi%3A*%20urn%3Amatrix%3Aorg.matrix.msc2967.client%3Adevice%3AABCDEFGHIJKL&state=" + state + "&code_challenge_method=S256" + "&code_challenge=" + code_challenge; @@ -92,5 +104,5 @@ private: const std::string &redirect_url) const; [[nodiscard]] cppcoro::task<OpenIDConfiguration> fetch_openid_configuration( - std::string auth_endpoint) const; + std::string auth_endpoint); }; diff --git a/src/matrix_coro.cpp b/src/matrix_coro.cpp @@ -1,4 +1,7 @@ #include "matrix_coro.hpp" + +#include <regex> + #include "spdlog/spdlog.h" #include <json/json.h> @@ -8,10 +11,45 @@ static size_t WriteCallback(void *contents, const size_t size, const size_t nmem return size * nmemb; } -cppcoro::task<WellKnownResponse> Client::fetch_wellknown(const std::string &homeserver) const { +cppcoro::task<std::string> Client::get_auth_url(std::string homeserver, std::string redirect_url, + std::string state, std::string code_verifier, + const ClientRegistrationData &registration_data) { + // Get the well-known configuration + const auto well_known = co_await fetch_wellknown(homeserver); + spdlog::debug("Fetched well-known from homeserver: {}", well_known.homeserver); + + // Get the auth issuer information + const auto auth_issuer = co_await fetch_auth_issuer(well_known.homeserver); + spdlog::debug("Fetched auth issuer from homeserver: {}", auth_issuer.issuer); + + // Get the openid configuration + const auto openid_configuration = co_await fetch_openid_configuration(auth_issuer.issuer); + + // Register the client + const auto client_registration = co_await register_client(openid_configuration.registration_endpoint, + registration_data); + + spdlog::debug("Registered client with client_id: {}", client_registration.client_id); + spdlog::debug("Authorization endpoint: {}", openid_configuration.authorization_endpoint); + // Build the auth URL + co_return this->generate_authorize_url(openid_configuration.authorization_endpoint, client_registration, + redirect_url, state, + code_verifier); +} + +cppcoro::task<LoggedInClient> Client::exchange_token(const std::string &code, const std::string &redirect_url) const { + const auto token_resp = co_await exchange_code_for_token(this->openid_configuration.token_endpoint, code, + this->code_verifier, + this->client_registration.client_id, redirect_url); + spdlog::info("Successfully exchanged code for token"); + co_return LoggedInClient(token_resp); +} + +cppcoro::task<WellKnownResponse> Client::fetch_wellknown(std::string homeserver) { if (!curl) { throw std::runtime_error("http client is not initialized"); } + spdlog::info("Fetching well-known from homeserver: {}", homeserver); // Add https as needed to the homeserver address std::string homeserver_https = homeserver; @@ -43,21 +81,24 @@ cppcoro::task<WellKnownResponse> Client::fetch_wellknown(const std::string &home WellKnownResponse response; response.homeserver = root["m.homeserver"]["base_url"].asString(); response.identity_server = root["m.identity_server"]["base_url"].asString(); + this->well_known = response; co_return response; } -cppcoro::task<AuthIssuerResponse> Client::fetch_auth_issuer(std::string cs_endpoint) const { +cppcoro::task<AuthIssuerResponse> Client::fetch_auth_issuer(std::string cs_endpoint) { if (!curl) { throw std::runtime_error("http client is not initialized"); } - // Throw if the cs_endpoint doesnt start with https:// or if it contains a trailing slash or if it is empty or it contains _matrix/client - if (cs_endpoint.find("https://") == std::string::npos || cs_endpoint.find("_matrix/client") != std::string::npos || - cs_endpoint.back() == '/') { + if (cs_endpoint.find("https://") == std::string::npos || cs_endpoint.find("_matrix/client") != std::string::npos) { throw std::runtime_error("invalid cs_endpoint"); } - const auto endpoint = cs_endpoint + "/_matrix/client/unstable/org.matrix.msc2965/auth_issuer"; + auto endpoint = cs_endpoint + "/_matrix/client/unstable/org.matrix.msc2965/auth_issuer"; + + // Remove double slashes and make them single + std::regex re("([^:])(//+)"); + endpoint = std::regex_replace(endpoint, re, "$1/"); std::string str_buffer; curl_easy_setopt(curl, CURLOPT_URL, endpoint.c_str()); @@ -76,17 +117,18 @@ cppcoro::task<AuthIssuerResponse> Client::fetch_auth_issuer(std::string cs_endpo Json::Value root; Json::Reader reader; + spdlog::debug("Auth issuer response: {}", str_buffer); if (const bool parse_status = reader.parse(str_buffer, root); !parse_status) { throw std::runtime_error("failed to parse auth_issuer information"); } AuthIssuerResponse response; response.issuer = root["issuer"].asString(); + this->auth_issuer = response; co_return response; } cppcoro::task<ClientRegistrationResponse> Client::register_client(std::string registration_endpoint, - const ClientRegistrationData &registration_data) -const { + const ClientRegistrationData &registration_data) { if (!curl) { throw std::runtime_error("http client is not initialized"); } @@ -129,6 +171,7 @@ const { // Convert the JSON to a string Json::StreamWriterBuilder writer; const std::string json_str = Json::writeString(writer, root); + spdlog::debug("Registration request: {}", json_str); // Set the POST data curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_str.c_str()); @@ -142,17 +185,20 @@ const { //curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); if (const CURLcode res = curl_easy_perform(curl); res != CURLE_OK) { - throw std::runtime_error("failed to find registration information: " + std::string(curl_easy_strerror(res))); + throw std::runtime_error( + "failed to find registration information: " + std::string(curl_easy_strerror(res))); } Json::Value resp_root; Json::Reader reader; + spdlog::debug("Registration response: {}", str_buffer); if (const bool parse_status = reader.parse(str_buffer, resp_root); !parse_status) { throw std::runtime_error("failed to parse registration information"); } ClientRegistrationResponse response; response.client_id = resp_root["client_id"].asString(); response.client_id_issued_at = resp_root["client_id_issued_at"].asInt(); + this->client_registration = response; co_return response; } @@ -200,7 +246,8 @@ cppcoro::task<TokenResponse> Client::exchange_code_for_token(std::string token_e //curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); if (const CURLcode res = curl_easy_perform(curl); res != CURLE_OK) { - throw std::runtime_error("failed to find exchange token information: " + std::string(curl_easy_strerror(res))); + throw std::runtime_error( + "failed to find exchange token information: " + std::string(curl_easy_strerror(res))); } Json::Value resp_root; @@ -219,18 +266,22 @@ cppcoro::task<TokenResponse> Client::exchange_code_for_token(std::string token_e co_return response; } -cppcoro::task<OpenIDConfiguration> Client::fetch_openid_configuration(std::string auth_endpoint) const { +cppcoro::task<OpenIDConfiguration> Client::fetch_openid_configuration(std::string auth_endpoint) { if (!curl) { throw std::runtime_error("http client is not initialized"); } + spdlog::info("Fetching openid configuration from auth_endpoint: {}", auth_endpoint); - if (auth_endpoint.find("https://") == std::string::npos || auth_endpoint.find("_matrix/client") != std::string::npos - || - auth_endpoint.back() == '/') { + if (auth_endpoint.find("https://") == std::string::npos || auth_endpoint.find("_matrix/client") != + std::string::npos) { throw std::runtime_error("invalid auth_endpoint"); } - const auto endpoint = auth_endpoint + "/.well-known/openid-configuration"; + auto endpoint = auth_endpoint + "/.well-known/openid-configuration"; + + // Remove double slashes and make them single + std::regex re("([^:])(//+)"); + endpoint = std::regex_replace(endpoint, re, "$1/"); std::string str_buffer; curl_easy_setopt(curl, CURLOPT_URL, endpoint.c_str()); @@ -277,7 +328,8 @@ cppcoro::task<OpenIDConfiguration> Client::fetch_openid_configuration(std::strin response.token_endpoint_auth_methods_supported.push_back(token_endpoint_auth_method.asString()); } for (const auto &token_endpoint_auth_signing_alg: root["token_endpoint_auth_signing_alg_values_supported"]) { - response.token_endpoint_auth_signing_alg_values_supported.push_back(token_endpoint_auth_signing_alg.asString()); + response.token_endpoint_auth_signing_alg_values_supported.push_back( + token_endpoint_auth_signing_alg.asString()); } response.revocation_endpoint = root["revocation_endpoint"].asString(); for (const auto &revocation_endpoint_auth_method: root["revocation_endpoint_auth_methods_supported"]) { @@ -290,7 +342,8 @@ cppcoro::task<OpenIDConfiguration> Client::fetch_openid_configuration(std::strin } response.introspection_endpoint = root["introspection_endpoint"].asString(); for (const auto &introspection_endpoint_auth_method: root["introspection_endpoint_auth_methods_supported"]) { - response.introspection_endpoint_auth_methods_supported.push_back(introspection_endpoint_auth_method.asString()); + response.introspection_endpoint_auth_methods_supported.push_back( + introspection_endpoint_auth_method.asString()); } for (const auto &introspection_endpoint_auth_signing_alg: root[ "introspection_endpoint_auth_signing_alg_values_supported"]) { @@ -333,5 +386,7 @@ cppcoro::task<OpenIDConfiguration> Client::fetch_openid_configuration(std::strin response.account_management_actions_supported.push_back(account_management_action.asString()); } + this->openid_configuration = response; + co_return response; } diff --git a/tests/test.cpp b/tests/test.cpp @@ -8,23 +8,23 @@ class ClientTest { public: - static cppcoro::task<WellKnownResponse> test_fetch_wellknown(const Client &client, const std::string &homeserver) { + static cppcoro::task<WellKnownResponse> test_fetch_wellknown(Client &client, const std::string &homeserver) { return client.fetch_wellknown(homeserver); } - static cppcoro::task<AuthIssuerResponse> test_fetch_auth_issuer(const Client &client, + static cppcoro::task<AuthIssuerResponse> test_fetch_auth_issuer(Client &client, const std::string &cs_endpoint) { return client.fetch_auth_issuer(cs_endpoint); } - static cppcoro::task<ClientRegistrationResponse> test_register_client(const Client &client, + static cppcoro::task<ClientRegistrationResponse> test_register_client(Client &client, const std::string &registration_endpoint, const ClientRegistrationData & registration_data) { return client.register_client(registration_endpoint, registration_data); } - static std::string test_generate_authorize_url(const Client &client, + static std::string test_generate_authorize_url(Client &client, const std::string &auth_endpoint, const ClientRegistrationResponse &auth_data, const std::string &redirect_url, @@ -33,7 +33,7 @@ public: return client.generate_authorize_url(auth_endpoint, auth_data, redirect_url, state, code_verifier); } - static cppcoro::task<OpenIDConfiguration> fetch_openid_configuration(const Client &client, + static cppcoro::task<OpenIDConfiguration> fetch_openid_configuration(Client &client, const std::string &auth_endpoint) { return client.fetch_openid_configuration(auth_endpoint); } @@ -58,7 +58,7 @@ SCENARIO("fetch_wellknown can find and parse well-known at matrix.org") { initLogging(); GIVEN("A Client instance") { WHEN("fetch_wellknown is called with matrix.org") { - const Client client; + Client client; auto task = ClientTest::test_fetch_wellknown(client, "matrix.org"); auto [homeserver, identity_server, raw] = sync_wait(task); THEN("A valid WellKnownResponse should be returned") { @@ -74,7 +74,7 @@ SCENARIO("fetch_wellknown throws runtime_error if curl_easy_perform fails") { GIVEN("A Client instance with an invalid URL") { WHEN("fetch_wellknown is called with an invalid URL") { THEN("A runtime_error should be thrown") { - const Client client; + Client client; REQUIRE_THROWS_AS(sync_wait(ClientTest::test_fetch_wellknown(client,"invalid_url")), std::runtime_error); } @@ -87,7 +87,7 @@ SCENARIO("fetch_wellknown throws runtime_error if JSON parsing fails") { GIVEN("A Client instance with a URL returning invalid JSON") { WHEN("fetch_wellknown is called with a URL returning invalid JSON") { THEN("A runtime_error should be thrown") { - const Client client; + Client client; REQUIRE_THROWS_AS( sync_wait(ClientTest::test_fetch_wellknown(client,"https://example.com/invalid-json")), std::runtime_error); @@ -100,7 +100,7 @@ SCENARIO("fetch_auth_issuer can find and parse auth issuer at https://synapse-oi initLogging(); GIVEN("A Client instance") { WHEN("fetch_auth_issuer is called with https://synapse-oidc.element.dev") { - const Client client; + Client client; auto task = ClientTest::test_fetch_auth_issuer(client, "https://synapse-oidc.element.dev"); auto [issuer] = sync_wait(task); THEN("A valid AuthIssuerResponse should be returned") { @@ -115,7 +115,7 @@ SCENARIO("fetch_auth_issuer throws runtime_error if curl_easy_perform fails") { GIVEN("A Client instance with an invalid URL") { WHEN("fetch_auth_issuer is called with an invalid URL") { THEN("A runtime_error should be thrown") { - const Client client; + Client client; REQUIRE_THROWS_AS(sync_wait(ClientTest::test_fetch_auth_issuer(client,"invalid_url")), std::runtime_error); } @@ -128,7 +128,7 @@ SCENARIO("fetch_auth_issuer throws runtime_error if JSON parsing fails") { GIVEN("A Client instance with a URL returning invalid JSON") { WHEN("fetch_auth_issuer is called with a URL returning invalid JSON") { THEN("A runtime_error should be thrown") { - const Client client; + Client client; REQUIRE_THROWS_AS( sync_wait(ClientTest::test_fetch_auth_issuer(client,"https://example.com/invalid-json")), std::runtime_error); @@ -141,7 +141,7 @@ SCENARIO("register_client can register a client at https://synapse-oidc.element. initLogging(); GIVEN("A Client instance") { WHEN("register_client is called with https://synapse-oidc.element.dev") { - const Client client; + Client client; ClientRegistrationData registration_data; registration_data.application_type = "web"; registration_data.client_name = "Test Client"; @@ -167,7 +167,7 @@ SCENARIO("register_client throws runtime_error if curl_easy_perform fails") { GIVEN("A Client instance with an invalid URL") { WHEN("register_client is called with an invalid URL") { THEN("A runtime_error should be thrown") { - const Client client; + Client client; ClientRegistrationData registration_data; registration_data.application_type = "web"; registration_data.client_name = "Test Client"; @@ -189,7 +189,7 @@ SCENARIO("register_client throws runtime_error if JSON parsing fails") { GIVEN("A Client instance with a URL returning invalid JSON") { WHEN("register_client is called with a URL returning invalid JSON") { THEN("A runtime_error should be thrown") { - const Client client; + Client client; ClientRegistrationData registration_data; registration_data.application_type = "web"; registration_data.client_name = "Test Client"; @@ -212,15 +212,16 @@ SCENARIO("generate_authorize_url can generate a valid authorize URL") { initLogging(); GIVEN("A Client") { WHEN("generate_authorize_url is called") { - const Client client; + Client client; ClientRegistrationResponse auth_data; auth_data.client_id = "test_client_id"; auth_data.client_id_issued_at = 1630000000; std::string redirect_url = "https://example.com"; std::string state = "test_state"; std::string code_verifier = "test_code_verifier"; - auto authorize_url = ClientTest::test_generate_authorize_url(client, "https://auth-oidc.element.dev", - auth_data, redirect_url, state, code_verifier); + auto authorize_url = ClientTest::test_generate_authorize_url( + client, "https://auth-oidc.element.dev/authorize", + auth_data, redirect_url, state, code_verifier); THEN("A valid authorize URL should be returned") { REQUIRE( authorize_url == @@ -236,7 +237,7 @@ SCENARIO("fetch_openid_configuration can find and parse openid configuration at initLogging(); GIVEN("A Client instance") { WHEN("fetch_openid_configuration is called with https://auth-oidc.element.dev") { - const Client client; + Client client; auto task = ClientTest::fetch_openid_configuration(client, "https://auth-oidc.element.dev"); auto resp = sync_wait(task); THEN("A valid OpenIDConfiguration should be returned") { @@ -260,7 +261,7 @@ SCENARIO("fetch_openid_configuration throws runtime_error if curl_easy_perform f GIVEN("A Client instance with an invalid URL") { WHEN("fetch_openid_configuration is called with an invalid URL") { THEN("A runtime_error should be thrown") { - const Client client; + Client client; REQUIRE_THROWS_AS(sync_wait(ClientTest::fetch_openid_configuration(client,"invalid_url")), std::runtime_error); } @@ -273,7 +274,7 @@ SCENARIO("fetch_openid_configuration throws runtime_error if JSON parsing fails" GIVEN("A Client instance with a URL returning invalid JSON") { WHEN("fetch_openid_configuration is called with a URL returning invalid JSON") { THEN("A runtime_error should be thrown") { - const Client client; + Client client; REQUIRE_THROWS_AS( sync_wait(ClientTest::fetch_openid_configuration(client,"https://example.com/invalid-json")), std::runtime_error);