You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

488 lines
17 KiB

// Copyright (C) 2024 Simon Quigley <tsimonq2@ubuntu.com>
//
// 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 <https://www.gnu.org/licenses/>.
#include "launchpad.h"
#include "authentication.h"
#include "person.h"
#include "distribution.h"
#include "archive.h"
#include "utils.h"
#include <curl/curl.h>
#include <iostream>
#include <sstream>
#include <nlohmann/json.hpp>
#include <regex>
static size_t StaticWriteCallback(void* contents, size_t size, size_t nmemb, void* userp) {
size_t totalSize = size * nmemb;
std::string* mem = static_cast<std::string*>(userp);
mem->append(static_cast<char*>(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<std::string> launchpad::static_http_post(const std::string& url,
const std::map<std::string,std::string>& 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<std::shared_ptr<launchpad>> 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<std::string,std::string> 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<std::string,std::string> 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<std::streamsize>::max(), '\n');
}
// Access token
{
std::string url="https://launchpad.net/+access-token";
std::map<std::string,std::string> 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<std::string,std::string> 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<launchpad>(ck, cs, ot, ots, "https://api.launchpad.net", "devel");
lp->authenticated = true;
lp->distributions = CallableWrapper<distribution>(lp.get(), [lp](const std::string& key) -> std::optional<distribution> {
return lp->get_distribution(key);
});
lp->people = CallableWrapper<person>(lp.get(), [lp](const std::string& key) -> std::optional<person> {
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<std::string> launchpad::api_get(const std::string& endpoint, const std::map<std::string, std::string>& 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<std::string> launchpad::api_post(
const std::string& endpoint,
const std::map<std::string, std::string>& 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<std::string> 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<std::string, std::string> 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<std::string> 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<std::string, std::string> 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<person> 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<person> p_opt = person::parse(response.value());
if (p_opt.has_value()) {
p_opt->set_lp(this);
return p_opt;
}
}
return std::nullopt;
}
std::optional<distribution> launchpad::get_distribution(const std::string& name_) {
std::string endpoint = url_encode(name_);
auto response = api_get(endpoint);
if (response.has_value()) {
std::optional<distribution> d_opt = distribution::parse(response.value());
if (d_opt.has_value()) {
d_opt->set_lp(this);
return d_opt;
}
}
return std::nullopt;
}
std::optional<archive> 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<archive> a_opt = archive::parse(response.value());
if (a_opt.has_value()) {
a_opt->set_lp(this);
return a_opt;
}
}
return std::nullopt;
}