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
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;
|
|
}
|