pin support

This commit is contained in:
2026-04-28 12:01:35 +08:00
parent 2556c2db83
commit 7c884a2185
9 changed files with 324 additions and 2 deletions

View File

@@ -37,6 +37,8 @@ void AppController::initialize()
emit serverRunningChanged();
}
m_server->setReceivePin(m_settings->receivePin());
connect(m_discovery, &LocalSend::DiscoveryManager::deviceDiscovered,
this, &AppController::onDeviceDiscovered);
connect(m_discovery, &LocalSend::DiscoveryManager::deviceLost,
@@ -60,6 +62,10 @@ void AppController::initialize()
this, &AppController::onPrepareUploadResponse);
connect(m_httpClient, &LocalSend::HttpClient::prepareUploadError,
this, &AppController::onPrepareUploadError);
connect(m_httpClient, &LocalSend::HttpClient::prepareUploadPinRequired,
this, &AppController::onPrepareUploadPinRequired);
connect(m_httpClient, &LocalSend::HttpClient::prepareUploadTooManyAttempts,
this, &AppController::onPrepareUploadTooManyAttempts);
connect(m_httpClient, &LocalSend::HttpClient::uploadProgress,
this, &AppController::onUploadProgress);
connect(m_httpClient, &LocalSend::HttpClient::uploadCompleted,
@@ -379,6 +385,20 @@ bool AppController::hasPendingFiles() const
return !m_pendingFilesList.isEmpty();
}
QString AppController::receivePin() const
{
return m_settings->receivePin();
}
void AppController::setReceivePin(const QString& pin)
{
if (m_settings->receivePin() != pin) {
m_settings->setReceivePin(pin);
m_server->setReceivePin(pin);
emit receivePinChanged();
}
}
void AppController::addFiles(const QStringList& filePaths)
{
QMimeDatabase mimeDb;
@@ -449,6 +469,7 @@ void AppController::sendFiles(const QString& deviceFingerprint, const QStringLis
m_pendingSendPaths = filePaths;
m_currentFileIndex = 0;
m_sendProgress = 0.0;
m_pinFirstAttempt = true;
emit sendingChanged();
emit sendProgressChanged();
@@ -552,6 +573,55 @@ void AppController::onPrepareUploadError(const QString& error)
emit sendingChanged();
}
void AppController::onPrepareUploadPinRequired()
{
qDebug() << "[AppController] onPrepareUploadPinRequired, firstAttempt:" << m_pinFirstAttempt;
emit pinRequired(m_pinFirstAttempt);
m_pinFirstAttempt = false;
}
void AppController::onPrepareUploadTooManyAttempts()
{
qWarning() << "[AppController] onPrepareUploadTooManyAttempts";
emit sendError(QStringLiteral("Too many PIN attempts"));
m_currentSendSessionId.clear();
emit sendingChanged();
}
void AppController::retryWithPin(const QString& pin)
{
if (m_currentSendDeviceFingerprint.isEmpty() || !m_devices.contains(m_currentSendDeviceFingerprint)) {
m_currentSendSessionId.clear();
emit sendingChanged();
return;
}
LocalSend::Device target = m_devices[m_currentSendDeviceFingerprint];
QMap<QString, LocalSend::FileDto> files;
QMimeDatabase mimeDb;
for (int i = 0; i < m_pendingSendPaths.size(); ++i) {
QFileInfo info(m_pendingSendPaths[i]);
if (!info.exists()) {
continue;
}
LocalSend::FileDto fileDto;
fileDto.id = QString::number(i);
fileDto.fileName = info.fileName();
fileDto.size = info.size();
fileDto.fileType = mimeDb.mimeTypeForFile(info).name();
files.insert(fileDto.id, fileDto);
}
LocalSend::PrepareUploadRequestDto request;
request.info = buildRegisterDto();
request.files = files;
qDebug() << "[AppController] Retrying prepare-upload with PIN to" << target.ip;
m_httpClient->prepareUpload(target, request, pin);
}
void AppController::onUploadProgress(qint64 sent, qint64 total)
{
if (total > 0) {

View File

@@ -23,6 +23,7 @@ class AppController : public QObject
Q_PROPERTY(double sendProgress READ sendProgress NOTIFY sendProgressChanged)
Q_PROPERTY(QVariantList pendingFiles READ pendingFiles NOTIFY pendingFilesChanged)
Q_PROPERTY(bool hasPendingFiles READ hasPendingFiles NOTIFY pendingFilesChanged)
Q_PROPERTY(QString receivePin READ receivePin WRITE setReceivePin NOTIFY receivePinChanged)
public:
explicit AppController(QObject* parent = nullptr);
@@ -49,6 +50,8 @@ public:
double sendProgress() const;
QVariantList pendingFiles() const;
bool hasPendingFiles() const;
QString receivePin() const;
void setReceivePin(const QString& pin);
Q_INVOKABLE void startDiscovery();
Q_INVOKABLE void stopDiscovery();
@@ -63,6 +66,7 @@ public:
Q_INVOKABLE void addFiles(const QStringList& filePaths);
Q_INVOKABLE void removePendingFile(int index);
Q_INVOKABLE void clearPendingFiles();
Q_INVOKABLE void retryWithPin(const QString& pin);
signals:
void aliasChanged();
@@ -81,6 +85,8 @@ signals:
void receiveError(const QString& sessionId, const QString& error);
void sendCompleted(const QString& sessionId);
void sendError(const QString& error);
void pinRequired(bool firstAttempt);
void receivePinChanged();
private slots:
void onDeviceDiscovered(const LocalSend::Device& device);
@@ -98,6 +104,8 @@ private slots:
void onPrepareUploadResponse(const LocalSend::PrepareUploadResponseDto& response);
void onPrepareUploadError(const QString& error);
void onPrepareUploadPinRequired();
void onPrepareUploadTooManyAttempts();
void onUploadProgress(qint64 sent, qint64 total);
void onUploadCompleted();
void onUploadError(const QString& error);
@@ -119,6 +127,7 @@ private:
QVariantList m_pendingFilesList;
int m_currentFileIndex = 0;
double m_sendProgress = 0.0;
bool m_pinFirstAttempt = true;
LocalSend::InfoDto buildInfoDto() const;
QString generateUniqueFilePath(const QString& baseDir, const QString& fileName) const;

View File

@@ -286,9 +286,15 @@ ApplicationWindow {
function onSendError(error) {
sendProgressDialog.close()
pinDialog.close()
errorDialog.text = error
errorDialog.open()
}
function onPinRequired(firstAttempt) {
pinDialog.isFirstAttempt = firstAttempt
pinDialog.open()
}
}
Dialog {
@@ -317,6 +323,112 @@ ApplicationWindow {
}
}
Dialog {
id: pinDialog
anchors.centerIn: parent
modal: true
closePolicy: Popup.NoAutoClose
title: qsTr("PIN Required")
property bool isFirstAttempt: true
ColumnLayout {
spacing: 12
Label {
text: pinDialog.isFirstAttempt
? qsTr("The receiver requires a PIN to accept files.")
: qsTr("Invalid PIN. Please try again.")
wrapMode: Text.WordWrap
Layout.fillWidth: true
color: pinDialog.isFirstAttempt ? palette.text : "red"
}
TextField {
id: pinInput
Layout.fillWidth: true
placeholderText: qsTr("Enter PIN")
echoMode: TextInput.Password
focus: true
onAccepted: pinDialog.submitPin()
}
}
footer: DialogButtonBox {
Button {
text: qsTr("Cancel")
DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
}
Button {
text: qsTr("Submit")
DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
enabled: pinInput.text.length > 0
}
}
onAccepted: submitPin()
onRejected: {
appController.cancelSend()
close()
}
function submitPin() {
if (pinInput.text.length > 0) {
appController.retryWithPin(pinInput.text)
pinInput.text = ""
}
}
onOpened: {
pinInput.text = ""
pinInput.forceActiveFocus()
}
}
Dialog {
id: setPinDialog
anchors.centerIn: parent
modal: true
title: qsTr("Set Receive PIN")
ColumnLayout {
spacing: 12
Label {
text: qsTr("Enter a PIN that senders must provide to transfer files to this device. Leave empty to disable.")
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
TextField {
id: setPinInput
Layout.fillWidth: true
placeholderText: qsTr("Enter PIN")
onAccepted: setPinDialog.accepted()
}
}
footer: DialogButtonBox {
Button {
text: qsTr("Cancel")
DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
}
Button {
text: qsTr("OK")
DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
}
}
onAccepted: {
appController.receivePin = setPinInput.text
}
onOpened: {
setPinInput.text = appController.receivePin
setPinInput.forceActiveFocus()
}
}
function formatSize(bytes) {
if (bytes < 1024) return bytes + " B"
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB"
@@ -597,6 +709,25 @@ ApplicationWindow {
checked: appController.quickSave
onCheckedChanged: appController.quickSave = checked
}
Label { text: qsTr("Receive PIN:") }
RowLayout {
Layout.fillWidth: true
Label {
text: appController.receivePin.length > 0 ? qsTr("Enabled") : qsTr("Disabled")
color: appController.receivePin.length > 0 ? "green" : palette.mid
}
Item { Layout.fillWidth: true }
Button {
text: appController.receivePin.length > 0 ? qsTr("Change") : qsTr("Set PIN")
onClicked: setPinDialog.open()
}
Button {
text: qsTr("Remove")
visible: appController.receivePin.length > 0
onClicked: appController.receivePin = ""
}
}
}
FolderDialog {

View File

@@ -22,7 +22,7 @@ public:
void getInfo(const Device& device);
void registerDevice(const Device& device, const RegisterDto& dto);
void prepareUpload(const Device& device, const PrepareUploadRequestDto& dto);
void prepareUpload(const Device& device, const PrepareUploadRequestDto& dto, const QString& pin = QString());
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);
@@ -34,6 +34,8 @@ signals:
void registerError(const QString& error);
void prepareUploadResponse(const PrepareUploadResponseDto& response);
void prepareUploadError(const QString& error);
void prepareUploadPinRequired();
void prepareUploadTooManyAttempts();
void uploadProgress(qint64 sent, qint64 total);
void uploadCompleted();
void uploadError(const QString& error);

View File

@@ -29,6 +29,7 @@ public:
void stop();
void setLocalInfo(const InfoDto& info, const QString& fingerprint);
void setReceivePin(const QString& pin);
#ifdef HAS_QTHTTPSERVER
void setSslConfiguration(const QSslConfiguration& config);
#endif
@@ -62,6 +63,8 @@ private:
InfoDto m_localInfo;
QString m_localFingerprint;
QString m_receivePin;
QMap<QString, int> m_pinAttempts;
quint16 m_port = 0;
#ifdef HAS_QTHTTPSERVER
@@ -71,6 +74,7 @@ private:
QFuture<QHttpServerResponse> handlePrepareUploadRequest(const QHttpServerRequest& request);
QHttpServerResponse handleUploadRequest(const QHttpServerRequest& request);
QHttpServerResponse handleCancelRequest(const QHttpServerRequest& request);
bool checkPin(const QHttpServerRequest& request, const QHostAddress& peer);
#endif
};

View File

@@ -29,6 +29,9 @@ public:
bool quickSave() const;
void setQuickSave(bool enabled);
QString receivePin() const;
void setReceivePin(const QString& pin);
QString deviceModel() const;
void setDeviceModel(const QString& model);

View File

@@ -99,9 +99,14 @@ void HttpClient::registerDevice(const Device& device, const RegisterDto& dto)
});
}
void HttpClient::prepareUpload(const Device& device, const PrepareUploadRequestDto& dto)
void HttpClient::prepareUpload(const Device& device, const PrepareUploadRequestDto& dto, const QString& pin)
{
QUrl url = buildUrl(device, ApiRoute::PREPARE_UPLOAD);
if (!pin.isEmpty()) {
QUrlQuery query;
query.addQueryItem(QStringLiteral("pin"), pin);
url.setQuery(query);
}
QByteArray data = QJsonDocument(dto.toJson()).toJson(QJsonDocument::Compact);
qDebug() << "[HttpClient] prepareUpload to" << url.toString();
@@ -112,6 +117,20 @@ void HttpClient::prepareUpload(const Device& device, const PrepareUploadRequestD
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
reply->deleteLater();
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (statusCode == 401) {
qDebug() << "[HttpClient] prepareUpload: PIN required (401)";
emit prepareUploadPinRequired();
return;
}
if (statusCode == 429) {
qWarning() << "[HttpClient] prepareUpload: Too many attempts (429)";
emit prepareUploadTooManyAttempts();
return;
}
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "[HttpClient] prepareUpload error:" << reply->errorString();
emit prepareUploadError(reply->errorString());

View File

@@ -33,6 +33,38 @@ void HttpServer::setSslConfiguration(const QSslConfiguration& config)
m_sslConfig = config;
}
void HttpServer::setReceivePin(const QString& pin)
{
m_receivePin = pin;
m_pinAttempts.clear();
}
bool HttpServer::checkPin(const QHttpServerRequest& request, const QHostAddress& peer)
{
if (m_receivePin.isEmpty()) {
return true;
}
QString peerIp = peer.toString();
int attempts = m_pinAttempts.value(peerIp, 0);
if (attempts >= 3) {
return false;
}
QUrlQuery query(request.url().query());
QString requestPin = query.queryItemValue(QStringLiteral("pin"));
if (requestPin != m_receivePin) {
if (!requestPin.isEmpty()) {
m_pinAttempts[peerIp] = attempts + 1;
}
return false;
}
return true;
}
bool HttpServer::start(quint16 port, bool https)
{
Q_UNUSED(https)
@@ -180,6 +212,41 @@ QHttpServerResponse HttpServer::handleRegisterRequest(const QHttpServerRequest&
QFuture<QHttpServerResponse> HttpServer::handlePrepareUploadRequest(const QHttpServerRequest& request)
{
QHostAddress peer = request.remoteAddress();
if (!m_receivePin.isEmpty()) {
QString peerIp = peer.toString();
int attempts = m_pinAttempts.value(peerIp, 0);
if (attempts >= 3) {
auto promise = std::make_shared<QPromise<QHttpServerResponse>>();
promise->start();
QJsonObject errorObj;
errorObj[QStringLiteral("message")] = QStringLiteral("Too many attempts.");
promise->addResult(QHttpServerResponse(QJsonDocument(errorObj).toJson(QJsonDocument::Compact),
QHttpServerResponse::StatusCode::Forbidden));
promise->finish();
return promise->future();
}
QUrlQuery query(request.url().query());
QString requestPin = query.queryItemValue(QStringLiteral("pin"));
if (requestPin != m_receivePin) {
if (!requestPin.isEmpty()) {
m_pinAttempts[peerIp] = attempts + 1;
}
auto promise = std::make_shared<QPromise<QHttpServerResponse>>();
promise->start();
QJsonObject errorObj;
errorObj[QStringLiteral("message")] = QStringLiteral("Invalid pin.");
promise->addResult(QHttpServerResponse(QJsonDocument(errorObj).toJson(QJsonDocument::Compact),
QHttpServerResponse::StatusCode::Unauthorized));
promise->finish();
return promise->future();
}
}
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(request.body(), &error);

View File

@@ -78,6 +78,23 @@ void Settings::setQuickSave(bool enabled)
}
}
QString Settings::receivePin() const
{
return m_settings.value(QStringLiteral("receivePin")).toString();
}
void Settings::setReceivePin(const QString& pin)
{
if (receivePin() != pin) {
if (pin.isEmpty()) {
m_settings.remove(QStringLiteral("receivePin"));
} else {
m_settings.setValue(QStringLiteral("receivePin"), pin);
}
m_settings.sync();
}
}
QString Settings::deviceModel() const
{
return m_settings.value(QStringLiteral("deviceModel"),