diff --git a/include/multipass/cli/command.h b/include/multipass/cli/command.h index d5714ba8fa..0dfc1ebf2f 100644 --- a/include/multipass/cli/command.h +++ b/include/multipass/cli/command.h @@ -30,6 +30,8 @@ #include #include +#include +#include namespace multipass { @@ -102,14 +104,17 @@ class Command : private DisabledCopyMove if (tokens[0] == "unix") { socket_address = tokens[1]; - QLocalSocket multipassd_socket; - multipassd_socket.connectToServer(QString::fromStdString(socket_address)); - if (!multipassd_socket.waitForConnected() && - multipassd_socket.error() == QLocalSocket::SocketAccessError) + try + { + Poco::Net::SocketAddress local_address(socket_address); + Poco::Net::StreamSocket socket; + socket.connect(local_address, Poco::Timespan(5, 0)); // 5 seconds timeout + } + catch (const Poco::Exception& e) { grpc::Status denied_status{ grpc::StatusCode::PERMISSION_DENIED, "multipass socket access denied", - fmt::format("Please check that you have read/write permissions to '{}'", socket_address)}; + fmt::format("Please check that you have read/write permissions to '{}': {}", socket_address, e.displayText())}; return handle_failure(denied_status); } } diff --git a/include/multipass/network_access_manager.h b/include/multipass/network_access_manager.h index 8be5418074..63521d6a5b 100644 --- a/include/multipass/network_access_manager.h +++ b/include/multipass/network_access_manager.h @@ -18,26 +18,47 @@ #ifndef MULTIPASS_NETWORK_ACCESS_MANAGER_H #define MULTIPASS_NETWORK_ACCESS_MANAGER_H -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include namespace multipass { -class NetworkAccessManager : public QNetworkAccessManager +class NetworkAccessManager { - Q_OBJECT public: using UPtr = std::unique_ptr; + NetworkAccessManager(); + ~NetworkAccessManager(); - NetworkAccessManager(QObject* parent = nullptr); + QByteArray sendRequest(const QUrl& url, const std::string& method, const QByteArray& data = QByteArray(), + const std::map& headers = {}); -protected: - QNetworkReply* createRequest(Operation op, const QNetworkRequest& orig_request, - QIODevice* outgoingData = nullptr) override; + // New method for multipart requests + QByteArray sendMultipartRequest(const QUrl& url, const std::string& method, + const std::vector>& parts, + const std::map& headers = {}); + +private: + QByteArray sendUnixRequest(const QUrl& url, const std::string& method, const QByteArray& data, + const std::map& headers); + + QByteArray sendUnixMultipartRequest(const QUrl& url, const std::string& method, + const std::vector>& parts, + const std::map& headers); }; + } // namespace multipass #endif // MULTIPASS_NETWORK_ACCESS_MANAGER_H diff --git a/src/network/network_access_manager.cpp b/src/network/network_access_manager.cpp index fd5c3c77e4..8d1e269f99 100644 --- a/src/network/network_access_manager.cpp +++ b/src/network/network_access_manager.cpp @@ -15,60 +15,189 @@ * */ -#include "local_socket_reply.h" - -#include -#include #include +#include +#include +#include +#include namespace mp = multipass; -mp::NetworkAccessManager::NetworkAccessManager(QObject* parent) : QNetworkAccessManager(parent) +mp::NetworkAccessManager::NetworkAccessManager() +{ +} + +mp::NetworkAccessManager::~NetworkAccessManager() { } -QNetworkReply* mp::NetworkAccessManager::createRequest(QNetworkAccessManager::Operation operation, - const QNetworkRequest& orig_request, QIODevice* device) +QByteArray mp::NetworkAccessManager::sendRequest(const QUrl& url, const std::string& method, const QByteArray& data, + const std::map& headers) { - auto scheme = orig_request.url().scheme(); + auto scheme = url.scheme(); - // To support http requests over Unix sockets, the initial URL needs to be in the form of: - // unix:///path/to/unix_socket@path/in/server (or 'local' instead of 'unix') - // - // For example, to get the general LXD configuration when LXD is installed as a snap: - // unix:////var/snap/lxd/common/lxd/unix.socket@1.0 if (scheme == "unix" || scheme == "local") { - const auto url_parts = orig_request.url().toString().split('@'); - if (url_parts.count() != 2) + return sendUnixRequest(url, method, data, headers); + } + else + { + throw std::runtime_error("Only UNIX socket requests are supported"); + } +} + +QByteArray mp::NetworkAccessManager::sendMultipartRequest(const QUrl& url, const std::string& method, + const std::vector>& parts, + const std::map& headers) +{ + auto scheme = url.scheme(); + + if (scheme == "unix" || scheme == "local") + { + return sendUnixMultipartRequest(url, method, parts, headers); + } + else + { + throw std::runtime_error("Only UNIX socket requests are supported"); + } +} + +QByteArray mp::NetworkAccessManager::sendUnixRequest(const QUrl& url, const std::string& method, const QByteArray& data, + const std::map& headers) +{ + // Parse the URL to get the socket path and the request path + auto url_str = url.toString(); + auto url_parts = url_str.split('@'); + if (url_parts.count() != 2) + { + throw std::runtime_error("The local socket scheme is malformed."); + } + + auto socket_path = QUrl(url_parts[0]).path().toStdString(); + auto request_path = url_parts[1].toStdString(); + + try + { + // Create a local stream socket and connect to the UNIX socket + Poco::Net::SocketAddress local_address(socket_path); + Poco::Net::StreamSocket local_socket; + local_socket.connect(local_address); + + // Create an HTTP client session with the local socket + Poco::Net::HTTPClientSession session(local_socket); + + // Create the request + Poco::Net::HTTPRequest request(method, "/" + request_path, Poco::Net::HTTPMessage::HTTP_1_1); + if (!data.isEmpty()) + request.setContentLength(data.size()); + + // Set headers + for (const auto& header : headers) { - throw LocalSocketConnectionException("The local socket scheme is malformed."); + request.set(header.first, header.second); } - const auto socket_path = QUrl(url_parts[0]).path(); + // Send the request + std::ostream& os = session.sendRequest(request); + if (!data.isEmpty()) + { + os.write(data.constData(), data.size()); + } - LocalSocketUPtr local_socket = std::make_unique(); + // Receive the response + Poco::Net::HTTPResponse response; + std::istream& rs = session.receiveResponse(response); - local_socket->connectToServer(socket_path); - if (!local_socket->waitForConnected(5000)) + // Read the response data + QByteArray response_data; + char buffer[1024]; + while (rs.read(buffer, sizeof(buffer))) + { + response_data.append(buffer, rs.gcount()); + } + // Read any remaining bytes + if (rs.gcount() > 0) { - throw LocalSocketConnectionException( - fmt::format("Cannot connect to {}: {}", socket_path, local_socket->errorString())); + response_data.append(buffer, rs.gcount()); } - const auto server_path = url_parts[1]; - QNetworkRequest request{orig_request}; + return response_data; + } + catch (Poco::Exception& ex) + { + throw std::runtime_error("Failed to communicate over UNIX socket: " + ex.displayText()); + } +} - QUrl url(QString("/%1").arg(server_path)); - url.setHost(orig_request.url().host()); +QByteArray mp::NetworkAccessManager::sendUnixMultipartRequest(const QUrl& url, const std::string& method, + const std::vector>& parts, + const std::map& headers) +{ + // Parse the URL to get the socket path and the request path + auto url_str = url.toString(); + auto url_parts = url_str.split('@'); + if (url_parts.count() != 2) + { + throw std::runtime_error("The local socket scheme is malformed."); + } - request.setUrl(url); + auto socket_path = QUrl(url_parts[0]).path().toStdString(); + auto request_path = url_parts[1].toStdString(); + + try + { + // Create a local stream socket and connect to the UNIX socket + Poco::Net::SocketAddress local_address(socket_path); + Poco::Net::StreamSocket local_socket; + local_socket.connect(local_address); - // The caller needs to be responsible for freeing the allocated memory - return new LocalSocketReply(std::move(local_socket), request, device); + // Create an HTTP client session with the local socket + Poco::Net::HTTPClientSession session(local_socket); + + // Create the request + Poco::Net::HTTPRequest request(method, "/" + request_path, Poco::Net::HTTPMessage::HTTP_1_1); + + // Set headers + for (const auto& header : headers) + { + request.set(header.first, header.second); + } + + // Create the HTMLForm and add parts + Poco::Net::HTMLForm form(Poco::Net::HTMLForm::ENCODING_MULTIPART); + for (const auto& part : parts) + { + form.addPart(part.first, part.second); // HTMLForm takes ownership of PartSource* + } + + // Prepare the request with the form + form.prepareSubmit(request); + + // Send the request + std::ostream& os = session.sendRequest(request); + form.write(os); + + // Receive the response + Poco::Net::HTTPResponse response; + std::istream& rs = session.receiveResponse(response); + + // Read the response data + QByteArray response_data; + char buffer[1024]; + while (rs.read(buffer, sizeof(buffer))) + { + response_data.append(buffer, rs.gcount()); + } + // Read any remaining bytes + if (rs.gcount() > 0) + { + response_data.append(buffer, rs.gcount()); + } + + return response_data; } - else + catch (Poco::Exception& ex) { - return QNetworkAccessManager::createRequest(operation, orig_request, device); + throw std::runtime_error("Failed to communicate over UNIX socket: " + ex.displayText()); } } diff --git a/src/platform/backends/lxd/lxd_request.cpp b/src/platform/backends/lxd/lxd_request.cpp index 857512b085..f457e7c128 100644 --- a/src/platform/backends/lxd/lxd_request.cpp +++ b/src/platform/backends/lxd/lxd_request.cpp @@ -22,10 +22,9 @@ #include #include -#include #include -#include -#include +#include +#include namespace mp = multipass; namespace mpl = multipass::logging; @@ -34,13 +33,11 @@ namespace { constexpr auto request_category = "lxd request"; -template -const QJsonObject lxd_request_common(const std::string& method, QUrl& url, int timeout, Callable&& handle_request) +const QJsonObject lxd_request_common(mp::NetworkAccessManager* manager, const std::string& method, QUrl& url, + const QByteArray& data, + const std::vector>* parts, + const std::map& headers, int timeout) { - QEventLoop event_loop; - QTimer download_timeout; - download_timeout.setInterval(timeout); - if (url.host().isEmpty()) { url.setHost(mp::lxd_project_name); @@ -57,66 +54,43 @@ const QJsonObject lxd_request_common(const std::string& method, QUrl& url, int t } mpl::log(mpl::Level::trace, request_category, fmt::format("Requesting LXD: {} {}", method, url.toString())); - QNetworkRequest request{url}; - - request.setHeader(QNetworkRequest::UserAgentHeader, QString("Multipass/%1").arg(mp::version_string)); - - auto verb = QByteArray::fromStdString(method); - - auto reply = handle_request(request, verb); - QObject::connect(reply, &QNetworkReply::finished, &event_loop, &QEventLoop::quit); - QObject::connect(&download_timeout, &QTimer::timeout, [&]() { - download_timeout.stop(); - reply->abort(); - }); - - if (!reply->isFinished()) + QByteArray reply_data; + if (parts) { - download_timeout.start(); - event_loop.exec(); + // Multipart request + reply_data = manager->sendMultipartRequest(url, method, *parts, headers); + } + else + { + // Regular request + reply_data = manager->sendRequest(url, method, data, headers); } - if (reply->error() == QNetworkReply::ContentNotFoundError) - throw mp::LXDNotFoundException(); - - if (reply->error() == QNetworkReply::OperationCanceledError) - throw mp::LXDRuntimeError( - fmt::format("Timeout getting response for {} operation on {}", method, url.toString())); - - auto bytearray_reply = reply->readAll(); - reply->deleteLater(); - - if (bytearray_reply.isEmpty()) - throw mp::LXDRuntimeError(fmt::format("Empty reply received for {} operation on {}", method, url.toString())); + if (reply_data.isEmpty()) + throw mp::LXDRuntimeError(fmt::format("Empty reply received for {} operation on {}", method, url.toString().toStdString())); QJsonParseError json_error; - auto json_reply = QJsonDocument::fromJson(bytearray_reply, &json_error); + auto json_reply = QJsonDocument::fromJson(reply_data, &json_error); if (json_error.error != QJsonParseError::NoError) { std::string error_string{ - fmt::format("Error parsing JSON response for {}: {}", url.toString(), json_error.errorString())}; + fmt::format("Error parsing JSON response for {}: {}", url.toString().toStdString(), json_error.errorString().toStdString())}; - mpl::log(mpl::Level::debug, request_category, fmt::format("{}\n{}", error_string, bytearray_reply)); + mpl::log(mpl::Level::debug, request_category, fmt::format("{}\n{}", error_string, reply_data.toStdString())); throw mp::LXDJsonParseError(error_string); } if (json_reply.isNull() || !json_reply.isObject()) { - std::string error_string{fmt::format("Invalid LXD response for {}", url.toString())}; + std::string error_string{fmt::format("Invalid LXD response for {}", url.toString().toStdString())}; - mpl::log(mpl::Level::debug, request_category, fmt::format("{}\n{}", error_string, bytearray_reply)); + mpl::log(mpl::Level::debug, request_category, fmt::format("{}\n{}", error_string, reply_data.toStdString())); throw mp::LXDJsonParseError(error_string); } - mpl::log(mpl::Level::trace, request_category, fmt::format("Got reply: {}", QJsonDocument(json_reply).toJson())); - - if (reply->error() != QNetworkReply::NoError) - throw mp::LXDNetworkError(fmt::format("Network error for {}: {} - {}", - url.toString(), - reply->errorString(), - json_reply.object()["error"].toString())); + mpl::log(mpl::Level::trace, request_category, fmt::format("Got reply: {}", QString(json_reply.toJson()).toStdString())); return json_reply.object(); } @@ -124,60 +98,36 @@ const QJsonObject lxd_request_common(const std::string& method, QUrl& url, int t const QJsonObject mp::lxd_request(mp::NetworkAccessManager* manager, const std::string& method, QUrl url, const std::optional& json_data, int timeout) -try { - auto handle_request = [manager, &json_data](QNetworkRequest& request, const QByteArray& verb) { - QByteArray data; - if (json_data) - { - data = QJsonDocument(*json_data).toJson(QJsonDocument::Compact); - - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setHeader(QNetworkRequest::ContentLengthHeader, QByteArray::number(data.size())); + QByteArray data; + std::map headers; - mpl::log(mpl::Level::trace, request_category, fmt::format("Sending data: {}", data)); - } + if (json_data) + { + data = QJsonDocument(*json_data).toJson(QJsonDocument::Compact); - return manager->sendCustomRequest(request, verb, data); - }; + headers["Content-Type"] = "application/json"; + headers["Content-Length"] = std::to_string(data.size()); - return lxd_request_common(method, url, timeout, handle_request); -} -catch (const LXDNetworkError& e) -{ - mpl::log(mpl::Level::warning, request_category, e.what()); - - throw; -} -catch (const LXDRuntimeError& e) -{ - mpl::log(mpl::Level::error, request_category, e.what()); + mpl::log(mpl::Level::trace, request_category, fmt::format("Sending data: {}", data.toStdString())); + } - throw; + return lxd_request_common(manager, method, url, data, nullptr, headers, timeout); } const QJsonObject mp::lxd_request(mp::NetworkAccessManager* manager, const std::string& method, QUrl url, - QHttpMultiPart& multi_part, int timeout) -try + const std::vector>& parts, + int timeout) { - auto handle_request = [manager, &multi_part](QNetworkRequest& request, const QByteArray& verb) { - request.setRawHeader("Transfer-Encoding", "chunked"); - - return manager->sendCustomRequest(request, verb, &multi_part); - }; + std::map headers; + // Set any headers if necessary - return lxd_request_common(method, url, timeout, handle_request); + return lxd_request_common(manager, method, url, QByteArray(), &parts, headers, timeout); } -catch (const LXDRuntimeError& e) -{ - mpl::log(mpl::Level::error, request_category, e.what()); - throw; -} const QJsonObject mp::lxd_wait(mp::NetworkAccessManager* manager, const QUrl& base_url, const QJsonObject& task_data, int timeout) -try { QJsonObject task_reply; @@ -193,26 +143,20 @@ try if (task_reply["error_code"].toInt() >= 400) { throw mp::LXDRuntimeError(fmt::format("Error waiting on operation: ({}) {}", - task_reply["error_code"].toInt(), task_reply["error"].toString())); + task_reply["error_code"].toInt(), task_reply["error"].toString().toStdString())); } else if (task_reply["status_code"].toInt() >= 400) { throw mp::LXDRuntimeError(fmt::format("Failure waiting on operation: ({}) {}", - task_reply["status_code"].toInt(), task_reply["status"].toString())); + task_reply["status_code"].toInt(), task_reply["status"].toString().toStdString())); } else if (task_reply["metadata"].toObject()["status_code"].toInt() >= 400) { throw mp::LXDRuntimeError(fmt::format("Operation completed with error: ({}) {}", task_reply["metadata"].toObject()["status_code"].toInt(), - task_reply["metadata"].toObject()["err"].toString())); + task_reply["metadata"].toObject()["err"].toString().toStdString())); } } return task_reply; } -catch (const LXDRuntimeError& e) -{ - mpl::log(mpl::Level::error, request_category, e.what()); - - throw; -} diff --git a/src/platform/backends/lxd/lxd_request.h b/src/platform/backends/lxd/lxd_request.h index 4c352c6a33..b853e8ca57 100644 --- a/src/platform/backends/lxd/lxd_request.h +++ b/src/platform/backends/lxd/lxd_request.h @@ -24,6 +24,7 @@ #include #include +#include namespace multipass { @@ -69,7 +70,8 @@ const QJsonObject lxd_request(NetworkAccessManager* manager, const std::string& int timeout = 30000 /* in milliseconds */); const QJsonObject lxd_request(NetworkAccessManager* manager, const std::string& method, QUrl url, - QHttpMultiPart& multi_part, int timeout = 30000 /* in milliseconds */); + const std::vector>& parts, + int timeout = 30000 /* in milliseconds */); const QJsonObject lxd_wait(NetworkAccessManager* manager, const QUrl& base_url, const QJsonObject& task_data, int timeout /* in milliseconds */); diff --git a/src/platform/backends/lxd/lxd_vm_image_vault.cpp b/src/platform/backends/lxd/lxd_vm_image_vault.cpp index b2b7cd05af..866fb3d2b5 100644 --- a/src/platform/backends/lxd/lxd_vm_image_vault.cpp +++ b/src/platform/backends/lxd/lxd_vm_image_vault.cpp @@ -37,6 +37,7 @@ #include #include +#include #include #include #include @@ -46,6 +47,9 @@ #include #include +#include +#include + #include #include @@ -582,39 +586,27 @@ void mp::LXDVMImageVault::poll_download_operation(const QJsonObject& json_reply, std::string mp::LXDVMImageVault::lxd_import_metadata_and_image(const QString& metadata_path, const QString& image_path) { - QHttpMultiPart lxd_multipart{QHttpMultiPart::FormDataType}; - QFileInfo metadata_info{metadata_path}, image_info{image_path}; - - QHttpPart metadata_part; - metadata_part.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream")); - metadata_part.setHeader( - QNetworkRequest::ContentDispositionHeader, - QVariant(QString("form-data; name=\"metadata\"; filename=\"%1\"").arg(metadata_info.fileName()))); - QFile* metadata_file = new QFile(metadata_path); - metadata_file->open(QIODevice::ReadOnly); - metadata_part.setBodyDevice(metadata_file); - metadata_file->setParent(&lxd_multipart); - - QHttpPart image_part; - image_part.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream")); - image_part.setHeader( - QNetworkRequest::ContentDispositionHeader, - QVariant(QString("form-data; name=\"rootfs.img\"; filename=\"%1\"").arg(image_info.fileName()))); - QFile* image_file = new QFile(image_path); - image_file->open(QIODevice::ReadOnly); - image_part.setBodyDevice(image_file); - image_file->setParent(&lxd_multipart); - - lxd_multipart.append(metadata_part); - lxd_multipart.append(image_part); - - auto json_reply = lxd_request(manager, "POST", QUrl(QString("%1/images").arg(base_url.toString())), lxd_multipart); + // Prepare the parts + std::vector> parts; + + // Metadata part + parts.emplace_back("metadata", new Poco::Net::FilePartSource(metadata_path.toStdString(), "application/octet-stream")); + + // Image part + parts.emplace_back("rootfs.img", new Poco::Net::FilePartSource(image_path.toStdString(), "application/octet-stream")); + + // You can set any additional headers if required + std::map headers; + // For example, you might want to set the 'Transfer-Encoding' header if necessary + // headers["Transfer-Encoding"] = "chunked"; + + // Make the multipart request + auto json_reply = lxd_request(manager, "POST", QUrl(QString("%1/images").arg(base_url.toString())), parts); auto task_reply = lxd_wait(manager, base_url, json_reply, 300000); return task_reply["metadata"].toObject()["metadata"].toObject()["fingerprint"].toString().toStdString(); } - std::string mp::LXDVMImageVault::get_lxd_image_hash_for(const QString& id) { auto images = retrieve_image_list();