pin support
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user