init commit

This commit is contained in:
2026-04-24 20:20:24 +08:00
commit 5442e32abc
34 changed files with 2684 additions and 0 deletions

151
src/app/AppController.cpp Normal file
View File

@@ -0,0 +1,151 @@
#include "AppController.h"
#include <QJsonArray>
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);
}

63
src/app/AppController.h Normal file
View File

@@ -0,0 +1,63 @@
#pragma once
#include <QObject>
#include <QVariantList>
#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<QString, LocalSend::Device> m_devices;
LocalSend::InfoDto buildInfoDto() const;
};

9
src/app/Application.cpp Normal file
View File

@@ -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"));
}

12
src/app/Application.h Normal file
View File

@@ -0,0 +1,12 @@
#pragma once
#include <QGuiApplication>
#include <QIcon>
class Application : public QGuiApplication
{
Q_OBJECT
public:
Application(int& argc, char** argv);
};

23
src/app/CMakeLists.txt Normal file
View File

@@ -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}
)

33
src/app/main.cpp Normal file
View File

@@ -0,0 +1,33 @@
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQuickStyle>
#include <QDir>
#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();
}

178
src/app/qml/main.qml Normal file
View File

@@ -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()
}
}
}

52
src/core/CMakeLists.txt Normal file
View File

@@ -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
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
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
)

View File

@@ -0,0 +1,27 @@
#pragma once
#include <QtGlobal>
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";
}
}

View File

@@ -0,0 +1,41 @@
#pragma once
#include <QString>
#include <QHostAddress>
#include <QDateTime>
#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);
}
};
}

View File

@@ -0,0 +1,48 @@
#pragma once
#include <QObject>
#include <QMap>
#include <QTimer>
#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<Device> 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<QString, Device> m_devices;
QTimer* m_cleanupTimer = nullptr;
static constexpr int DEVICE_TIMEOUT_MS = 30000;
};
}

View File

@@ -0,0 +1,102 @@
#pragma once
#include <QString>
#include <QJsonObject>
#include <QMap>
#include <QDateTime>
#include <optional>
#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> metadata;
QJsonObject toJson() const;
static FileDto fromJson(const QJsonObject& json);
};
struct PrepareUploadRequestDto
{
RegisterDto info;
QMap<QString, FileDto> files;
QJsonObject toJson() const;
static PrepareUploadRequestDto fromJson(const QJsonObject& json);
};
struct PrepareUploadResponseDto
{
QString sessionId;
QMap<QString, QString> files;
static PrepareUploadResponseDto fromJson(const QJsonObject& json);
};
struct ReceiveRequestResponseDto
{
QString sessionId;
QMap<QString, QString> destinationPaths;
bool cancel = false;
QJsonObject toJson() const;
};
}

View File

@@ -0,0 +1,51 @@
#pragma once
#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QSslConfiguration>
#include <functional>
#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;
};
}

View File

@@ -0,0 +1,63 @@
#pragma once
#include <QObject>
#include <QHostAddress>
#include "LocalSendCore/Device.h"
#include "LocalSendCore/DtoTypes.h"
#ifdef HAS_QTHTTPSERVER
#include <QHttpServer>
#include <QSslConfiguration>
#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
};
}

View File

@@ -0,0 +1,47 @@
#pragma once
#include <QObject>
#include <QUdpSocket>
#include <QHostAddress>
#include <QNetworkInterface>
#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);
};
}

View File

@@ -0,0 +1,41 @@
#pragma once
#include <QObject>
#include <QSslCertificate>
#include <QSslKey>
#include <QSslConfiguration>
#include <QString>
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;
};
}

View File

@@ -0,0 +1,86 @@
#pragma once
#include <QObject>
#include <QMap>
#include <QUuid>
#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<QString, FileTransfer> files;
SessionStatus status = SessionStatus::Waiting;
};
struct SendSession
{
QString sessionId;
Device target;
QMap<QString, FileTransfer> 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<QString, FileDto>& files);
void acceptReceiveSession(const QString& sessionId, const QMap<QString, QString>& 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<QString, FileDto>& files,
const QMap<QString, QString>& 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<QString, QString>& 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<QString, ReceiveSession> m_receiveSessions;
QMap<QString, SendSession> m_sendSessions;
};
}

View File

@@ -0,0 +1,46 @@
#pragma once
#include <QObject>
#include <QSettings>
#include <QString>
#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;
};
}

View File

@@ -0,0 +1,57 @@
#pragma once
#include <QString>
#include <QMetaType>
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)

View File

@@ -0,0 +1 @@
#include "LocalSendCore/Constants.h"

16
src/core/src/Device.cpp Normal file
View File

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

View File

@@ -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<Device> 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);
}
}
}

192
src/core/src/DtoTypes.cpp Normal file
View File

@@ -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<quint16>(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<quint16>(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;
}
}

182
src/core/src/HttpClient.cpp Normal file
View File

@@ -0,0 +1,182 @@
#include "LocalSendCore/HttpClient.h"
#include "LocalSendCore/Constants.h"
#include <QFile>
#include <QUrlQuery>
#include <QJsonDocument>
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);
}
}

213
src/core/src/HttpServer.cpp Normal file
View File

@@ -0,0 +1,213 @@
#include "LocalSendCore/HttpServer.h"
#include "LocalSendCore/Constants.h"
#ifdef HAS_QTHTTPSERVER
#include <QTcpServer>
#include <QJsonDocument>
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

View File

@@ -0,0 +1,130 @@
#include "LocalSendCore/MulticastDiscovery.h"
#include "LocalSendCore/Constants.h"
#include <QJsonDocument>
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<int>(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);
}
}

View File

@@ -0,0 +1,141 @@
#include "LocalSendCore/SecurityContext.h"
#include <QStandardPaths>
#include <QDir>
#include <QFile>
#include <QCryptographicHash>
#include <QProcess>
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;
}
}

View File

@@ -0,0 +1,263 @@
#include "LocalSendCore/SessionManager.h"
#include <QUuid>
namespace LocalSend {
SessionManager::SessionManager(QObject* parent)
: QObject(parent)
{
}
QString SessionManager::createReceiveSession(const Device& sender, const QMap<QString, FileDto>& 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<QString, QString>& destinationPaths)
{
if (!m_receiveSessions.contains(sessionId)) {
return;
}
ReceiveSession& session = m_receiveSessions[sessionId];
session.status = SessionStatus::Sending;
QMap<QString, QString> 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<double>(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<QString, FileDto>& files,
const QMap<QString, QString>& 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<double>(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);
}
}

114
src/core/src/Settings.cpp Normal file
View File

@@ -0,0 +1,114 @@
#include "LocalSendCore/Settings.h"
#include "LocalSendCore/Constants.h"
#include <QStandardPaths>
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<quint16>();
}
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();
}
}

37
src/core/src/Types.cpp Normal file
View File

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