// Copyright (C) 2024 Simon Quigley // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . #include "launchpad.h" #include "authentication.h" #include "person.h" #include "distribution.h" #include "archive.h" #include "utils.h" #include #include #include #include #include static size_t StaticWriteCallback(void* contents, size_t size, size_t nmemb, void* userp) { size_t totalSize = size * nmemb; std::string* mem = static_cast(userp); mem->append(static_cast(contents), totalSize); return totalSize; } // Minimal URL encoder for the static helper std::string launchpad::url_encode(const std::string& value) { std::ostringstream escaped; escaped.fill('0'); escaped << std::hex; for (unsigned char c : value) { if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') { escaped << c; } else { escaped << '%' << std::setw(2) << (int)c; } } return escaped.str(); } // A static helper to do a raw POST without OAuth for initial token requests std::optional launchpad::static_http_post(const std::string& url, const std::map& params) { CURL* curl = curl_easy_init(); if (!curl) { std::cerr << "Failed to initialize CURL for static HTTP POST." << std::endl; return std::nullopt; } std::string post_fields; for (const auto& [k,v] : params) { post_fields += url_encode(k) + "=" + url_encode(v) + "&"; } if (!post_fields.empty()) post_fields.pop_back(); std::string readBuffer; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_fields.c_str()); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)post_fields.size()); curl_easy_setopt(curl, CURLOPT_POST, 1L); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, StaticWriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); CURLcode res = curl_easy_perform(curl); long http_code=0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); curl_easy_cleanup(curl); if (res != CURLE_OK) { std::cerr << "static_http_post error: " << curl_easy_strerror(res) << std::endl; return std::nullopt; } if (http_code >= 400) { std::cerr << "static_http_post failed with code: " << http_code << "\nResponse: " << readBuffer << std::endl; return std::nullopt; } return readBuffer; } launchpad::launchpad() : consumer_key("Ubuntu"), consumer_secret("&"), oauth_token(""), oauth_token_secret(""), service_root("https://api.launchpad.net"), api_version("devel"), distributions(), // Default constructor people(), // Default constructor authenticated(false) {} launchpad::launchpad(const std::string& consumer_key_, const std::string& consumer_secret_, const std::string& oauth_token_, const std::string& oauth_token_secret_, const std::string& service_root_, const std::string& api_version_) : consumer_key(consumer_key_), consumer_secret(consumer_secret_), oauth_token(oauth_token_), oauth_token_secret(oauth_token_secret_), service_root(service_root_), api_version(api_version_), distributions(), people(), authenticated(false) {} launchpad::~launchpad() {} std::optional> launchpad::login() { std::string ck, cs, ot, ots; // Try gnome keyring if (read_gnome_keyring_impl(ck, cs, ot, ots)) { std::cout << "Credentials retrieved from GNOME Keyring." << std::endl; } else if (read_plaintext_credentials_impl(ck, cs, ot, ots)) { std::cout << "Credentials retrieved from plaintext keyring." << std::endl; } else { std::cout << "No credentials found. Initiating OAuth flow...\n"; // Request token { std::string url = "https://launchpad.net/+request-token"; std::map params = { {"oauth_consumer_key", "Ubuntu"}, {"oauth_signature_method", "PLAINTEXT"}, {"oauth_signature", "&"}, {"oauth_version", "1.0"} }; auto resp = static_http_post(url, params); if(!resp.has_value()) { std::cerr << "Failed to get request token." << std::endl; return std::nullopt; } std::map token_map; parse_response(resp.value(), token_map); if (token_map.find("oauth_token")==token_map.end() || token_map.find("oauth_token_secret")==token_map.end()) { std::cerr << "Invalid request token response." << std::endl; return std::nullopt; } ot = token_map["oauth_token"]; ots = token_map["oauth_token_secret"]; } // Authorize { std::string auth_url = "https://launchpad.net/+authorize-token?oauth_token=" + url_encode(ot); std::cout << "Please authorize this application by visiting:\n" << auth_url << "\n"; if(!open_url_impl(auth_url)) { std::cout << "Failed to open browser. Please open manually:\n" << auth_url << "\n"; } std::cout << "Press ENTER once authorized..." << std::endl; std::cin.ignore(std::numeric_limits::max(), '\n'); } // Access token { std::string url="https://launchpad.net/+access-token"; std::map params = { {"oauth_consumer_key", "Ubuntu"}, {"oauth_token", ot}, {"oauth_signature_method", "PLAINTEXT"}, {"oauth_signature","&"+url_encode(ots)}, {"oauth_version","1.0"} }; auto resp = static_http_post(url, params); if(!resp.has_value()) { std::cerr << "Failed to get access token." << std::endl; return std::nullopt; } std::map token_map; parse_response(resp.value(), token_map); if (token_map.find("oauth_token")==token_map.end()||token_map.find("oauth_token_secret")==token_map.end()) { std::cerr << "Invalid access token response." << std::endl; return std::nullopt; } ot = token_map["oauth_token"]; ots = token_map["oauth_token_secret"]; if(!create_keyring_credentials_impl("Ubuntu","&",ot,ots)) { if(!create_plaintext_credentials_impl("Ubuntu","&",ot,ots)) { std::cerr << "Failed to store credentials." << std::endl; } } std::cout << "OAuth authentication successful." << std::endl; } ck="Ubuntu"; cs="&"; } auto lp = std::make_shared(ck, cs, ot, ots, "https://api.launchpad.net", "devel"); lp->authenticated = true; lp->distributions = CallableWrapper(lp.get(), [lp](const std::string& key) -> std::optional { return lp->get_distribution(key); }); lp->people = CallableWrapper(lp.get(), [lp](const std::string& key) -> std::optional { return lp->get_person(key); }); return lp; } bool launchpad::is_authenticated() const { return authenticated; } std::string launchpad::build_full_url(const std::string& endpoint) const { if (endpoint.rfind("http", 0) == 0) { return endpoint; } return service_root + "/" + api_version + "/" + endpoint; } std::optional launchpad::api_get(const std::string& endpoint, const std::map& params) const { std::string url = build_full_url(endpoint); if (!params.empty()) { url += "?"; for (const auto& [key, value] : params) { url += url_encode(key) + "=" + url_encode(value) + "&"; } url.pop_back(); } CURL* curl = curl_easy_init(); if (!curl) { std::cerr << "Failed to initialize CURL for GET request." << std::endl; return std::nullopt; } std::string readBuffer; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); std::string auth_header = build_oauth_header_impl( consumer_key, oauth_token, oauth_token_secret, "GET", url, params ); struct curl_slist* headers = nullptr; headers = curl_slist_append(headers, "Accept: application/json"); headers = curl_slist_append(headers, ("Authorization: " + auth_header).c_str()); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); CURLcode res = curl_easy_perform(curl); long http_code = 0; if (res != CURLE_OK) { std::cerr << "CURL GET error: " << curl_easy_strerror(res) << std::endl; } curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); curl_slist_free_all(headers); curl_easy_cleanup(curl); if (res != CURLE_OK) { return std::nullopt; } if (http_code >= 400) { std::cerr << "HTTP GET request failed with code: " << http_code << "\nResponse: " << readBuffer << std::endl; return std::nullopt; } return readBuffer; } std::optional launchpad::api_post( const std::string& endpoint, const std::map& params, bool build_endpoint, const std::string& token_secret_override ) { std::string url = build_endpoint ? build_full_url(endpoint) : endpoint; CURL* curl = curl_easy_init(); if (!curl) { std::cerr << "Failed to initialize CURL for POST request." << std::endl; return std::nullopt; } std::string readBuffer; std::string post_fields; for (const auto& [key, value] : params) { post_fields += url_encode(key) + "=" + url_encode(value) + "&"; } if (!post_fields.empty()) { post_fields.pop_back(); } std::cout << url << std::endl; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_POST, 1L); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_fields.c_str()); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, post_fields.length()); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); std::string secret = token_secret_override.empty() ? oauth_token_secret : token_secret_override; std::string auth_header = build_oauth_header_impl( consumer_key, oauth_token, secret, "POST", url, params ); struct curl_slist* headers = nullptr; headers = curl_slist_append(headers, "Content-Type: application/x-www-form-urlencoded"); headers = curl_slist_append(headers, ("Authorization: " + auth_header).c_str()); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); CURLcode res = curl_easy_perform(curl); long http_code = 0; if (res != CURLE_OK) { std::cerr << "CURL POST error: " << curl_easy_strerror(res) << std::endl; } curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); curl_slist_free_all(headers); curl_easy_cleanup(curl); if (res != CURLE_OK) { return std::nullopt; } if (http_code >= 400) { std::cerr << "HTTP POST request failed with code: " << http_code << "\nResponse: " << readBuffer << std::endl; return std::nullopt; } return readBuffer; } std::optional launchpad::api_patch(const std::string& endpoint, const nlohmann::json& data) { std::string url = build_full_url(endpoint) + "?ws.op=edit"; CURL* curl = curl_easy_init(); if (!curl) { std::cerr << "Failed to initialize CURL for PATCH request." << std::endl; return std::nullopt; } std::string readBuffer; std::string payload = data.dump(); curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload.c_str()); curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST"); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, payload.size()); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); std::map emptyParams; std::string auth_header = build_oauth_header_impl( consumer_key, oauth_token, oauth_token_secret, "POST", url, emptyParams ); struct curl_slist* headers = nullptr; headers = curl_slist_append(headers, "Content-Type: application/json"); headers = curl_slist_append(headers, ("Authorization: " + auth_header).c_str()); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); CURLcode res = curl_easy_perform(curl); long http_code = 0; if (res != CURLE_OK) { std::cerr << "CURL PATCH error: " << curl_easy_strerror(res) << std::endl; } curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); curl_slist_free_all(headers); curl_easy_cleanup(curl); if (res != CURLE_OK) return std::nullopt; if (http_code >= 400) { std::cerr << "HTTP PATCH request failed with code: " << http_code << "\nResponse: " << readBuffer << std::endl; return std::nullopt; } return readBuffer; } std::optional launchpad::api_delete(const std::string& endpoint) { // We'll try DELETE method: std::string url = build_full_url(endpoint); CURL* curl = curl_easy_init(); if (!curl) { std::cerr << "Failed to initialize CURL for DELETE request." << std::endl; return std::nullopt; } std::string readBuffer; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); std::map emptyParams; std::string auth_header = build_oauth_header_impl( consumer_key, oauth_token, oauth_token_secret, "DELETE", url, emptyParams ); struct curl_slist* headers = nullptr; headers = curl_slist_append(headers, ("Authorization: " + auth_header).c_str()); headers = curl_slist_append(headers, "Accept: application/json"); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); CURLcode res = curl_easy_perform(curl); long http_code = 0; if (res != CURLE_OK) { std::cerr << "CURL DELETE error: " << curl_easy_strerror(res) << std::endl; } curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); curl_slist_free_all(headers); curl_easy_cleanup(curl); if (res != CURLE_OK) return std::nullopt; if (http_code >= 400) { std::cerr << "HTTP DELETE request failed with code: " << http_code << "\nResponse: " << readBuffer << std::endl; return std::nullopt; } return readBuffer; } std::optional launchpad::get_person(const std::string& name_) { std::string endpoint = "people/" + url_encode(name_); auto response = api_get(endpoint); if (response.has_value()) { std::optional p_opt = person::parse(response.value()); if (p_opt.has_value()) { p_opt->set_lp(this); return p_opt; } } return std::nullopt; } std::optional launchpad::get_distribution(const std::string& name_) { std::string endpoint = url_encode(name_); auto response = api_get(endpoint); if (response.has_value()) { std::optional d_opt = distribution::parse(response.value()); if (d_opt.has_value()) { d_opt->set_lp(this); return d_opt; } } return std::nullopt; } std::optional launchpad::get_archive(const std::string& distribution_name) { std::string endpoint = url_encode(distribution_name) + "/+archive/primary"; auto response = api_get(endpoint); if (response.has_value()) { std::optional a_opt = archive::parse(response.value()); if (a_opt.has_value()) { a_opt->set_lp(this); return a_opt; } } return std::nullopt; }