From 5442e32abc5a6bf5c63a1712b55bf10a693da8e2 Mon Sep 17 00:00:00 2001 From: Gary Wang Date: Fri, 24 Apr 2026 20:20:24 +0800 Subject: [PATCH] init commit --- CMakeLists.txt | 32 +++ src/app/AppController.cpp | 151 ++++++++++ src/app/AppController.h | 63 +++++ src/app/Application.cpp | 9 + src/app/Application.h | 12 + src/app/CMakeLists.txt | 23 ++ src/app/main.cpp | 33 +++ src/app/qml/main.qml | 178 ++++++++++++ src/core/CMakeLists.txt | 52 ++++ src/core/include/LocalSendCore/Constants.h | 27 ++ src/core/include/LocalSendCore/Device.h | 41 +++ .../include/LocalSendCore/DiscoveryManager.h | 48 ++++ src/core/include/LocalSendCore/DtoTypes.h | 102 +++++++ src/core/include/LocalSendCore/HttpClient.h | 51 ++++ src/core/include/LocalSendCore/HttpServer.h | 63 +++++ .../LocalSendCore/MulticastDiscovery.h | 47 ++++ .../include/LocalSendCore/SecurityContext.h | 41 +++ .../include/LocalSendCore/SessionManager.h | 86 ++++++ src/core/include/LocalSendCore/Settings.h | 46 +++ src/core/include/LocalSendCore/Types.h | 57 ++++ src/core/src/Constants.cpp | 1 + src/core/src/Device.cpp | 16 ++ src/core/src/DiscoveryManager.cpp | 85 ++++++ src/core/src/DtoTypes.cpp | 192 +++++++++++++ src/core/src/HttpClient.cpp | 182 ++++++++++++ src/core/src/HttpServer.cpp | 213 ++++++++++++++ src/core/src/MulticastDiscovery.cpp | 130 +++++++++ src/core/src/SecurityContext.cpp | 141 ++++++++++ src/core/src/SessionManager.cpp | 263 ++++++++++++++++++ src/core/src/Settings.cpp | 114 ++++++++ src/core/src/Types.cpp | 37 +++ tests/CMakeLists.txt | 7 + tests/core/TestDtoTypes.cpp | 122 ++++++++ tests/core/TestMulticastDiscovery.cpp | 19 ++ 34 files changed, 2684 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 src/app/AppController.cpp create mode 100644 src/app/AppController.h create mode 100644 src/app/Application.cpp create mode 100644 src/app/Application.h create mode 100644 src/app/CMakeLists.txt create mode 100644 src/app/main.cpp create mode 100644 src/app/qml/main.qml create mode 100644 src/core/CMakeLists.txt create mode 100644 src/core/include/LocalSendCore/Constants.h create mode 100644 src/core/include/LocalSendCore/Device.h create mode 100644 src/core/include/LocalSendCore/DiscoveryManager.h create mode 100644 src/core/include/LocalSendCore/DtoTypes.h create mode 100644 src/core/include/LocalSendCore/HttpClient.h create mode 100644 src/core/include/LocalSendCore/HttpServer.h create mode 100644 src/core/include/LocalSendCore/MulticastDiscovery.h create mode 100644 src/core/include/LocalSendCore/SecurityContext.h create mode 100644 src/core/include/LocalSendCore/SessionManager.h create mode 100644 src/core/include/LocalSendCore/Settings.h create mode 100644 src/core/include/LocalSendCore/Types.h create mode 100644 src/core/src/Constants.cpp create mode 100644 src/core/src/Device.cpp create mode 100644 src/core/src/DiscoveryManager.cpp create mode 100644 src/core/src/DtoTypes.cpp create mode 100644 src/core/src/HttpClient.cpp create mode 100644 src/core/src/HttpServer.cpp create mode 100644 src/core/src/MulticastDiscovery.cpp create mode 100644 src/core/src/SecurityContext.cpp create mode 100644 src/core/src/SessionManager.cpp create mode 100644 src/core/src/Settings.cpp create mode 100644 src/core/src/Types.cpp create mode 100644 tests/CMakeLists.txt create mode 100644 tests/core/TestDtoTypes.cpp create mode 100644 tests/core/TestMulticastDiscovery.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..89b983a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.16) +project(LocalSendQt VERSION 1.0.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + +option(BUILD_TESTS "Build unit tests" ON) +option(WITH_HTTP_SERVER "Build with QHttpServer support" ON) + +find_package(Qt6 REQUIRED COMPONENTS Core Network Quick QuickControls2) + +if(WITH_HTTP_SERVER) + find_package(Qt6 COMPONENTS HttpServer) +endif() + +add_subdirectory(src/core) +add_subdirectory(src/app) + +if(BUILD_TESTS) + enable_testing() + find_package(Qt6 REQUIRED COMPONENTS Test) + add_subdirectory(tests) +endif() diff --git a/src/app/AppController.cpp b/src/app/AppController.cpp new file mode 100644 index 0000000..7382ca1 --- /dev/null +++ b/src/app/AppController.cpp @@ -0,0 +1,151 @@ +#include "AppController.h" +#include + +AppController::AppController(QObject* parent) + : QObject(parent) + , m_settings(new LocalSend::Settings(this)) + , m_security(new LocalSend::SecurityContext(this)) + , m_discovery(new LocalSend::DiscoveryManager(this)) + , m_server(new LocalSend::HttpServer(this)) + , m_sessions(new LocalSend::SessionManager(this)) +{ +} + +AppController::~AppController() +{ + stopDiscovery(); +} + +void AppController::initialize() +{ + m_security->initialize(); + + LocalSend::InfoDto info = buildInfoDto(); + m_server->setLocalInfo(info, m_security->fingerprint()); + m_discovery->setLocalInfo(info, m_security->fingerprint(), m_settings->port(), + m_settings->https() ? LocalSend::ProtocolType::Https : LocalSend::ProtocolType::Http); + + if (m_server->start(m_settings->port(), m_settings->https())) { + emit serverRunningChanged(); + } + + connect(m_discovery, &LocalSend::DiscoveryManager::deviceDiscovered, + this, &AppController::onDeviceDiscovered); + connect(m_discovery, &LocalSend::DiscoveryManager::deviceLost, + this, &AppController::onDeviceLost); + connect(m_server, &LocalSend::HttpServer::prepareUploadRequest, + this, &AppController::onPrepareUploadRequest); + + startDiscovery(); +} + +LocalSend::InfoDto AppController::buildInfoDto() const +{ + LocalSend::InfoDto info; + info.alias = m_settings->alias(); + info.version = m_settings->version(); + info.deviceModel = m_settings->deviceModel(); + info.deviceType = m_settings->deviceType(); + info.download = false; + return info; +} + +QString AppController::alias() const +{ + return m_settings->alias(); +} + +void AppController::setAlias(const QString& alias) +{ + if (m_settings->alias() != alias) { + m_settings->setAlias(alias); + emit aliasChanged(); + } +} + +quint16 AppController::port() const +{ + return m_settings->port(); +} + +void AppController::setPort(quint16 port) +{ + if (m_settings->port() != port) { + m_settings->setPort(port); + emit portChanged(); + } +} + +QVariantList AppController::devices() const +{ + QVariantList result; + for (const LocalSend::Device& device : m_devices) { + QVariantMap map; + map[QStringLiteral("ip")] = device.ip; + map[QStringLiteral("port")] = device.port; + map[QStringLiteral("alias")] = device.alias; + map[QStringLiteral("fingerprint")] = device.fingerprint; + map[QStringLiteral("deviceModel")] = device.deviceModel; + map[QStringLiteral("deviceType")] = LocalSend::deviceTypeToString(device.deviceType); + result.append(map); + } + return result; +} + +bool AppController::serverRunning() const +{ + return m_server->isRunning(); +} + +void AppController::startDiscovery() +{ + m_discovery->start(); +} + +void AppController::stopDiscovery() +{ + m_discovery->stop(); +} + +void AppController::refreshDevices() +{ + m_discovery->startScan(); +} + +void AppController::onDeviceDiscovered(const LocalSend::Device& device) +{ + m_devices.insert(device.fingerprint, device); + emit devicesChanged(); +} + +void AppController::onDeviceLost(const QString& fingerprint) +{ + m_devices.remove(fingerprint); + emit devicesChanged(); +} + +void AppController::onPrepareUploadRequest(const LocalSend::PrepareUploadRequestDto& dto, + const QHostAddress& sender) +{ + LocalSend::Device device; + device.ip = sender.toString(); + device.alias = dto.info.alias; + device.fingerprint = dto.info.fingerprint; + device.deviceModel = dto.info.deviceModel; + device.deviceType = dto.info.deviceType; + device.version = dto.info.version; + + QString sessionId = m_sessions->createReceiveSession(device, dto.files); + + QVariantList filesList; + for (auto it = dto.files.constBegin(); it != dto.files.constEnd(); ++it) { + QVariantMap file; + file[QStringLiteral("id")] = it.key(); + file[QStringLiteral("fileName")] = it.value().fileName; + file[QStringLiteral("size")] = it.value().size; + file[QStringLiteral("fileType")] = it.value().fileType; + filesList.append(file); + } + + emit receiveRequest(sessionId, dto.info.alias, filesList); +} diff --git a/src/app/AppController.h b/src/app/AppController.h new file mode 100644 index 0000000..58bb1db --- /dev/null +++ b/src/app/AppController.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include "LocalSendCore/DiscoveryManager.h" +#include "LocalSendCore/HttpServer.h" +#include "LocalSendCore/HttpClient.h" +#include "LocalSendCore/SessionManager.h" +#include "LocalSendCore/SecurityContext.h" +#include "LocalSendCore/Settings.h" + +class AppController : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString alias READ alias WRITE setAlias NOTIFY aliasChanged) + Q_PROPERTY(quint16 port READ port WRITE setPort NOTIFY portChanged) + Q_PROPERTY(QVariantList devices READ devices NOTIFY devicesChanged) + Q_PROPERTY(bool serverRunning READ serverRunning NOTIFY serverRunningChanged) + +public: + explicit AppController(QObject* parent = nullptr); + ~AppController() override; + + void initialize(); + + QString alias() const; + void setAlias(const QString& alias); + + quint16 port() const; + void setPort(quint16 port); + + QVariantList devices() const; + bool serverRunning() const; + + Q_INVOKABLE void startDiscovery(); + Q_INVOKABLE void stopDiscovery(); + Q_INVOKABLE void refreshDevices(); + +signals: + void aliasChanged(); + void portChanged(); + void devicesChanged(); + void serverRunningChanged(); + void receiveRequest(const QString& sessionId, const QString& senderAlias, const QVariantList& files); + void sendProgress(const QString& sessionId, double progress); + void sendCompleted(const QString& sessionId); + +private slots: + void onDeviceDiscovered(const LocalSend::Device& device); + void onDeviceLost(const QString& fingerprint); + void onPrepareUploadRequest(const LocalSend::PrepareUploadRequestDto& dto, const QHostAddress& sender); + +private: + LocalSend::Settings* m_settings = nullptr; + LocalSend::SecurityContext* m_security = nullptr; + LocalSend::DiscoveryManager* m_discovery = nullptr; + LocalSend::HttpServer* m_server = nullptr; + LocalSend::SessionManager* m_sessions = nullptr; + + QMap m_devices; + + LocalSend::InfoDto buildInfoDto() const; +}; diff --git a/src/app/Application.cpp b/src/app/Application.cpp new file mode 100644 index 0000000..7a295e8 --- /dev/null +++ b/src/app/Application.cpp @@ -0,0 +1,9 @@ +#include "Application.h" + +Application::Application(int& argc, char** argv) + : QGuiApplication(argc, argv) +{ + setApplicationName(QStringLiteral("LocalSendQt")); + setApplicationVersion(QStringLiteral("1.0.0")); + setOrganizationName(QStringLiteral("LocalSend")); +} diff --git a/src/app/Application.h b/src/app/Application.h new file mode 100644 index 0000000..1232ccc --- /dev/null +++ b/src/app/Application.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +class Application : public QGuiApplication +{ + Q_OBJECT + +public: + Application(int& argc, char** argv); +}; diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt new file mode 100644 index 0000000..c192125 --- /dev/null +++ b/src/app/CMakeLists.txt @@ -0,0 +1,23 @@ +qt_add_executable(LocalSendQt + main.cpp + Application.cpp + AppController.cpp +) + +qt_add_qml_module(LocalSendQt + URI LocalSend + VERSION 1.0 + QML_FILES + qml/main.qml +) + +target_link_libraries(LocalSendQt PRIVATE + LocalSendCore + Qt6::Quick + Qt6::QuickControls2 +) + +install(TARGETS LocalSendQt + BUNDLE DESTINATION . + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} +) diff --git a/src/app/main.cpp b/src/app/main.cpp new file mode 100644 index 0000000..6b1ec7b --- /dev/null +++ b/src/app/main.cpp @@ -0,0 +1,33 @@ +#include +#include +#include +#include +#include +#include "Application.h" +#include "AppController.h" + +int main(int argc, char *argv[]) +{ + Application app(argc, argv); + QQuickStyle::setStyle("Basic"); + + AppController controller; + controller.initialize(); + + QQmlApplicationEngine engine; + + engine.addImportPath(QStringLiteral("qrc:/")); + + engine.rootContext()->setContextProperty(QStringLiteral("appController"), &controller); + + const QUrl url(u"qrc:/LocalSend/qml/main.qml"_qs); + + QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, + &app, [url](QObject *obj, const QUrl &objUrl) { + if (!obj && url == objUrl) QCoreApplication::exit(-1); + }, Qt::QueuedConnection); + + engine.load(url); + + return app.exec(); +} diff --git a/src/app/qml/main.qml b/src/app/qml/main.qml new file mode 100644 index 0000000..63f0941 --- /dev/null +++ b/src/app/qml/main.qml @@ -0,0 +1,178 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +ApplicationWindow { + id: root + visible: true + width: 800 + height: 600 + title: qsTr("LocalSend") + + StackView { + id: stackView + anchors.fill: parent + + Component.onCompleted: { + push(homePageComponent) + } + } + + Component { + id: homePageComponent + Page { + id: homePage + + signal openSettings() + + header: ToolBar { + RowLayout { + anchors.fill: parent + Label { + text: qsTr("LocalSend") + font.bold: true + font.pixelSize: 20 + Layout.leftMargin: 16 + } + Item { Layout.fillWidth: true } + ToolButton { + text: qsTr("Settings") + onClicked: homePage.openSettings() + } + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 16 + + Label { + text: qsTr("Nearby Devices") + font.bold: true + font.pixelSize: 16 + } + + ListView { + id: deviceListView + Layout.fillWidth: true + Layout.fillHeight: true + + property var devices: appController.devices + model: devices + spacing: 8 + + delegate: Pane { + width: ListView.view.width + padding: 12 + + background: Rectangle { + color: Qt.lighter("gray", 1.8) + radius: 8 + } + + RowLayout { + anchors.fill: parent + + Column { + Layout.fillWidth: true + Label { + text: modelData.alias || modelData.ip + font.bold: true + } + Label { + text: "%1:%2".arg(modelData.ip).arg(modelData.port) + color: "gray" + font.pixelSize: 12 + } + } + + Button { + text: qsTr("Send") + onClicked: { + // send to this device + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + Button { + text: qsTr("Refresh") + onClicked: appController.refreshDevices() + } + Item { Layout.fillWidth: true } + Label { + text: qsTr("Alias: %1").arg(appController.alias) + color: "gray" + } + } + } + + onOpenSettings: stackView.push(settingsPageComponent) + } + } + + Component { + id: settingsPageComponent + Page { + id: settingsPage + signal back() + + header: ToolBar { + RowLayout { + anchors.fill: parent + ToolButton { + text: qsTr("Back") + onClicked: settingsPage.back() + } + Label { + text: qsTr("Settings") + font.bold: true + Layout.fillWidth: true + Layout.leftMargin: 16 + } + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 16 + + GridLayout { + columns: 2 + Layout.fillWidth: true + + Label { text: qsTr("Device Alias:") } + TextField { + id: aliasField + text: appController.alias + onEditingFinished: appController.alias = text + Layout.fillWidth: true + } + + Label { text: qsTr("Port:") } + SpinBox { + id: portField + value: appController.port + from: 1 + to: 65535 + onValueChanged: appController.port = value + } + } + + Item { Layout.fillHeight: true } + + Label { + text: qsTr("Server Status: %1").arg(appController.serverRunning ? qsTr("Running") : qsTr("Stopped")) + color: appController.serverRunning ? "green" : "red" + } + } + + onBack: stackView.pop() + } + } +} diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt new file mode 100644 index 0000000..d8672c4 --- /dev/null +++ b/src/core/CMakeLists.txt @@ -0,0 +1,52 @@ +add_library(LocalSendCore SHARED + src/Constants.cpp + src/Types.cpp + src/Device.cpp + src/DtoTypes.cpp + src/MulticastDiscovery.cpp + src/DiscoveryManager.cpp + src/HttpServer.cpp + src/HttpClient.cpp + src/SessionManager.cpp + src/SecurityContext.cpp + src/Settings.cpp + + include/LocalSendCore/Constants.h + include/LocalSendCore/Types.h + include/LocalSendCore/Device.h + include/LocalSendCore/DtoTypes.h + include/LocalSendCore/MulticastDiscovery.h + include/LocalSendCore/DiscoveryManager.h + include/LocalSendCore/HttpServer.h + include/LocalSendCore/HttpClient.h + include/LocalSendCore/SessionManager.h + include/LocalSendCore/SecurityContext.h + include/LocalSendCore/Settings.h +) + +target_include_directories(LocalSendCore PUBLIC + $ + $ +) + +target_link_libraries(LocalSendCore PUBLIC + Qt6::Core + Qt6::Network +) + +if(Qt6HttpServer_FOUND) + target_link_libraries(LocalSendCore PUBLIC Qt6::HttpServer) + target_compile_definitions(LocalSendCore PUBLIC HAS_QTHTTPSERVER) +endif() + +find_package(OpenSSL) +if(OpenSSL_FOUND) + target_link_libraries(LocalSendCore PUBLIC OpenSSL::SSL OpenSSL::Crypto) + target_compile_definitions(LocalSendCore PUBLIC USE_OPENSSL) +endif() + +set_target_properties(LocalSendCore PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION 1 + OUTPUT_NAME localsend-core +) diff --git a/src/core/include/LocalSendCore/Constants.h b/src/core/include/LocalSendCore/Constants.h new file mode 100644 index 0000000..474926c --- /dev/null +++ b/src/core/include/LocalSendCore/Constants.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +namespace LocalSend { + +constexpr const char* PROTOCOL_VERSION = "2.1"; +constexpr const char* PEER_PROTOCOL_VERSION = "1.0"; +constexpr const char* FALLBACK_PROTOCOL_VERSION = "1.0"; + +constexpr quint16 DEFAULT_PORT = 53317; +constexpr const char* DEFAULT_MULTICAST_GROUP = "224.0.0.167"; +constexpr int DEFAULT_DISCOVERY_TIMEOUT_MS = 500; + +constexpr const char* API_BASE_PATH = "/api/localsend"; +constexpr const char* API_V2_PATH = "/api/localsend/v2"; + +namespace ApiRoute { + constexpr const char* INFO = "/api/localsend/v2/info"; + constexpr const char* REGISTER = "/api/localsend/v2/register"; + constexpr const char* PREPARE_UPLOAD = "/api/localsend/v2/prepare-upload"; + constexpr const char* UPLOAD = "/api/localsend/v2/upload"; + constexpr const char* CANCEL = "/api/localsend/v2/cancel"; + constexpr const char* SHOW = "/api/localsend/v2/show"; +} + +} diff --git a/src/core/include/LocalSendCore/Device.h b/src/core/include/LocalSendCore/Device.h new file mode 100644 index 0000000..5de5aa6 --- /dev/null +++ b/src/core/include/LocalSendCore/Device.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include "LocalSendCore/Types.h" + +namespace LocalSend { + +class Device +{ +public: + QString ip; + quint16 port = 53317; + ProtocolType protocol = ProtocolType::Http; + + QString alias; + QString fingerprint; + QString deviceModel; + DeviceType deviceType = DeviceType::Desktop; + QString version; + bool download = false; + + DiscoveryMethod discoveryMethod = DiscoveryMethod::Multicast; + QDateTime lastSeen; + + Device() = default; + Device(const QString& ip, quint16 port); + + QString displayName() const; + bool isHttps() const { return protocol == ProtocolType::Https; } + + bool operator==(const Device& other) const { + return fingerprint == other.fingerprint; + } + bool operator!=(const Device& other) const { + return !(*this == other); + } +}; + +} diff --git a/src/core/include/LocalSendCore/DiscoveryManager.h b/src/core/include/LocalSendCore/DiscoveryManager.h new file mode 100644 index 0000000..d379bbe --- /dev/null +++ b/src/core/include/LocalSendCore/DiscoveryManager.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include "LocalSendCore/Device.h" +#include "LocalSendCore/DtoTypes.h" +#include "LocalSendCore/MulticastDiscovery.h" + +namespace LocalSend { + +class DiscoveryManager : public QObject +{ + Q_OBJECT + +public: + explicit DiscoveryManager(QObject* parent = nullptr); + ~DiscoveryManager() override; + + void start(); + void stop(); + void startScan(); + + void setLocalInfo(const InfoDto& info, const QString& fingerprint, + quint16 port, ProtocolType protocol); + + QList discoveredDevices() const; + Device deviceByFingerprint(const QString& fingerprint) const; + +signals: + void deviceDiscovered(const Device& device); + void deviceUpdated(const Device& device); + void deviceLost(const QString& fingerprint); + void scanFinished(); + +private slots: + void onDeviceDiscovered(const Device& device); + void onDeviceTimeout(); + +private: + MulticastDiscovery* m_multicastDiscovery = nullptr; + QMap m_devices; + QTimer* m_cleanupTimer = nullptr; + + static constexpr int DEVICE_TIMEOUT_MS = 30000; +}; + +} diff --git a/src/core/include/LocalSendCore/DtoTypes.h b/src/core/include/LocalSendCore/DtoTypes.h new file mode 100644 index 0000000..f966eb6 --- /dev/null +++ b/src/core/include/LocalSendCore/DtoTypes.h @@ -0,0 +1,102 @@ +#pragma once + +#include +#include +#include +#include +#include +#include "LocalSendCore/Types.h" + +namespace LocalSend { + +struct MulticastDto +{ + QString alias; + QString version; + QString deviceModel; + DeviceType deviceType = DeviceType::Desktop; + QString fingerprint; + quint16 port = 53317; + ProtocolType protocol = ProtocolType::Http; + bool download = false; + bool announcement = false; + bool announce = false; + + QJsonObject toJson() const; + static MulticastDto fromJson(const QJsonObject& json); +}; + +struct InfoDto +{ + QString alias; + QString version; + QString deviceModel; + DeviceType deviceType = DeviceType::Desktop; + QString fingerprint; + bool download = false; + + QJsonObject toJson() const; + static InfoDto fromJson(const QJsonObject& json); +}; + +struct RegisterDto +{ + QString alias; + QString version; + QString deviceModel; + DeviceType deviceType = DeviceType::Desktop; + QString fingerprint; + quint16 port = 53317; + ProtocolType protocol = ProtocolType::Http; + bool download = false; + + QJsonObject toJson() const; + static RegisterDto fromJson(const QJsonObject& json); +}; + +struct FileDto +{ + QString id; + QString fileName; + qint64 size = 0; + QString fileType; + QString hash; + QString preview; + + struct Metadata { + QDateTime lastModified; + QDateTime lastAccessed; + }; + std::optional metadata; + + QJsonObject toJson() const; + static FileDto fromJson(const QJsonObject& json); +}; + +struct PrepareUploadRequestDto +{ + RegisterDto info; + QMap files; + + QJsonObject toJson() const; + static PrepareUploadRequestDto fromJson(const QJsonObject& json); +}; + +struct PrepareUploadResponseDto +{ + QString sessionId; + QMap files; + + static PrepareUploadResponseDto fromJson(const QJsonObject& json); +}; + +struct ReceiveRequestResponseDto +{ + QString sessionId; + QMap destinationPaths; + bool cancel = false; + + QJsonObject toJson() const; +}; + +} diff --git a/src/core/include/LocalSendCore/HttpClient.h b/src/core/include/LocalSendCore/HttpClient.h new file mode 100644 index 0000000..c327dbf --- /dev/null +++ b/src/core/include/LocalSendCore/HttpClient.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include +#include +#include +#include "LocalSendCore/Device.h" +#include "LocalSendCore/DtoTypes.h" + +namespace LocalSend { + +class HttpClient : public QObject +{ + Q_OBJECT + +public: + explicit HttpClient(QObject* parent = nullptr); + ~HttpClient() override; + + void setSslConfiguration(const QSslConfiguration& config); + + void getInfo(const Device& device); + void registerDevice(const Device& device, const RegisterDto& dto); + void prepareUpload(const Device& device, const PrepareUploadRequestDto& dto); + void uploadFile(const Device& device, const QString& sessionId, const QString& fileId, + const QString& token, const QString& filePath); + void cancel(const Device& device, const QString& sessionId); + +signals: + void infoReceived(const InfoDto& info); + void infoError(const QString& error); + void registerCompleted(); + void registerError(const QString& error); + void prepareUploadResponse(const PrepareUploadResponseDto& response); + void prepareUploadError(const QString& error); + void uploadProgress(qint64 sent, qint64 total); + void uploadCompleted(); + void uploadError(const QString& error); + +private: + QNetworkAccessManager* m_manager = nullptr; + QSslConfiguration m_sslConfig; + + QNetworkReply* sendGet(const QUrl& url); + QNetworkReply* sendPost(const QUrl& url, const QByteArray& data); + + QUrl buildUrl(const Device& device, const QString& path) const; +}; + +} diff --git a/src/core/include/LocalSendCore/HttpServer.h b/src/core/include/LocalSendCore/HttpServer.h new file mode 100644 index 0000000..4be3890 --- /dev/null +++ b/src/core/include/LocalSendCore/HttpServer.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include "LocalSendCore/Device.h" +#include "LocalSendCore/DtoTypes.h" + +#ifdef HAS_QTHTTPSERVER +#include +#include +#endif + +namespace LocalSend { + +class HttpServer : public QObject +{ + Q_OBJECT + +public: + explicit HttpServer(QObject* parent = nullptr); + ~HttpServer() override; + + bool start(quint16 port, bool https = false); + void stop(); + + void setLocalInfo(const InfoDto& info, const QString& fingerprint); +#ifdef HAS_QTHTTPSERVER + void setSslConfiguration(const QSslConfiguration& config); +#endif + + quint16 serverPort() const; + bool isRunning() const; + +signals: + void registerRequest(const RegisterDto& dto, const QHostAddress& sender); + void prepareUploadRequest(const PrepareUploadRequestDto& dto, const QHostAddress& sender); + void uploadRequest(const QString& sessionId, const QString& fileId, const QString& token, + const QByteArray& data); + void cancelRequest(const QString& sessionId); + +private: +#ifdef HAS_QTHTTPSERVER + QHttpServer* m_server = nullptr; + QSslConfiguration m_sslConfig; +#else + QObject* m_server = nullptr; +#endif + + InfoDto m_localInfo; + QString m_localFingerprint; + quint16 m_port = 0; + +#ifdef HAS_QTHTTPSERVER + void setupRoutes(); + QHttpServerResponse handleInfoRequest(); + QHttpServerResponse handleRegisterRequest(const QHttpServerRequest& request, const QHostAddress& peer); + QHttpServerResponse handlePrepareUploadRequest(const QHttpServerRequest& request); + QHttpServerResponse handleUploadRequest(const QHttpServerRequest& request); + QHttpServerResponse handleCancelRequest(const QHttpServerRequest& request); +#endif +}; + +} diff --git a/src/core/include/LocalSendCore/MulticastDiscovery.h b/src/core/include/LocalSendCore/MulticastDiscovery.h new file mode 100644 index 0000000..d5d4a06 --- /dev/null +++ b/src/core/include/LocalSendCore/MulticastDiscovery.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include +#include "LocalSendCore/Constants.h" +#include "LocalSendCore/Device.h" +#include "LocalSendCore/DtoTypes.h" + +namespace LocalSend { + +class MulticastDiscovery : public QObject +{ + Q_OBJECT + +public: + explicit MulticastDiscovery(QObject* parent = nullptr); + ~MulticastDiscovery() override; + + void start(const QString& multicastGroup = DEFAULT_MULTICAST_GROUP, + quint16 port = DEFAULT_PORT); + void stop(); + void sendAnnouncement(); + + void setLocalInfo(const InfoDto& info, const QString& fingerprint, quint16 port, ProtocolType protocol); + +signals: + void deviceDiscovered(const Device& device); + +private slots: + void onReadyRead(); + +private: + QUdpSocket* m_socket = nullptr; + QString m_multicastGroup; + quint16 m_port = DEFAULT_PORT; + + QString m_localFingerprint; + InfoDto m_localInfo; + quint16 m_localPort = DEFAULT_PORT; + ProtocolType m_localProtocol = ProtocolType::Http; + + void handleMulticastMessage(const QHostAddress& sender, const QByteArray& data); +}; + +} diff --git a/src/core/include/LocalSendCore/SecurityContext.h b/src/core/include/LocalSendCore/SecurityContext.h new file mode 100644 index 0000000..c2a8c43 --- /dev/null +++ b/src/core/include/LocalSendCore/SecurityContext.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace LocalSend { + +class SecurityContext : public QObject +{ + Q_OBJECT + +public: + explicit SecurityContext(QObject* parent = nullptr); + + void initialize(); + void regenerate(); + + QString fingerprint() const { return m_fingerprint; } + QSslCertificate certificate() const { return m_certificate; } + QSslKey privateKey() const { return m_privateKey; } + QSslConfiguration sslConfiguration() const; + + bool isValid() const { return !m_fingerprint.isEmpty(); } + +private: + void loadFromStorage(); + void saveToStorage(); + void generateCertificate(); + QString calculateFingerprint(const QSslCertificate& cert) const; + + QString m_fingerprint; + QSslCertificate m_certificate; + QSslKey m_privateKey; + + QString storagePath() const; +}; + +} diff --git a/src/core/include/LocalSendCore/SessionManager.h b/src/core/include/LocalSendCore/SessionManager.h new file mode 100644 index 0000000..88e311c --- /dev/null +++ b/src/core/include/LocalSendCore/SessionManager.h @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include +#include "LocalSendCore/Device.h" +#include "LocalSendCore/DtoTypes.h" + +namespace LocalSend { + +struct FileTransfer +{ + FileDto file; + QString token; + QString localPath; + QString destinationPath; + qint64 bytesTransferred = 0; + FileStatus status = FileStatus::Queue; +}; + +struct ReceiveSession +{ + QString sessionId; + Device sender; + QMap files; + SessionStatus status = SessionStatus::Waiting; +}; + +struct SendSession +{ + QString sessionId; + Device target; + QMap files; + QString currentFileId; + SessionStatus status = SessionStatus::Waiting; +}; + +class SessionManager : public QObject +{ + Q_OBJECT + +public: + explicit SessionManager(QObject* parent = nullptr); + + QString createReceiveSession(const Device& sender, const QMap& files); + void acceptReceiveSession(const QString& sessionId, const QMap& destinationPaths); + void declineReceiveSession(const QString& sessionId); + void updateReceiveProgress(const QString& sessionId, const QString& fileId, qint64 bytes); + void completeReceiveFile(const QString& sessionId, const QString& fileId); + void failReceiveFile(const QString& sessionId, const QString& fileId); + void cancelReceiveSession(const QString& sessionId); + + QString createSendSession(const Device& target, const QMap& files, + const QMap& localPaths); + void startSendSession(const QString& sessionId); + void updateSendProgress(const QString& sessionId, const QString& fileId, qint64 bytes); + void completeSendFile(const QString& sessionId, const QString& fileId); + void failSendFile(const QString& sessionId, const QString& fileId); + void cancelSendSession(const QString& sessionId); + + ReceiveSession receiveSession(const QString& sessionId) const; + SendSession sendSession(const QString& sessionId) const; + bool hasReceiveSession(const QString& sessionId) const; + bool hasSendSession(const QString& sessionId) const; + QString generateToken(); + +signals: + void receiveSessionCreated(const QString& sessionId); + void receiveSessionAccepted(const QString& sessionId, const QMap& tokens); + void receiveSessionDeclined(const QString& sessionId); + void receiveSessionCompleted(const QString& sessionId); + void receiveSessionCanceled(const QString& sessionId); + void receiveProgress(const QString& sessionId, const QString& fileId, double progress); + + void sendSessionCreated(const QString& sessionId); + void sendSessionStarted(const QString& sessionId); + void sendSessionCompleted(const QString& sessionId); + void sendSessionCanceled(const QString& sessionId); + void sendProgress(const QString& sessionId, const QString& fileId, double progress); + +private: + QMap m_receiveSessions; + QMap m_sendSessions; +}; + +} diff --git a/src/core/include/LocalSendCore/Settings.h b/src/core/include/LocalSendCore/Settings.h new file mode 100644 index 0000000..924e2ad --- /dev/null +++ b/src/core/include/LocalSendCore/Settings.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include +#include "LocalSendCore/Types.h" + +namespace LocalSend { + +class Settings : public QObject +{ + Q_OBJECT + +public: + explicit Settings(QObject* parent = nullptr); + + QString alias() const; + void setAlias(const QString& alias); + + quint16 port() const; + void setPort(quint16 port); + + bool https() const; + void setHttps(bool enabled); + + QString downloadDir() const; + void setDownloadDir(const QString& path); + + bool quickSave() const; + void setQuickSave(bool enabled); + + QString deviceModel() const; + void setDeviceModel(const QString& model); + + DeviceType deviceType() const; + void setDeviceType(DeviceType type); + + QString version() const; + + void sync(); + +private: + QSettings m_settings; +}; + +} diff --git a/src/core/include/LocalSendCore/Types.h b/src/core/include/LocalSendCore/Types.h new file mode 100644 index 0000000..5269264 --- /dev/null +++ b/src/core/include/LocalSendCore/Types.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include + +namespace LocalSend { + +enum class DeviceType { + Mobile, + Desktop, + Web, + Headless, + Server +}; + +enum class ProtocolType { + Http, + Https +}; + +enum class SessionStatus { + Waiting, + Sending, + Finished, + FinishedWithErrors, + CanceledBySender, + CanceledByReceiver, + Declined +}; + +enum class FileStatus { + Queue, + Sending, + Finished, + Skipped, + Failed +}; + +enum class DiscoveryMethod { + Multicast, + HttpScan, + HttpTarget +}; + +QString deviceTypeToString(DeviceType type); +DeviceType deviceTypeFromString(const QString& str); + +QString protocolTypeToString(ProtocolType type); +ProtocolType protocolTypeFromString(const QString& str); + +} + +Q_DECLARE_METATYPE(LocalSend::DeviceType) +Q_DECLARE_METATYPE(LocalSend::ProtocolType) +Q_DECLARE_METATYPE(LocalSend::SessionStatus) +Q_DECLARE_METATYPE(LocalSend::FileStatus) +Q_DECLARE_METATYPE(LocalSend::DiscoveryMethod) diff --git a/src/core/src/Constants.cpp b/src/core/src/Constants.cpp new file mode 100644 index 0000000..6687197 --- /dev/null +++ b/src/core/src/Constants.cpp @@ -0,0 +1 @@ +#include "LocalSendCore/Constants.h" diff --git a/src/core/src/Device.cpp b/src/core/src/Device.cpp new file mode 100644 index 0000000..e00dc4d --- /dev/null +++ b/src/core/src/Device.cpp @@ -0,0 +1,16 @@ +#include "LocalSendCore/Device.h" + +namespace LocalSend { + +Device::Device(const QString& ip, quint16 port) + : ip(ip) + , port(port) +{ +} + +QString Device::displayName() const +{ + return alias.isEmpty() ? ip : alias; +} + +} diff --git a/src/core/src/DiscoveryManager.cpp b/src/core/src/DiscoveryManager.cpp new file mode 100644 index 0000000..2e652cb --- /dev/null +++ b/src/core/src/DiscoveryManager.cpp @@ -0,0 +1,85 @@ +#include "LocalSendCore/DiscoveryManager.h" +#include "LocalSendCore/Constants.h" + +namespace LocalSend { + +DiscoveryManager::DiscoveryManager(QObject* parent) + : QObject(parent) + , m_multicastDiscovery(new MulticastDiscovery(this)) + , m_cleanupTimer(new QTimer(this)) +{ + connect(m_multicastDiscovery, &MulticastDiscovery::deviceDiscovered, + this, &DiscoveryManager::onDeviceDiscovered); + connect(m_cleanupTimer, &QTimer::timeout, this, &DiscoveryManager::onDeviceTimeout); +} + +DiscoveryManager::~DiscoveryManager() +{ + stop(); +} + +void DiscoveryManager::setLocalInfo(const InfoDto& info, const QString& fingerprint, + quint16 port, ProtocolType protocol) +{ + m_multicastDiscovery->setLocalInfo(info, fingerprint, port, protocol); +} + +void DiscoveryManager::start() +{ + m_multicastDiscovery->start(); + m_cleanupTimer->start(5000); +} + +void DiscoveryManager::stop() +{ + m_multicastDiscovery->stop(); + m_cleanupTimer->stop(); + m_devices.clear(); +} + +void DiscoveryManager::startScan() +{ + m_multicastDiscovery->sendAnnouncement(); +} + +QList DiscoveryManager::discoveredDevices() const +{ + return m_devices.values(); +} + +Device DiscoveryManager::deviceByFingerprint(const QString& fingerprint) const +{ + return m_devices.value(fingerprint); +} + +void DiscoveryManager::onDeviceDiscovered(const Device& device) +{ + bool isNew = !m_devices.contains(device.fingerprint); + m_devices.insert(device.fingerprint, device); + + if (isNew) { + emit deviceDiscovered(device); + } else { + emit deviceUpdated(device); + } +} + +void DiscoveryManager::onDeviceTimeout() +{ + QDateTime now = QDateTime::currentDateTime(); + QStringList timedOut; + + for (auto it = m_devices.constBegin(); it != m_devices.constEnd(); ++it) { + if (it->lastSeen.msecsTo(now) > DEVICE_TIMEOUT_MS) { + timedOut.append(it.key()); + } + } + + for (const QString& fingerprint : timedOut) { + m_devices.remove(fingerprint); + emit deviceLost(fingerprint); + } +} + +} + diff --git a/src/core/src/DtoTypes.cpp b/src/core/src/DtoTypes.cpp new file mode 100644 index 0000000..442ad4e --- /dev/null +++ b/src/core/src/DtoTypes.cpp @@ -0,0 +1,192 @@ +#include "LocalSendCore/DtoTypes.h" +#include "LocalSendCore/Constants.h" + +namespace LocalSend { + +QJsonObject MulticastDto::toJson() const +{ + QJsonObject obj; + obj[QStringLiteral("alias")] = alias; + obj[QStringLiteral("version")] = version.isEmpty() ? QString(PROTOCOL_VERSION) : version; + obj[QStringLiteral("deviceModel")] = deviceModel; + obj[QStringLiteral("deviceType")] = deviceTypeToString(deviceType); + obj[QStringLiteral("fingerprint")] = fingerprint; + obj[QStringLiteral("port")] = port; + obj[QStringLiteral("protocol")] = protocolTypeToString(protocol); + obj[QStringLiteral("download")] = download; + obj[QStringLiteral("announcement")] = announcement; + obj[QStringLiteral("announce")] = announce; + return obj; +} + +MulticastDto MulticastDto::fromJson(const QJsonObject& json) +{ + MulticastDto dto; + dto.alias = json[QStringLiteral("alias")].toString(); + dto.version = json[QStringLiteral("version")].toString(); + dto.deviceModel = json[QStringLiteral("deviceModel")].toString(); + dto.deviceType = deviceTypeFromString(json[QStringLiteral("deviceType")].toString()); + dto.fingerprint = json[QStringLiteral("fingerprint")].toString(); + dto.port = static_cast(json[QStringLiteral("port")].toInt(DEFAULT_PORT)); + dto.protocol = protocolTypeFromString(json[QStringLiteral("protocol")].toString()); + dto.download = json[QStringLiteral("download")].toBool(); + dto.announcement = json[QStringLiteral("announcement")].toBool(); + dto.announce = json[QStringLiteral("announce")].toBool(); + return dto; +} + +QJsonObject InfoDto::toJson() const +{ + QJsonObject obj; + obj[QStringLiteral("alias")] = alias; + obj[QStringLiteral("version")] = version.isEmpty() ? QString(PROTOCOL_VERSION) : version; + obj[QStringLiteral("deviceModel")] = deviceModel; + obj[QStringLiteral("deviceType")] = deviceTypeToString(deviceType); + obj[QStringLiteral("fingerprint")] = fingerprint; + obj[QStringLiteral("download")] = download; + return obj; +} + +InfoDto InfoDto::fromJson(const QJsonObject& json) +{ + InfoDto dto; + dto.alias = json[QStringLiteral("alias")].toString(); + dto.version = json[QStringLiteral("version")].toString(); + dto.deviceModel = json[QStringLiteral("deviceModel")].toString(); + dto.deviceType = deviceTypeFromString(json[QStringLiteral("deviceType")].toString()); + dto.fingerprint = json[QStringLiteral("fingerprint")].toString(); + dto.download = json[QStringLiteral("download")].toBool(); + return dto; +} + +QJsonObject RegisterDto::toJson() const +{ + QJsonObject obj; + obj[QStringLiteral("alias")] = alias; + obj[QStringLiteral("version")] = version.isEmpty() ? QString(PROTOCOL_VERSION) : version; + obj[QStringLiteral("deviceModel")] = deviceModel; + obj[QStringLiteral("deviceType")] = deviceTypeToString(deviceType); + obj[QStringLiteral("fingerprint")] = fingerprint; + obj[QStringLiteral("port")] = port; + obj[QStringLiteral("protocol")] = protocolTypeToString(protocol); + obj[QStringLiteral("download")] = download; + return obj; +} + +RegisterDto RegisterDto::fromJson(const QJsonObject& json) +{ + RegisterDto dto; + dto.alias = json[QStringLiteral("alias")].toString(); + dto.version = json[QStringLiteral("version")].toString(); + dto.deviceModel = json[QStringLiteral("deviceModel")].toString(); + dto.deviceType = deviceTypeFromString(json[QStringLiteral("deviceType")].toString()); + dto.fingerprint = json[QStringLiteral("fingerprint")].toString(); + dto.port = static_cast(json[QStringLiteral("port")].toInt(DEFAULT_PORT)); + dto.protocol = protocolTypeFromString(json[QStringLiteral("protocol")].toString()); + dto.download = json[QStringLiteral("download")].toBool(); + return dto; +} + +QJsonObject FileDto::toJson() const +{ + QJsonObject obj; + obj[QStringLiteral("id")] = id; + obj[QStringLiteral("fileName")] = fileName; + obj[QStringLiteral("size")] = size; + obj[QStringLiteral("fileType")] = fileType; + if (!hash.isEmpty()) { + obj[QStringLiteral("hash")] = hash; + } + if (!preview.isEmpty()) { + obj[QStringLiteral("preview")] = preview; + } + if (metadata) { + QJsonObject metaObj; + if (metadata->lastModified.isValid()) { + metaObj[QStringLiteral("lastModified")] = metadata->lastModified.toMSecsSinceEpoch(); + } + if (metadata->lastAccessed.isValid()) { + metaObj[QStringLiteral("lastAccessed")] = metadata->lastAccessed.toMSecsSinceEpoch(); + } + obj[QStringLiteral("metadata")] = metaObj; + } + return obj; +} + +FileDto FileDto::fromJson(const QJsonObject& json) +{ + FileDto dto; + dto.id = json[QStringLiteral("id")].toString(); + dto.fileName = json[QStringLiteral("fileName")].toString(); + dto.size = json[QStringLiteral("size")].toVariant().toLongLong(); + dto.fileType = json[QStringLiteral("fileType")].toString(); + dto.hash = json[QStringLiteral("hash")].toString(); + dto.preview = json[QStringLiteral("preview")].toString(); + + if (json.contains(QStringLiteral("metadata"))) { + QJsonObject metaObj = json[QStringLiteral("metadata")].toObject(); + FileDto::Metadata meta; + if (metaObj.contains(QStringLiteral("lastModified"))) { + meta.lastModified = QDateTime::fromMSecsSinceEpoch( + metaObj[QStringLiteral("lastModified")].toVariant().toLongLong()); + } + if (metaObj.contains(QStringLiteral("lastAccessed"))) { + meta.lastAccessed = QDateTime::fromMSecsSinceEpoch( + metaObj[QStringLiteral("lastAccessed")].toVariant().toLongLong()); + } + dto.metadata = meta; + } + return dto; +} + +QJsonObject PrepareUploadRequestDto::toJson() const +{ + QJsonObject obj; + obj[QStringLiteral("info")] = info.toJson(); + + QJsonObject filesObj; + for (auto it = files.constBegin(); it != files.constEnd(); ++it) { + filesObj[it.key()] = it.value().toJson(); + } + obj[QStringLiteral("files")] = filesObj; + return obj; +} + +PrepareUploadRequestDto PrepareUploadRequestDto::fromJson(const QJsonObject& json) +{ + PrepareUploadRequestDto dto; + dto.info = RegisterDto::fromJson(json[QStringLiteral("info")].toObject()); + + QJsonObject filesObj = json[QStringLiteral("files")].toObject(); + for (auto it = filesObj.constBegin(); it != filesObj.constEnd(); ++it) { + dto.files.insert(it.key(), FileDto::fromJson(it.value().toObject())); + } + return dto; +} + +PrepareUploadResponseDto PrepareUploadResponseDto::fromJson(const QJsonObject& json) +{ + PrepareUploadResponseDto dto; + dto.sessionId = json[QStringLiteral("sessionId")].toString(); + + QJsonObject filesObj = json[QStringLiteral("files")].toObject(); + for (auto it = filesObj.constBegin(); it != filesObj.constEnd(); ++it) { + dto.files.insert(it.key(), it.value().toString()); + } + return dto; +} + +QJsonObject ReceiveRequestResponseDto::toJson() const +{ + QJsonObject obj; + obj[QStringLiteral("sessionId")] = sessionId; + + QJsonObject pathsObj; + for (auto it = destinationPaths.constBegin(); it != destinationPaths.constEnd(); ++it) { + pathsObj[it.key()] = it.value(); + } + obj[QStringLiteral("destinationPaths")] = pathsObj; + return obj; +} + +} diff --git a/src/core/src/HttpClient.cpp b/src/core/src/HttpClient.cpp new file mode 100644 index 0000000..326542f --- /dev/null +++ b/src/core/src/HttpClient.cpp @@ -0,0 +1,182 @@ +#include "LocalSendCore/HttpClient.h" +#include "LocalSendCore/Constants.h" +#include +#include +#include + +namespace LocalSend { + +HttpClient::HttpClient(QObject* parent) + : QObject(parent) + , m_manager(new QNetworkAccessManager(this)) +{ +} + +HttpClient::~HttpClient() +{ +} + +void HttpClient::setSslConfiguration(const QSslConfiguration& config) +{ + m_sslConfig = config; +} + +QUrl HttpClient::buildUrl(const Device& device, const QString& path) const +{ + QUrl url; + url.setScheme(device.isHttps() ? QStringLiteral("https") : QStringLiteral("http")); + url.setHost(device.ip); + url.setPort(device.port); + url.setPath(path); + return url; +} + +QNetworkReply* HttpClient::sendGet(const QUrl& url) +{ + QNetworkRequest request(url); + if (m_sslConfig.isNull() == false) { + request.setSslConfiguration(m_sslConfig); + } + return m_manager->get(request); +} + +QNetworkReply* HttpClient::sendPost(const QUrl& url, const QByteArray& data) +{ + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json")); + if (m_sslConfig.isNull() == false) { + request.setSslConfiguration(m_sslConfig); + } + return m_manager->post(request, data); +} + +void HttpClient::getInfo(const Device& device) +{ + QUrl url = buildUrl(device, ApiRoute::INFO); + QNetworkReply* reply = sendGet(url); + + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + reply->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) { + emit infoError(reply->errorString()); + return; + } + + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(reply->readAll(), &error); + + if (error.error != QJsonParseError::NoError) { + emit infoError(QStringLiteral("Invalid JSON response")); + return; + } + + InfoDto info = InfoDto::fromJson(doc.object()); + emit infoReceived(info); + }); +} + +void HttpClient::registerDevice(const Device& device, const RegisterDto& dto) +{ + QUrl url = buildUrl(device, ApiRoute::REGISTER); + QByteArray data = QJsonDocument(dto.toJson()).toJson(QJsonDocument::Compact); + QNetworkReply* reply = sendPost(url, data); + + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + reply->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) { + emit registerError(reply->errorString()); + return; + } + + emit registerCompleted(); + }); +} + +void HttpClient::prepareUpload(const Device& device, const PrepareUploadRequestDto& dto) +{ + QUrl url = buildUrl(device, ApiRoute::PREPARE_UPLOAD); + QByteArray data = QJsonDocument(dto.toJson()).toJson(QJsonDocument::Compact); + QNetworkReply* reply = sendPost(url, data); + + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + reply->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) { + emit prepareUploadError(reply->errorString()); + return; + } + + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(reply->readAll(), &error); + + if (error.error != QJsonParseError::NoError) { + emit prepareUploadError(QStringLiteral("Invalid JSON response")); + return; + } + + PrepareUploadResponseDto response = PrepareUploadResponseDto::fromJson(doc.object()); + emit prepareUploadResponse(response); + }); +} + +void HttpClient::uploadFile(const Device& device, const QString& sessionId, + const QString& fileId, const QString& token, const QString& filePath) +{ + QUrl url = buildUrl(device, ApiRoute::UPLOAD); + QUrlQuery query; + query.addQueryItem(QStringLiteral("sessionId"), sessionId); + query.addQueryItem(QStringLiteral("fileId"), fileId); + query.addQueryItem(QStringLiteral("token"), token); + url.setQuery(query); + + QFile* file = new QFile(filePath); + if (!file->open(QIODevice::ReadOnly)) { + emit uploadError(QStringLiteral("Cannot open file: ") + file->errorString()); + delete file; + return; + } + + qint64 fileSize = file->size(); + + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/octet-stream")); + request.setHeader(QNetworkRequest::ContentLengthHeader, fileSize); + if (m_sslConfig.isNull() == false) { + request.setSslConfiguration(m_sslConfig); + } + + QNetworkReply* reply = m_manager->post(request, file); + file->setParent(reply); + + connect(reply, &QNetworkReply::uploadProgress, this, [this, fileSize](qint64 sent, qint64) { + emit uploadProgress(sent, fileSize); + }); + + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + reply->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) { + emit uploadError(reply->errorString()); + return; + } + + emit uploadCompleted(); + }); +} + +void HttpClient::cancel(const Device& device, const QString& sessionId) +{ + QUrl url = buildUrl(device, ApiRoute::CANCEL); + + QJsonObject body; + body[QStringLiteral("sessionId")] = sessionId; + QByteArray data = QJsonDocument(body).toJson(QJsonDocument::Compact); + + QNetworkReply* reply = sendPost(url, data); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); +} + +} + diff --git a/src/core/src/HttpServer.cpp b/src/core/src/HttpServer.cpp new file mode 100644 index 0000000..98b76d6 --- /dev/null +++ b/src/core/src/HttpServer.cpp @@ -0,0 +1,213 @@ +#include "LocalSendCore/HttpServer.h" +#include "LocalSendCore/Constants.h" + +#ifdef HAS_QTHTTPSERVER +#include +#include + +namespace LocalSend { + +HttpServer::HttpServer(QObject* parent) + : QObject(parent) + , m_server(new QHttpServer(this)) +{ + setupRoutes(); +} + +HttpServer::~HttpServer() +{ + stop(); +} + +void HttpServer::setLocalInfo(const InfoDto& info, const QString& fingerprint) +{ + m_localInfo = info; + m_localFingerprint = fingerprint; +} + +void HttpServer::setSslConfiguration(const QSslConfiguration& config) +{ + m_sslConfig = config; +} + +bool HttpServer::start(quint16 port, bool https) +{ + Q_UNUSED(https) + + if (m_server->isListening()) { + stop(); + } + + m_port = port; + + auto tcpServer = new QTcpServer(this); + if (!tcpServer->listen(QHostAddress::Any, port)) { + delete tcpServer; + return false; + } + m_server->bind(tcpServer); + + return true; +} + +void HttpServer::stop() +{ + m_server->close(); +} + +quint16 HttpServer::serverPort() const +{ + return m_port; +} + +bool HttpServer::isRunning() const +{ + return m_server->isListening(); +} + +void HttpServer::setupRoutes() +{ + m_server->route(ApiRoute::INFO, QHttpServerRequest::Method::Get, + [this](const QHttpServerRequest&) { + return handleInfoRequest(); + }); + + m_server->route(ApiRoute::REGISTER, QHttpServerRequest::Method::Post, + [this](const QHttpServerRequest& request) { + return handleRegisterRequest(request, request.remoteAddress()); + }); + + m_server->route(ApiRoute::PREPARE_UPLOAD, QHttpServerRequest::Method::Post, + [this](const QHttpServerRequest& request) { + return handlePrepareUploadRequest(request); + }); + + m_server->route(ApiRoute::UPLOAD, QHttpServerRequest::Method::Post, + [this](const QHttpServerRequest& request) { + return handleUploadRequest(request); + }); + + m_server->route(ApiRoute::CANCEL, QHttpServerRequest::Method::Post, + [this](const QHttpServerRequest& request) { + return handleCancelRequest(request); + }); +} + +QHttpServerResponse HttpServer::handleInfoRequest() +{ + QJsonDocument doc(m_localInfo.toJson()); + return QHttpServerResponse(doc.toJson(QJsonDocument::Compact), + QHttpServerResponse::StatusCode::Ok); +} + +QHttpServerResponse HttpServer::handleRegisterRequest(const QHttpServerRequest& request, + const QHostAddress& peer) +{ + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(request.body(), &error); + + if (error.error != QJsonParseError::NoError) { + return QHttpServerResponse(QHttpServerResponse::StatusCode::BadRequest); + } + + RegisterDto dto = RegisterDto::fromJson(doc.object()); + emit registerRequest(dto, peer); + + QJsonDocument response(m_localInfo.toJson()); + return QHttpServerResponse(response.toJson(QJsonDocument::Compact), + QHttpServerResponse::StatusCode::Ok); +} + +QHttpServerResponse HttpServer::handlePrepareUploadRequest(const QHttpServerRequest& request) +{ + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(request.body(), &error); + + if (error.error != QJsonParseError::NoError) { + return QHttpServerResponse(QHttpServerResponse::StatusCode::BadRequest); + } + + PrepareUploadRequestDto dto = PrepareUploadRequestDto::fromJson(doc.object()); + emit prepareUploadRequest(dto, request.remoteAddress()); + + return QHttpServerResponse(QHttpServerResponse::StatusCode::Accepted); +} + +QHttpServerResponse HttpServer::handleUploadRequest(const QHttpServerRequest& request) +{ + QUrlQuery query(request.url().query()); + QString sessionId = query.queryItemValue(QStringLiteral("sessionId")); + QString fileId = query.queryItemValue(QStringLiteral("fileId")); + QString token = query.queryItemValue(QStringLiteral("token")); + + if (sessionId.isEmpty() || fileId.isEmpty() || token.isEmpty()) { + return QHttpServerResponse(QHttpServerResponse::StatusCode::BadRequest); + } + + emit uploadRequest(sessionId, fileId, token, request.body()); + + return QHttpServerResponse(QHttpServerResponse::StatusCode::Ok); +} + +QHttpServerResponse HttpServer::handleCancelRequest(const QHttpServerRequest& request) +{ + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(request.body(), &error); + + QString sessionId; + if (error.error == QJsonParseError::NoError) { + sessionId = doc.object()[QStringLiteral("sessionId")].toString(); + } + + emit cancelRequest(sessionId); + + return QHttpServerResponse(QHttpServerResponse::StatusCode::Ok); +} + +} + + +#else + +namespace LocalSend { + +HttpServer::HttpServer(QObject* parent) + : QObject(parent) +{ +} + +HttpServer::~HttpServer() +{ +} + +void HttpServer::setLocalInfo(const InfoDto& info, const QString& fingerprint) +{ + m_localInfo = info; + m_localFingerprint = fingerprint; +} + +bool HttpServer::start(quint16 port, bool https) +{ + Q_UNUSED(port) + Q_UNUSED(https) + return false; +} + +void HttpServer::stop() +{ +} + +quint16 HttpServer::serverPort() const +{ + return m_port; +} + +bool HttpServer::isRunning() const +{ + return false; +} + +} + + +#endif diff --git a/src/core/src/MulticastDiscovery.cpp b/src/core/src/MulticastDiscovery.cpp new file mode 100644 index 0000000..b1af244 --- /dev/null +++ b/src/core/src/MulticastDiscovery.cpp @@ -0,0 +1,130 @@ +#include "LocalSendCore/MulticastDiscovery.h" +#include "LocalSendCore/Constants.h" +#include + +namespace LocalSend { + +MulticastDiscovery::MulticastDiscovery(QObject* parent) + : QObject(parent) +{ +} + +MulticastDiscovery::~MulticastDiscovery() +{ + stop(); +} + +void MulticastDiscovery::setLocalInfo(const InfoDto& info, const QString& fingerprint, + quint16 port, ProtocolType protocol) +{ + m_localInfo = info; + m_localFingerprint = fingerprint; + m_localPort = port; + m_localProtocol = protocol; +} + +void MulticastDiscovery::start(const QString& multicastGroup, quint16 port) +{ + if (m_socket) { + stop(); + } + + m_multicastGroup = multicastGroup; + m_port = port; + + m_socket = new QUdpSocket(this); + + if (!m_socket->bind(QHostAddress::AnyIPv4, port, + QAbstractSocket::ShareAddress | QAbstractSocket::ReuseAddressHint)) { + qWarning() << "Failed to bind UDP socket:" << m_socket->errorString(); + return; + } + + for (const QNetworkInterface& iface : QNetworkInterface::allInterfaces()) { + if (iface.flags() & QNetworkInterface::CanMulticast) { + m_socket->joinMulticastGroup(QHostAddress(multicastGroup), iface); + } + } + + connect(m_socket, &QUdpSocket::readyRead, this, &MulticastDiscovery::onReadyRead); + + sendAnnouncement(); +} + +void MulticastDiscovery::stop() +{ + if (m_socket) { + m_socket->close(); + m_socket->deleteLater(); + m_socket = nullptr; + } +} + +void MulticastDiscovery::sendAnnouncement() +{ + if (!m_socket || m_localFingerprint.isEmpty()) { + return; + } + + MulticastDto dto; + dto.alias = m_localInfo.alias; + dto.version = m_localInfo.version.isEmpty() ? PROTOCOL_VERSION : m_localInfo.version; + dto.deviceModel = m_localInfo.deviceModel; + dto.deviceType = m_localInfo.deviceType; + dto.fingerprint = m_localFingerprint; + dto.port = m_localPort; + dto.protocol = m_localProtocol; + dto.download = m_localInfo.download; + dto.announcement = true; + dto.announce = true; + + QByteArray data = QJsonDocument(dto.toJson()).toJson(QJsonDocument::Compact); + m_socket->writeDatagram(data, QHostAddress(m_multicastGroup), m_port); +} + +void MulticastDiscovery::onReadyRead() +{ + while (m_socket && m_socket->hasPendingDatagrams()) { + QByteArray datagram; + datagram.resize(static_cast(m_socket->pendingDatagramSize())); + QHostAddress sender; + quint16 senderPort; + + m_socket->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort); + handleMulticastMessage(sender, datagram); + } +} + +void MulticastDiscovery::handleMulticastMessage(const QHostAddress& sender, const QByteArray& data) +{ + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(data, &error); + + if (error.error != QJsonParseError::NoError) { + return; + } + + MulticastDto dto = MulticastDto::fromJson(doc.object()); + + if (dto.fingerprint.isEmpty() || dto.fingerprint == m_localFingerprint) { + return; + } + + Device device; + device.ip = sender.toString(); + device.port = dto.port; + device.protocol = dto.protocol; + device.alias = dto.alias; + device.fingerprint = dto.fingerprint; + device.deviceModel = dto.deviceModel; + device.deviceType = dto.deviceType; + device.version = dto.version; + device.download = dto.download; + device.discoveryMethod = DiscoveryMethod::Multicast; + device.lastSeen = QDateTime::currentDateTime(); + + emit deviceDiscovered(device); +} + +} + diff --git a/src/core/src/SecurityContext.cpp b/src/core/src/SecurityContext.cpp new file mode 100644 index 0000000..0cf6429 --- /dev/null +++ b/src/core/src/SecurityContext.cpp @@ -0,0 +1,141 @@ +#include "LocalSendCore/SecurityContext.h" +#include +#include +#include +#include +#include + +namespace LocalSend { + +SecurityContext::SecurityContext(QObject* parent) + : QObject(parent) +{ +} + +QString SecurityContext::storagePath() const +{ + QString configPath = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); + QDir dir(configPath); + if (!dir.exists()) { + dir.mkpath(QStringLiteral(".")); + } + return configPath; +} + +void SecurityContext::initialize() +{ + loadFromStorage(); + + if (!isValid()) { + generateCertificate(); + saveToStorage(); + } +} + +void SecurityContext::regenerate() +{ + generateCertificate(); + saveToStorage(); +} + +void SecurityContext::loadFromStorage() +{ + QString basePath = storagePath(); + + QFile certFile(basePath + QStringLiteral("/cert.pem")); + QFile keyFile(basePath + QStringLiteral("/key.pem")); + + if (!certFile.exists() || !keyFile.exists()) { + return; + } + + if (!certFile.open(QIODevice::ReadOnly) || !keyFile.open(QIODevice::ReadOnly)) { + return; + } + + QSslCertificate cert(&certFile, QSsl::Pem); + QSslKey key(&keyFile, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey); + + certFile.close(); + keyFile.close(); + + if (cert.isNull() || key.isNull()) { + return; + } + + m_certificate = cert; + m_privateKey = key; + m_fingerprint = calculateFingerprint(cert); +} + +void SecurityContext::saveToStorage() +{ + if (!isValid()) { + return; + } + + QString basePath = storagePath(); + + QFile certFile(basePath + QStringLiteral("/cert.pem")); + QFile keyFile(basePath + QStringLiteral("/key.pem")); + + if (!certFile.open(QIODevice::WriteOnly) || !keyFile.open(QIODevice::WriteOnly)) { + return; + } + + certFile.write(m_certificate.toPem()); + keyFile.write(m_privateKey.toPem()); + + certFile.close(); + keyFile.close(); + + keyFile.setPermissions(QFile::ReadOwner | QFile::WriteOwner); +} + +void SecurityContext::generateCertificate() +{ +#ifdef USE_OPENSSL + QString basePath = storagePath(); + QString keyPath = basePath + QStringLiteral("/key.pem"); + QString certPath = basePath + QStringLiteral("/cert.pem"); + + QProcess openssl; + + openssl.start(QStringLiteral("openssl"), { + QStringLiteral("req"), QStringLiteral("-x509"), + QStringLiteral("-newkey"), QStringLiteral("rsa:2048"), + QStringLiteral("-keyout"), keyPath, + QStringLiteral("-out"), certPath, + QStringLiteral("-days"), QStringLiteral("3650"), + QStringLiteral("-nodes"), + QStringLiteral("-subj"), QStringLiteral("/CN=LocalSend") + }); + + if (!openssl.waitForFinished(30000)) { + qWarning() << "Failed to generate certificate"; + return; + } + + loadFromStorage(); +#else + qWarning() << "Certificate generation requires OpenSSL support"; +#endif +} + +QString SecurityContext::calculateFingerprint(const QSslCertificate& cert) const +{ + QByteArray digest = cert.digest(QCryptographicHash::Sha256); + return QString::fromLatin1(digest.toHex()); +} + +QSslConfiguration SecurityContext::sslConfiguration() const +{ + QSslConfiguration config; + config.setLocalCertificate(m_certificate); + config.setPrivateKey(m_privateKey); + config.setPeerVerifyMode(QSslSocket::VerifyNone); + return config; +} + +} + diff --git a/src/core/src/SessionManager.cpp b/src/core/src/SessionManager.cpp new file mode 100644 index 0000000..20cab5f --- /dev/null +++ b/src/core/src/SessionManager.cpp @@ -0,0 +1,263 @@ +#include "LocalSendCore/SessionManager.h" +#include + +namespace LocalSend { + +SessionManager::SessionManager(QObject* parent) + : QObject(parent) +{ +} + +QString SessionManager::createReceiveSession(const Device& sender, const QMap& files) +{ + QString sessionId = QUuid::createUuid().toString(QUuid::WithoutBraces); + + ReceiveSession session; + session.sessionId = sessionId; + session.sender = sender; + session.status = SessionStatus::Waiting; + + for (auto it = files.constBegin(); it != files.constEnd(); ++it) { + FileTransfer transfer; + transfer.file = it.value(); + transfer.token = generateToken(); + transfer.status = FileStatus::Queue; + session.files.insert(it.key(), transfer); + } + + m_receiveSessions.insert(sessionId, session); + emit receiveSessionCreated(sessionId); + + return sessionId; +} + +void SessionManager::acceptReceiveSession(const QString& sessionId, + const QMap& destinationPaths) +{ + if (!m_receiveSessions.contains(sessionId)) { + return; + } + + ReceiveSession& session = m_receiveSessions[sessionId]; + session.status = SessionStatus::Sending; + + QMap tokens; + for (auto it = session.files.begin(); it != session.files.end(); ++it) { + it->destinationPath = destinationPaths.value(it.key()); + it->status = FileStatus::Queue; + tokens.insert(it.key(), it->token); + } + + emit receiveSessionAccepted(sessionId, tokens); +} + +void SessionManager::declineReceiveSession(const QString& sessionId) +{ + if (!m_receiveSessions.contains(sessionId)) { + return; + } + + m_receiveSessions[sessionId].status = SessionStatus::Declined; + emit receiveSessionDeclined(sessionId); + m_receiveSessions.remove(sessionId); +} + +void SessionManager::updateReceiveProgress(const QString& sessionId, const QString& fileId, qint64 bytes) +{ + if (!m_receiveSessions.contains(sessionId)) { + return; + } + + ReceiveSession& session = m_receiveSessions[sessionId]; + if (!session.files.contains(fileId)) { + return; + } + + FileTransfer& transfer = session.files[fileId]; + transfer.bytesTransferred = bytes; + transfer.status = FileStatus::Sending; + + double progress = transfer.file.size > 0 ? + static_cast(bytes) / transfer.file.size : 0.0; + emit receiveProgress(sessionId, fileId, progress); +} + +void SessionManager::completeReceiveFile(const QString& sessionId, const QString& fileId) +{ + if (!m_receiveSessions.contains(sessionId)) { + return; + } + + ReceiveSession& session = m_receiveSessions[sessionId]; + if (!session.files.contains(fileId)) { + return; + } + + session.files[fileId].status = FileStatus::Finished; + session.files[fileId].bytesTransferred = session.files[fileId].file.size; + + bool allFinished = true; + for (const FileTransfer& transfer : session.files) { + if (transfer.status != FileStatus::Finished) { + allFinished = false; + break; + } + } + + if (allFinished) { + session.status = SessionStatus::Finished; + emit receiveSessionCompleted(sessionId); + m_receiveSessions.remove(sessionId); + } +} + +void SessionManager::failReceiveFile(const QString& sessionId, const QString& fileId) +{ + if (!m_receiveSessions.contains(sessionId)) { + return; + } + + m_receiveSessions[sessionId].files[fileId].status = FileStatus::Failed; +} + +void SessionManager::cancelReceiveSession(const QString& sessionId) +{ + if (!m_receiveSessions.contains(sessionId)) { + return; + } + + m_receiveSessions[sessionId].status = SessionStatus::CanceledByReceiver; + emit receiveSessionCanceled(sessionId); + m_receiveSessions.remove(sessionId); +} + +QString SessionManager::createSendSession(const Device& target, const QMap& files, + const QMap& localPaths) +{ + QString sessionId = QUuid::createUuid().toString(QUuid::WithoutBraces); + + SendSession session; + session.sessionId = sessionId; + session.target = target; + session.status = SessionStatus::Waiting; + + for (auto it = files.constBegin(); it != files.constEnd(); ++it) { + FileTransfer transfer; + transfer.file = it.value(); + transfer.localPath = localPaths.value(it.key()); + transfer.status = FileStatus::Queue; + session.files.insert(it.key(), transfer); + } + + m_sendSessions.insert(sessionId, session); + emit sendSessionCreated(sessionId); + + return sessionId; +} + +void SessionManager::startSendSession(const QString& sessionId) +{ + if (!m_sendSessions.contains(sessionId)) { + return; + } + + m_sendSessions[sessionId].status = SessionStatus::Sending; + emit sendSessionStarted(sessionId); +} + +void SessionManager::updateSendProgress(const QString& sessionId, const QString& fileId, qint64 bytes) +{ + if (!m_sendSessions.contains(sessionId)) { + return; + } + + SendSession& session = m_sendSessions[sessionId]; + if (!session.files.contains(fileId)) { + return; + } + + FileTransfer& transfer = session.files[fileId]; + transfer.bytesTransferred = bytes; + transfer.status = FileStatus::Sending; + session.currentFileId = fileId; + + double progress = transfer.file.size > 0 ? + static_cast(bytes) / transfer.file.size : 0.0; + emit sendProgress(sessionId, fileId, progress); +} + +void SessionManager::completeSendFile(const QString& sessionId, const QString& fileId) +{ + if (!m_sendSessions.contains(sessionId)) { + return; + } + + SendSession& session = m_sendSessions[sessionId]; + if (!session.files.contains(fileId)) { + return; + } + + session.files[fileId].status = FileStatus::Finished; + + bool allFinished = true; + for (const FileTransfer& transfer : session.files) { + if (transfer.status != FileStatus::Finished) { + allFinished = false; + break; + } + } + + if (allFinished) { + session.status = SessionStatus::Finished; + emit sendSessionCompleted(sessionId); + m_sendSessions.remove(sessionId); + } +} + +void SessionManager::failSendFile(const QString& sessionId, const QString& fileId) +{ + if (!m_sendSessions.contains(sessionId)) { + return; + } + + m_sendSessions[sessionId].files[fileId].status = FileStatus::Failed; +} + +void SessionManager::cancelSendSession(const QString& sessionId) +{ + if (!m_sendSessions.contains(sessionId)) { + return; + } + + m_sendSessions[sessionId].status = SessionStatus::CanceledBySender; + emit sendSessionCanceled(sessionId); + m_sendSessions.remove(sessionId); +} + +ReceiveSession SessionManager::receiveSession(const QString& sessionId) const +{ + return m_receiveSessions.value(sessionId); +} + +SendSession SessionManager::sendSession(const QString& sessionId) const +{ + return m_sendSessions.value(sessionId); +} + +bool SessionManager::hasReceiveSession(const QString& sessionId) const +{ + return m_receiveSessions.contains(sessionId); +} + +bool SessionManager::hasSendSession(const QString& sessionId) const +{ + return m_sendSessions.contains(sessionId); +} + +QString SessionManager::generateToken() +{ + return QUuid::createUuid().toString(QUuid::WithoutBraces); +} + +} + diff --git a/src/core/src/Settings.cpp b/src/core/src/Settings.cpp new file mode 100644 index 0000000..ed67e3c --- /dev/null +++ b/src/core/src/Settings.cpp @@ -0,0 +1,114 @@ +#include "LocalSendCore/Settings.h" +#include "LocalSendCore/Constants.h" +#include + +namespace LocalSend { + +Settings::Settings(QObject* parent) + : QObject(parent) + , m_settings(QSettings::IniFormat, QSettings::UserScope, + QStringLiteral("LocalSendQt"), QStringLiteral("settings")) +{ +} + +QString Settings::alias() const +{ + return m_settings.value(QStringLiteral("alias"), + QSysInfo::machineHostName()).toString(); +} + +void Settings::setAlias(const QString& alias) +{ + if (this->alias() != alias) { + m_settings.setValue(QStringLiteral("alias"), alias); + m_settings.sync(); + } +} + +quint16 Settings::port() const +{ + return m_settings.value(QStringLiteral("port"), DEFAULT_PORT).value(); +} + +void Settings::setPort(quint16 port) +{ + if (this->port() != port) { + m_settings.setValue(QStringLiteral("port"), port); + m_settings.sync(); + } +} + +bool Settings::https() const +{ + return m_settings.value(QStringLiteral("https"), false).toBool(); +} + +void Settings::setHttps(bool enabled) +{ + if (https() != enabled) { + m_settings.setValue(QStringLiteral("https"), enabled); + m_settings.sync(); + } +} + +QString Settings::downloadDir() const +{ + return m_settings.value(QStringLiteral("downloadDir"), + QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)).toString(); +} + +void Settings::setDownloadDir(const QString& path) +{ + if (downloadDir() != path) { + m_settings.setValue(QStringLiteral("downloadDir"), path); + m_settings.sync(); + } +} + +bool Settings::quickSave() const +{ + return m_settings.value(QStringLiteral("quickSave"), false).toBool(); +} + +void Settings::setQuickSave(bool enabled) +{ + if (quickSave() != enabled) { + m_settings.setValue(QStringLiteral("quickSave"), enabled); + m_settings.sync(); + } +} + +QString Settings::deviceModel() const +{ + return m_settings.value(QStringLiteral("deviceModel"), + QSysInfo::prettyProductName()).toString(); +} + +void Settings::setDeviceModel(const QString& model) +{ + m_settings.setValue(QStringLiteral("deviceModel"), model); +} + +DeviceType Settings::deviceType() const +{ + return deviceTypeFromString(m_settings.value(QStringLiteral("deviceType"), + QStringLiteral("desktop")).toString()); +} + +void Settings::setDeviceType(DeviceType type) +{ + m_settings.setValue(QStringLiteral("deviceType"), deviceTypeToString(type)); +} + +QString Settings::version() const +{ + return PROTOCOL_VERSION; +} + +void Settings::sync() +{ + m_settings.sync(); +} + +} + diff --git a/src/core/src/Types.cpp b/src/core/src/Types.cpp new file mode 100644 index 0000000..1a577de --- /dev/null +++ b/src/core/src/Types.cpp @@ -0,0 +1,37 @@ +#include "LocalSendCore/Types.h" + +namespace LocalSend { + +QString deviceTypeToString(DeviceType type) +{ + switch (type) { + case DeviceType::Mobile: return QStringLiteral("mobile"); + case DeviceType::Desktop: return QStringLiteral("desktop"); + case DeviceType::Web: return QStringLiteral("web"); + case DeviceType::Headless: return QStringLiteral("headless"); + case DeviceType::Server: return QStringLiteral("server"); + } + return QStringLiteral("desktop"); +} + +DeviceType deviceTypeFromString(const QString& str) +{ + if (str == QStringLiteral("mobile")) return DeviceType::Mobile; + if (str == QStringLiteral("desktop")) return DeviceType::Desktop; + if (str == QStringLiteral("web")) return DeviceType::Web; + if (str == QStringLiteral("headless")) return DeviceType::Headless; + if (str == QStringLiteral("server")) return DeviceType::Server; + return DeviceType::Desktop; +} + +QString protocolTypeToString(ProtocolType type) +{ + return type == ProtocolType::Https ? QStringLiteral("https") : QStringLiteral("http"); +} + +ProtocolType protocolTypeFromString(const QString& str) +{ + return str == QStringLiteral("https") ? ProtocolType::Https : ProtocolType::Http; +} + +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..c449bb9 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,7 @@ +add_executable(TestDtoTypes core/TestDtoTypes.cpp) +target_link_libraries(TestDtoTypes PRIVATE LocalSendCore Qt6::Test) +add_test(NAME TestDtoTypes COMMAND TestDtoTypes) + +add_executable(TestMulticastDiscovery core/TestMulticastDiscovery.cpp) +target_link_libraries(TestMulticastDiscovery PRIVATE LocalSendCore Qt6::Test) +add_test(NAME TestMulticastDiscovery COMMAND TestMulticastDiscovery) diff --git a/tests/core/TestDtoTypes.cpp b/tests/core/TestDtoTypes.cpp new file mode 100644 index 0000000..f42ef4b --- /dev/null +++ b/tests/core/TestDtoTypes.cpp @@ -0,0 +1,122 @@ +#include +#include +#include + +class TestDtoTypes : public QObject +{ + Q_OBJECT + +private slots: + void testMulticastDtoSerialization(); + void testInfoDtoSerialization(); + void testRegisterDtoSerialization(); + void testFileDtoSerialization(); + void testPrepareUploadRequestDto(); +}; + +void TestDtoTypes::testMulticastDtoSerialization() +{ + LocalSend::MulticastDto dto; + dto.alias = QStringLiteral("Test Device"); + dto.version = LocalSend::PROTOCOL_VERSION; + dto.deviceModel = QStringLiteral("Test Model"); + dto.deviceType = LocalSend::DeviceType::Desktop; + dto.fingerprint = QStringLiteral("abc123"); + dto.port = 53317; + dto.protocol = LocalSend::ProtocolType::Http; + dto.download = false; + dto.announcement = true; + dto.announce = true; + + QJsonObject json = dto.toJson(); + LocalSend::MulticastDto parsed = LocalSend::MulticastDto::fromJson(json); + + QCOMPARE(parsed.alias, dto.alias); + QCOMPARE(parsed.version, dto.version); + QCOMPARE(parsed.deviceModel, dto.deviceModel); + QCOMPARE(parsed.deviceType, dto.deviceType); + QCOMPARE(parsed.fingerprint, dto.fingerprint); + QCOMPARE(parsed.port, dto.port); + QCOMPARE(parsed.protocol, dto.protocol); + QCOMPARE(parsed.download, dto.download); + QCOMPARE(parsed.announcement, dto.announcement); + QCOMPARE(parsed.announce, dto.announce); +} + +void TestDtoTypes::testInfoDtoSerialization() +{ + LocalSend::InfoDto dto; + dto.alias = QStringLiteral("My Device"); + dto.version = QStringLiteral("2.1"); + dto.deviceModel = QStringLiteral("Desktop"); + dto.deviceType = LocalSend::DeviceType::Desktop; + dto.fingerprint = QStringLiteral("def456"); + dto.download = true; + + QJsonObject json = dto.toJson(); + LocalSend::InfoDto parsed = LocalSend::InfoDto::fromJson(json); + + QCOMPARE(parsed.alias, dto.alias); + QCOMPARE(parsed.version, dto.version); + QCOMPARE(parsed.fingerprint, dto.fingerprint); +} + +void TestDtoTypes::testRegisterDtoSerialization() +{ + LocalSend::RegisterDto dto; + dto.alias = QStringLiteral("Sender"); + dto.port = 53318; + dto.protocol = LocalSend::ProtocolType::Https; + + QJsonObject json = dto.toJson(); + LocalSend::RegisterDto parsed = LocalSend::RegisterDto::fromJson(json); + + QCOMPARE(parsed.port, dto.port); + QCOMPARE(parsed.protocol, dto.protocol); +} + +void TestDtoTypes::testFileDtoSerialization() +{ + LocalSend::FileDto dto; + dto.id = QStringLiteral("file1"); + dto.fileName = QStringLiteral("test.txt"); + dto.size = 1024; + dto.fileType = QStringLiteral("text/plain"); + dto.hash = QStringLiteral("sha256hash"); + + QJsonObject json = dto.toJson(); + LocalSend::FileDto parsed = LocalSend::FileDto::fromJson(json); + + QCOMPARE(parsed.id, dto.id); + QCOMPARE(parsed.fileName, dto.fileName); + QCOMPARE(parsed.size, dto.size); + QCOMPARE(parsed.fileType, dto.fileType); +} + +void TestDtoTypes::testPrepareUploadRequestDto() +{ + LocalSend::PrepareUploadRequestDto dto; + dto.info.alias = QStringLiteral("Sender"); + + LocalSend::FileDto file1; + file1.id = QStringLiteral("f1"); + file1.fileName = QStringLiteral("file1.txt"); + file1.size = 100; + dto.files.insert(QStringLiteral("f1"), file1); + + LocalSend::FileDto file2; + file2.id = QStringLiteral("f2"); + file2.fileName = QStringLiteral("file2.txt"); + file2.size = 200; + dto.files.insert(QStringLiteral("f2"), file2); + + QJsonObject json = dto.toJson(); + LocalSend::PrepareUploadRequestDto parsed = LocalSend::PrepareUploadRequestDto::fromJson(json); + + QCOMPARE(parsed.files.size(), 2); + QCOMPARE(parsed.files[QStringLiteral("f1")].fileName, QStringLiteral("file1.txt")); + QCOMPARE(parsed.files[QStringLiteral("f2")].size, 200); +} + +QTEST_MAIN(TestDtoTypes) +#include "TestDtoTypes.moc" diff --git a/tests/core/TestMulticastDiscovery.cpp b/tests/core/TestMulticastDiscovery.cpp new file mode 100644 index 0000000..20c3886 --- /dev/null +++ b/tests/core/TestMulticastDiscovery.cpp @@ -0,0 +1,19 @@ +#include +#include + +class TestMulticastDiscovery : public QObject +{ + Q_OBJECT + +private slots: + void testDefaults(); +}; + +void TestMulticastDiscovery::testDefaults() +{ + LocalSend::MulticastDiscovery discovery; + QVERIFY(!discovery.parent()); +} + +QTEST_MAIN(TestMulticastDiscovery) +#include "TestMulticastDiscovery.moc"