From 326d8e97ac42877ab140de22807647b2568cbee0 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 4 Apr 2026 19:29:36 +0200 Subject: [PATCH] Umbau auf Pure-FTPd mit PureDB Virtual Users und Passwort-Dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Komplett auf Pure-FTPd umgestellt (weg von vsftpd) - PureDB virtuelle Benutzer: jeder User sieht nur seine Freigaben - Per-User Bind-Mounts mit Read-Only/Read-Write Durchsetzung - Passwort-Dialog beim Anlegen neuer FTP-Benutzer - Change-Password UI: Dropdown + Button zum Passwort ändern - Setup-Kommando für automatische Pure-FTPd Einrichtung - Anonymous-Checkbox entfernt (nur authentifizierte User) - sudoers-Fix: $SUDO_USER statt $USER bei sudo-Ausführung - openssl passwd statt pure-pw stdin für GUI-Kompatibilität Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 124 ++++++++++----------- kf5/src/ftpshareplugin.cpp | 128 ++++++++++++++++++---- kf5/src/ftpshareplugin.h | 4 +- kf6/src/ftpshareplugin.cpp | 128 ++++++++++++++++++---- kf6/src/ftpshareplugin.h | 4 +- scripts/dolphin-ftp-share | 214 +++++++++++++++++++++++++++++++------ 6 files changed, 460 insertions(+), 142 deletions(-) diff --git a/README.md b/README.md index b4ce871..a2532c9 100644 --- a/README.md +++ b/README.md @@ -9,80 +9,63 @@ Ein KDE Dolphin Properties-Dialog Plugin zum Erstellen und Verwalten von FTP-Fre - **FTP-Tab** in Dolphins Ordner-Eigenschaften (Rechtsklick → Eigenschaften) - Freigabe per Checkbox aktivieren/deaktivieren - Freigabename konfigurierbar (Standard = Ordnername) -- Anonymen Zugang erlauben -- Berechtigungen pro Benutzer: Read Only / Read-Write -- Nutzt das Helper-Script `dolphin-ftp-share` — **kein Root nötig!** -- Erkennt bestehende Freigaben und lädt deren Einstellungen +- **Per-User Berechtigungen**: Read Only / Read-Write pro Benutzer +- Jeder Benutzer sieht **nur seine freigegebenen Ordner** per FTP +- Read-Only wird direkt am Dateisystem durchgesetzt (read-only Bind-Mount) +- Automatische Einrichtung mit `dolphin-ftp-share setup` ## Voraussetzungen -- Ein FTP-Server (z.B. `vsftpd`) +- Pure-FTPd als FTP-Server - Das Helper-Script `dolphin-ftp-share` im PATH - Build-Dependencies (siehe unten) -### FTP-Server installieren (vsftpd) +## Einrichtung + +### 1. Pure-FTPd installieren ```bash -sudo apt install vsftpd +sudo apt install pure-ftpd ``` -### vsftpd konfigurieren - -In `/etc/vsftpd.conf`: - -```ini -listen=YES -anonymous_enable=YES -local_enable=YES -write_enable=YES -anon_root=/home/DEIN_USER/.local/share/dolphin-ftp-root -local_root=/home/DEIN_USER/.local/share/dolphin-ftp-root -chroot_local_user=YES -allow_writeable_chroot=YES -pasv_enable=NO -port_enable=YES -connect_from_port_20=YES - -# Logging -xferlog_enable=YES -xferlog_std_format=NO -log_ftp_protocol=YES -vsftpd_log_file=/var/log/vsftpd.log -``` - -Danach: `sudo systemctl restart vsftpd` - -### Logdatei - -vsftpd schreibt alle FTP-Aktivitäten (Logins, Uploads, Downloads, Fehler) in `/var/log/vsftpd.log`. - -```bash -# Logs live verfolgen: -sudo tail -f /var/log/vsftpd.log -``` - -### Helper-Script installieren +### 2. Helper-Script installieren ```bash sudo cp scripts/dolphin-ftp-share /usr/local/bin/ sudo chmod +x /usr/local/bin/dolphin-ftp-share ``` -### sudo für Bind-Mounts erlauben +### 3. Automatisches Setup -Das Helper-Script nutzt `sudo mount --bind` und `sudo umount`, um Ordner in das FTP-Root einzubinden (Symlinks funktionieren nicht in vsftpd's chroot-Umgebung). - -Damit das ohne Passwort-Eingabe funktioniert: +Das Setup konfiguriert Pure-FTPd komplett und erstellt die sudoers-Regel: ```bash -sudo visudo -f /etc/sudoers.d/dolphin-ftp-share +sudo dolphin-ftp-share setup ``` -Folgendes eintragen (DEIN_USER ersetzen): +Das macht automatisch: +- Aktiviert PureDB-Authentifizierung (virtuelle FTP-Benutzer) +- Deaktiviert System-Login und anonymen Zugang +- Aktiviert Chroot (jeder sieht nur seine Ordner) +- Erstellt sudoers-Regel für mount/umount/pure-pw +- Aktiviert Logging +- Startet Pure-FTPd neu +### 4. FTP-Passwörter setzen + +FTP-Benutzer werden beim ersten Freigeben automatisch erstellt (Standard-Passwort = Benutzername). Passwort ändern: + +```bash +dolphin-ftp-share passwd ``` -DEIN_USER ALL=(root) NOPASSWD: /usr/bin/mount --bind * -DEIN_USER ALL=(root) NOPASSWD: /usr/bin/umount /home/DEIN_USER/.local/share/dolphin-ftp-root/* + +### Logging + +Pure-FTPd loggt über syslog: + +```bash +# Logs live verfolgen: +sudo tail -f /var/log/syslog | grep pure-ftpd ``` ## Versionen @@ -165,8 +148,12 @@ sudo rm /usr/lib/x86_64-linux-gnu/qt5/plugins/kf5/propertiesdialog/ftpshareplugi # KF6 sudo rm /usr/lib/x86_64-linux-gnu/qt6/plugins/kf6/propertiesdialog/ftpshareplugin.so -# Helper-Script +# Helper-Script & sudoers sudo rm /usr/local/bin/dolphin-ftp-share +sudo rm /etc/sudoers.d/dolphin-ftp-share + +# Pure-FTPd (optional) +sudo apt remove pure-ftpd ``` ## Projektstruktur @@ -196,28 +183,31 @@ kde-dolphin-ftp-sharing-tab/ ## Wie es funktioniert -Das Plugin nutzt das Helper-Script `dolphin-ftp-share`: +1. **Rechtsklick auf Ordner → Eigenschaften → FTP-Tab** +2. Freigabe aktivieren, Berechtigungen pro Benutzer setzen (Read Only / Read-Write) +3. Beim Anwenden ruft das Plugin `dolphin-ftp-share add` auf +4. Das Helper-Script: + - Erstellt für jeden Benutzer ein persönliches FTP-Root (`~/.local/share/dolphin-ftp-root/users//`) + - Bind-mounted den freigegebenen Ordner dorthin + - Read-Only Freigaben werden als read-only Bind-Mount gemountet + - Erstellt automatisch virtuelle Pure-FTPd-Benutzer (PureDB) +5. **Jeder FTP-Benutzer sieht nur seine freigegebenen Ordner** — durch Chroot auf sein persönliches FTP-Root -- `dolphin-ftp-share add ` — Freigabe erstellen -- `dolphin-ftp-share delete ` — Freigabe löschen -- `dolphin-ftp-share info` — bestehende Freigaben auflisten +### Kommandos -Das Script speichert Freigabe-Konfigurationen in `~/.local/share/dolphin-ftp-shares/` und erstellt Bind-Mounts in `~/.local/share/dolphin-ftp-root/`. Der FTP-Server (z.B. vsftpd) dient dieses Root-Verzeichnis aus. - -### Konfigurationsformat - -Jede Freigabe wird als `.conf`-Datei gespeichert: - -```ini -path=/pfad/zum/ordner -anonymous=y -user_acl=user1:rw,user2:ro -``` +| Kommando | Beschreibung | +|---|---| +| `dolphin-ftp-share setup` | Einmalige Pure-FTPd Einrichtung | +| `dolphin-ftp-share add ` | Freigabe erstellen | +| `dolphin-ftp-share delete ` | Freigabe löschen | +| `dolphin-ftp-share info` | Bestehende Freigaben auflisten | +| `dolphin-ftp-share passwd ` | FTP-Passwort ändern | ### Berechtigungen | Level | Beschreibung | |---|---| +| `---` | Kein Zugriff — Ordner ist für diesen Benutzer nicht sichtbar | | `ro` | Read Only — nur Download | | `rw` | Read-Write — Upload und Download | diff --git a/kf5/src/ftpshareplugin.cpp b/kf5/src/ftpshareplugin.cpp index 9882919..ab4f094 100644 --- a/kf5/src/ftpshareplugin.cpp +++ b/kf5/src/ftpshareplugin.cpp @@ -5,7 +5,9 @@ #include #include +#include #include +#include #include #include #include @@ -37,6 +39,26 @@ static int indexForAcl(const QString &acl) return 0; } +// Check if a virtual FTP user exists +static bool ftpUserExists(const QString &username) +{ + QProcess proc; + proc.start(QStringLiteral("dolphin-ftp-share"), + {QStringLiteral("userexists"), username}); + proc.waitForFinished(5000); + return proc.exitCode() == 0; +} + +// Create a virtual FTP user with password +static bool createFtpUser(const QString &username, const QString &password) +{ + QProcess proc; + proc.start(QStringLiteral("dolphin-ftp-share"), + {QStringLiteral("passwd"), username, password}); + proc.waitForFinished(5000); + return proc.exitCode() == 0; +} + // Get list of local users (UID >= 1000, excluding nobody) static QStringList getLocalUsers() { @@ -89,13 +111,8 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args) nameLayout->addRow(i18n("Name:"), m_nameEdit); mainLayout->addLayout(nameLayout); - // Anonymous access - m_anonymousCheckBox = new QCheckBox(i18n("Allow Anonymous Access")); - mainLayout->addWidget(m_anonymousCheckBox); - // User permissions section - auto *usersGroup = new QGroupBox(); - usersGroup->setFlat(true); + auto *usersGroup = new QGroupBox(i18n("User Permissions")); auto *usersLayout = new QVBoxLayout(usersGroup); // Scroll area for user list @@ -123,6 +140,18 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args) usersLayout->addWidget(scrollArea); mainLayout->addWidget(usersGroup); + // Change password section + auto *pwGroup = new QGroupBox(i18n("FTP Password")); + auto *pwLayout = new QHBoxLayout(pwGroup); + m_pwUserCombo = new QComboBox(); + m_pwUserCombo->addItems(users); + auto *pwButton = new QPushButton(i18n("Change Password...")); + pwLayout->addWidget(m_pwUserCombo); + pwLayout->addWidget(pwButton); + mainLayout->addWidget(pwGroup); + + connect(pwButton, &QPushButton::clicked, this, &FtpSharePlugin::onChangePassword); + mainLayout->addStretch(); // Connect signals @@ -130,8 +159,6 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args) this, &FtpSharePlugin::onShareToggled); connect(m_nameEdit, &QLineEdit::textChanged, this, [this]() { setDirty(true); }); - connect(m_anonymousCheckBox, &QCheckBox::toggled, - this, [this]() { setDirty(true); }); // Load existing share info loadCurrentShare(); @@ -146,7 +173,6 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args) void FtpSharePlugin::onShareToggled(bool checked) { m_nameEdit->setEnabled(checked); - m_anonymousCheckBox->setEnabled(checked); m_usersWidget->setEnabled(checked); setDirty(true); } @@ -167,12 +193,10 @@ void FtpSharePlugin::loadCurrentShare() // Format: // [sharename] // path=/some/path - // anonymous=y // user_acl=user1:rw,user2:ro QString currentSection; QString sharePath; QString shareAcl; - QString anonymous; const QStringList lines = output.split(QLatin1Char('\n')); for (const QString &line : lines) { @@ -185,13 +209,10 @@ void FtpSharePlugin::loadCurrentShare() currentSection = line.mid(1, line.indexOf(QLatin1Char(']')) - 1); sharePath.clear(); shareAcl.clear(); - anonymous.clear(); } else if (line.startsWith(QLatin1String("path="))) { sharePath = line.mid(5); } else if (line.startsWith(QLatin1String("user_acl="))) { shareAcl = line.mid(9); - } else if (line.startsWith(QLatin1String("anonymous="))) { - anonymous = line.mid(10); } } @@ -204,7 +225,6 @@ void FtpSharePlugin::loadCurrentShare() m_originalShareName = currentSection; m_shareCheckBox->setChecked(true); m_nameEdit->setText(currentSection); - m_anonymousCheckBox->setChecked(anonymous == QLatin1String("y")); // Parse ACL: "user1:rw,user2:ro" if (!shareAcl.isEmpty()) { @@ -257,6 +277,41 @@ void FtpSharePlugin::applyChanges() proc.waitForFinished(5000); } + // Create FTP users for users that have permissions but no FTP account yet + for (const auto &up : m_userPerms) { + if (up.combo->currentIndex() == 0) + continue; // No permission set, skip + + if (ftpUserExists(up.username)) + continue; // Already has FTP account + + // Ask for FTP password + bool ok = false; + const QString password = QInputDialog::getText( + m_page, + i18n("FTP Password"), + i18n("Set FTP password for user '%1':", up.username), + QLineEdit::Password, + QString(), + &ok); + + if (!ok || password.isEmpty()) { + QMessageBox::warning(m_page, + i18n("FTP Share"), + i18n("No password set for '%1'. This user will be skipped.", up.username)); + up.combo->setCurrentIndex(0); + continue; + } + + if (!createFtpUser(up.username, password)) { + QMessageBox::warning(m_page, + i18n("FTP Share"), + i18n("Failed to create FTP user '%1'.", up.username)); + up.combo->setCurrentIndex(0); + continue; + } + } + // Build ACL string QStringList aclParts; for (const auto &up : m_userPerms) { @@ -267,14 +322,11 @@ void FtpSharePlugin::applyChanges() } const QString aclString = aclParts.join(QLatin1Char(',')); - const QString anonymous = m_anonymousCheckBox->isChecked() - ? QStringLiteral("y") - : QStringLiteral("n"); - // dolphin-ftp-share add + // dolphin-ftp-share add QProcess proc; proc.start(QStringLiteral("dolphin-ftp-share"), - {QStringLiteral("add"), shareName, m_path, anonymous, aclString}); + {QStringLiteral("add"), shareName, m_path, aclString}); proc.waitForFinished(5000); if (proc.exitCode() == 0) { @@ -283,4 +335,40 @@ void FtpSharePlugin::applyChanges() } } +void FtpSharePlugin::onChangePassword() +{ + const QString username = m_pwUserCombo->currentText(); + if (username.isEmpty()) + return; + + if (!ftpUserExists(username)) { + QMessageBox::information(m_page, + i18n("FTP Password"), + i18n("User '%1' has no FTP account yet.\nSet permissions for this user first.", username)); + return; + } + + bool ok = false; + const QString password = QInputDialog::getText( + m_page, + i18n("FTP Password"), + i18n("New FTP password for '%1':", username), + QLineEdit::Password, + QString(), + &ok); + + if (!ok || password.isEmpty()) + return; + + if (createFtpUser(username, password)) { + QMessageBox::information(m_page, + i18n("FTP Password"), + i18n("Password for '%1' updated.", username)); + } else { + QMessageBox::warning(m_page, + i18n("FTP Password"), + i18n("Failed to update password for '%1'.", username)); + } +} + #include "ftpshareplugin.moc" diff --git a/kf5/src/ftpshareplugin.h b/kf5/src/ftpshareplugin.h index 46fedc9..a0dd349 100644 --- a/kf5/src/ftpshareplugin.h +++ b/kf5/src/ftpshareplugin.h @@ -5,6 +5,7 @@ #include #include #include +#include #include struct UserPermission { @@ -22,6 +23,7 @@ public: private: void loadCurrentShare(); void onShareToggled(bool checked); + void onChangePassword(); QString m_path; QString m_defaultShareName; @@ -29,8 +31,8 @@ private: QWidget *m_page = nullptr; QCheckBox *m_shareCheckBox = nullptr; QLineEdit *m_nameEdit = nullptr; - QCheckBox *m_anonymousCheckBox = nullptr; QWidget *m_usersWidget = nullptr; + QComboBox *m_pwUserCombo = nullptr; QVector m_userPerms; diff --git a/kf6/src/ftpshareplugin.cpp b/kf6/src/ftpshareplugin.cpp index 9882919..ab4f094 100644 --- a/kf6/src/ftpshareplugin.cpp +++ b/kf6/src/ftpshareplugin.cpp @@ -5,7 +5,9 @@ #include #include +#include #include +#include #include #include #include @@ -37,6 +39,26 @@ static int indexForAcl(const QString &acl) return 0; } +// Check if a virtual FTP user exists +static bool ftpUserExists(const QString &username) +{ + QProcess proc; + proc.start(QStringLiteral("dolphin-ftp-share"), + {QStringLiteral("userexists"), username}); + proc.waitForFinished(5000); + return proc.exitCode() == 0; +} + +// Create a virtual FTP user with password +static bool createFtpUser(const QString &username, const QString &password) +{ + QProcess proc; + proc.start(QStringLiteral("dolphin-ftp-share"), + {QStringLiteral("passwd"), username, password}); + proc.waitForFinished(5000); + return proc.exitCode() == 0; +} + // Get list of local users (UID >= 1000, excluding nobody) static QStringList getLocalUsers() { @@ -89,13 +111,8 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args) nameLayout->addRow(i18n("Name:"), m_nameEdit); mainLayout->addLayout(nameLayout); - // Anonymous access - m_anonymousCheckBox = new QCheckBox(i18n("Allow Anonymous Access")); - mainLayout->addWidget(m_anonymousCheckBox); - // User permissions section - auto *usersGroup = new QGroupBox(); - usersGroup->setFlat(true); + auto *usersGroup = new QGroupBox(i18n("User Permissions")); auto *usersLayout = new QVBoxLayout(usersGroup); // Scroll area for user list @@ -123,6 +140,18 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args) usersLayout->addWidget(scrollArea); mainLayout->addWidget(usersGroup); + // Change password section + auto *pwGroup = new QGroupBox(i18n("FTP Password")); + auto *pwLayout = new QHBoxLayout(pwGroup); + m_pwUserCombo = new QComboBox(); + m_pwUserCombo->addItems(users); + auto *pwButton = new QPushButton(i18n("Change Password...")); + pwLayout->addWidget(m_pwUserCombo); + pwLayout->addWidget(pwButton); + mainLayout->addWidget(pwGroup); + + connect(pwButton, &QPushButton::clicked, this, &FtpSharePlugin::onChangePassword); + mainLayout->addStretch(); // Connect signals @@ -130,8 +159,6 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args) this, &FtpSharePlugin::onShareToggled); connect(m_nameEdit, &QLineEdit::textChanged, this, [this]() { setDirty(true); }); - connect(m_anonymousCheckBox, &QCheckBox::toggled, - this, [this]() { setDirty(true); }); // Load existing share info loadCurrentShare(); @@ -146,7 +173,6 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args) void FtpSharePlugin::onShareToggled(bool checked) { m_nameEdit->setEnabled(checked); - m_anonymousCheckBox->setEnabled(checked); m_usersWidget->setEnabled(checked); setDirty(true); } @@ -167,12 +193,10 @@ void FtpSharePlugin::loadCurrentShare() // Format: // [sharename] // path=/some/path - // anonymous=y // user_acl=user1:rw,user2:ro QString currentSection; QString sharePath; QString shareAcl; - QString anonymous; const QStringList lines = output.split(QLatin1Char('\n')); for (const QString &line : lines) { @@ -185,13 +209,10 @@ void FtpSharePlugin::loadCurrentShare() currentSection = line.mid(1, line.indexOf(QLatin1Char(']')) - 1); sharePath.clear(); shareAcl.clear(); - anonymous.clear(); } else if (line.startsWith(QLatin1String("path="))) { sharePath = line.mid(5); } else if (line.startsWith(QLatin1String("user_acl="))) { shareAcl = line.mid(9); - } else if (line.startsWith(QLatin1String("anonymous="))) { - anonymous = line.mid(10); } } @@ -204,7 +225,6 @@ void FtpSharePlugin::loadCurrentShare() m_originalShareName = currentSection; m_shareCheckBox->setChecked(true); m_nameEdit->setText(currentSection); - m_anonymousCheckBox->setChecked(anonymous == QLatin1String("y")); // Parse ACL: "user1:rw,user2:ro" if (!shareAcl.isEmpty()) { @@ -257,6 +277,41 @@ void FtpSharePlugin::applyChanges() proc.waitForFinished(5000); } + // Create FTP users for users that have permissions but no FTP account yet + for (const auto &up : m_userPerms) { + if (up.combo->currentIndex() == 0) + continue; // No permission set, skip + + if (ftpUserExists(up.username)) + continue; // Already has FTP account + + // Ask for FTP password + bool ok = false; + const QString password = QInputDialog::getText( + m_page, + i18n("FTP Password"), + i18n("Set FTP password for user '%1':", up.username), + QLineEdit::Password, + QString(), + &ok); + + if (!ok || password.isEmpty()) { + QMessageBox::warning(m_page, + i18n("FTP Share"), + i18n("No password set for '%1'. This user will be skipped.", up.username)); + up.combo->setCurrentIndex(0); + continue; + } + + if (!createFtpUser(up.username, password)) { + QMessageBox::warning(m_page, + i18n("FTP Share"), + i18n("Failed to create FTP user '%1'.", up.username)); + up.combo->setCurrentIndex(0); + continue; + } + } + // Build ACL string QStringList aclParts; for (const auto &up : m_userPerms) { @@ -267,14 +322,11 @@ void FtpSharePlugin::applyChanges() } const QString aclString = aclParts.join(QLatin1Char(',')); - const QString anonymous = m_anonymousCheckBox->isChecked() - ? QStringLiteral("y") - : QStringLiteral("n"); - // dolphin-ftp-share add + // dolphin-ftp-share add QProcess proc; proc.start(QStringLiteral("dolphin-ftp-share"), - {QStringLiteral("add"), shareName, m_path, anonymous, aclString}); + {QStringLiteral("add"), shareName, m_path, aclString}); proc.waitForFinished(5000); if (proc.exitCode() == 0) { @@ -283,4 +335,40 @@ void FtpSharePlugin::applyChanges() } } +void FtpSharePlugin::onChangePassword() +{ + const QString username = m_pwUserCombo->currentText(); + if (username.isEmpty()) + return; + + if (!ftpUserExists(username)) { + QMessageBox::information(m_page, + i18n("FTP Password"), + i18n("User '%1' has no FTP account yet.\nSet permissions for this user first.", username)); + return; + } + + bool ok = false; + const QString password = QInputDialog::getText( + m_page, + i18n("FTP Password"), + i18n("New FTP password for '%1':", username), + QLineEdit::Password, + QString(), + &ok); + + if (!ok || password.isEmpty()) + return; + + if (createFtpUser(username, password)) { + QMessageBox::information(m_page, + i18n("FTP Password"), + i18n("Password for '%1' updated.", username)); + } else { + QMessageBox::warning(m_page, + i18n("FTP Password"), + i18n("Failed to update password for '%1'.", username)); + } +} + #include "ftpshareplugin.moc" diff --git a/kf6/src/ftpshareplugin.h b/kf6/src/ftpshareplugin.h index 46fedc9..a0dd349 100644 --- a/kf6/src/ftpshareplugin.h +++ b/kf6/src/ftpshareplugin.h @@ -5,6 +5,7 @@ #include #include #include +#include #include struct UserPermission { @@ -22,6 +23,7 @@ public: private: void loadCurrentShare(); void onShareToggled(bool checked); + void onChangePassword(); QString m_path; QString m_defaultShareName; @@ -29,8 +31,8 @@ private: QWidget *m_page = nullptr; QCheckBox *m_shareCheckBox = nullptr; QLineEdit *m_nameEdit = nullptr; - QCheckBox *m_anonymousCheckBox = nullptr; QWidget *m_usersWidget = nullptr; + QComboBox *m_pwUserCombo = nullptr; QVector m_userPerms; diff --git a/scripts/dolphin-ftp-share b/scripts/dolphin-ftp-share index 045b798..91d84f8 100755 --- a/scripts/dolphin-ftp-share +++ b/scripts/dolphin-ftp-share @@ -1,21 +1,108 @@ #!/bin/bash -# dolphin-ftp-share - Manage FTP folder shares for Dolphin +# dolphin-ftp-share - Manage FTP folder shares for Dolphin with Pure-FTPd +# +# Each user gets a personal FTP root with only their shared folders. +# Virtual FTP users (PureDB) are created automatically. +# Permissions (ro/rw) are enforced via bind mount options. # # Usage: +# dolphin-ftp-share setup Initial Pure-FTPd setup # dolphin-ftp-share info List all shares -# dolphin-ftp-share add Add/update a share +# dolphin-ftp-share add Add/update a share # dolphin-ftp-share delete Remove a share -# -# Share configs are stored in ~/.local/share/dolphin-ftp-shares/ -# Shares are bind-mounted into ~/.local/share/dolphin-ftp-root/ -# -# The FTP root directory can be served by any FTP server (e.g. vsftpd). -# Each share appears as a real subdirectory under the FTP root via bind mount. +# dolphin-ftp-share passwd Set FTP password set -e SHARE_DIR="${HOME}/.local/share/dolphin-ftp-shares" FTP_ROOT="${HOME}/.local/share/dolphin-ftp-root" +PASSWD_FILE="/etc/pure-ftpd/pureftpd.passwd" +PDB_FILE="/etc/pure-ftpd/pureftpd.pdb" + +CALLER_UID=$(id -u) +CALLER_GID=$(id -g) + +# Update home directory for an existing virtual FTP user. +ensure_virtual_user() { + local username="$1" + local user_root="$FTP_ROOT/users/$username" + + mkdir -p "$user_root" + + if sudo pure-pw show "$username" -f "$PASSWD_FILE" >/dev/null 2>&1; then + sudo pure-pw usermod "$username" \ + -d "$user_root" \ + -f "$PASSWD_FILE" 2>/dev/null + fi +} + +rebuild_db() { + sudo pure-pw mkdb "$PDB_FILE" -f "$PASSWD_FILE" +} + +# Unmount a share from all user roots +unmount_share() { + local name="$1" + if [ -d "$FTP_ROOT/users" ]; then + for user_dir in "$FTP_ROOT/users"/*/; do + [ -d "$user_dir" ] || continue + local mp="${user_dir}${name}" + if mountpoint -q "$mp" 2>/dev/null; then + sudo umount "$mp" + fi + rmdir "$mp" 2>/dev/null || true + done + fi +} + +cmd_setup() { + echo "Setting up Pure-FTPd for Dolphin FTP sharing..." + + mkdir -p "$FTP_ROOT/users" + + # Enable PureDB authentication + echo "$PDB_FILE" | sudo tee /etc/pure-ftpd/conf/PureDB >/dev/null + sudo ln -sf /etc/pure-ftpd/conf/PureDB /etc/pure-ftpd/auth/50pure + + # Chroot all users to their FTP home + echo "yes" | sudo tee /etc/pure-ftpd/conf/ChrootEveryone >/dev/null + + # Disable anonymous access — only authenticated virtual users + echo "yes" | sudo tee /etc/pure-ftpd/conf/NoAnonymous >/dev/null + + # Remove system auth so only PureDB users can log in + sudo rm -f /etc/pure-ftpd/auth/65unix /etc/pure-ftpd/auth/70pam + + # Create empty password DB if needed + sudo touch "$PASSWD_FILE" + sudo pure-pw mkdb "$PDB_FILE" -f "$PASSWD_FILE" + + # Enable logging + echo "yes" | sudo tee /etc/pure-ftpd/conf/VerboseLog >/dev/null + + # Create sudoers rule for passwordless mount/umount/pure-pw + # Use SUDO_USER to get the real user (not root) when run with sudo + local REAL_USER="${SUDO_USER:-$USER}" + local REAL_HOME + REAL_HOME=$(getent passwd "$REAL_USER" | cut -d: -f6) + local REAL_FTP_ROOT="${REAL_HOME}/.local/share/dolphin-ftp-root" + + sudo tee /etc/sudoers.d/dolphin-ftp-share >/dev/null < " +} cmd_info() { [ -d "$SHARE_DIR" ] || exit 0 @@ -31,33 +118,49 @@ cmd_info() { cmd_add() { local name="$1" local path="$2" - local anon="$3" - local acl="$4" + local acl="$3" if [ -z "$name" ] || [ -z "$path" ]; then echo "Error: name and path are required" >&2 exit 1 fi - mkdir -p "$SHARE_DIR" "$FTP_ROOT" + mkdir -p "$SHARE_DIR" "$FTP_ROOT/users" - # Write share config + # Unmount any existing mounts for this share (handles permission changes) + unmount_share "$name" + + # Save config cat > "$SHARE_DIR/$name.conf" </dev/null && sudo umount "$FTP_ROOT/$name" 2>/dev/null || true + ensure_virtual_user "$username" - # Bind-mount the shared folder into the FTP root - sudo mount --bind "$path" "$FTP_ROOT/$name" + mkdir -p "$mount_point" - echo "Share '$name' added: $path -> $FTP_ROOT/$name" + # Bind mount the shared folder into user's personal FTP root + sudo mount --bind "$path" "$mount_point" + + # Read-only: remount with ro flag + if [ "$perm" = "ro" ]; then + sudo mount -o remount,ro,bind "$mount_point" + fi + done + fi + + rebuild_db + echo "Share '$name' added: $path" } cmd_delete() { @@ -68,32 +171,77 @@ cmd_delete() { exit 1 fi - # Unmount bind mount - if mountpoint -q "$FTP_ROOT/$name" 2>/dev/null; then - sudo umount "$FTP_ROOT/$name" - fi - + unmount_share "$name" rm -f "$SHARE_DIR/$name.conf" - rmdir "$FTP_ROOT/$name" 2>/dev/null || true - # Clean up empty directories + # Clean up rmdir "$SHARE_DIR" 2>/dev/null || true - rmdir "$FTP_ROOT" 2>/dev/null || true echo "Share '$name' deleted" } +cmd_userexists() { + local username="$1" + + if [ -z "$username" ]; then + exit 1 + fi + + if sudo pure-pw show "$username" -f "$PASSWD_FILE" >/dev/null 2>&1; then + exit 0 + else + exit 1 + fi +} + +cmd_passwd() { + local username="$1" + local password="$2" + + if [ -z "$username" ] || [ -z "$password" ]; then + echo "Error: username and password are required" >&2 + exit 1 + fi + + local user_root="$FTP_ROOT/users/$username" + mkdir -p "$user_root" + + # Generate MD5 password hash + local hash + hash=$(openssl passwd -1 "$password") + + if sudo pure-pw show "$username" -f "$PASSWD_FILE" >/dev/null 2>&1; then + # User exists — update password in-place + sudo sed -i "s|^${username}:[^:]*:|${username}:${hash}:|" "$PASSWD_FILE" + echo "Password updated for '$username'" + else + # User doesn't exist — add entry to passwd file + # Format: username:hash:uid:gid:gecos:home:uploadbw:downloadbw:uploadratio:downloadratio:maxconn:filesquota:sizequota:allowedlocalip:allowedclientip:timerestrictions + echo "${username}:${hash}:${CALLER_UID}:${CALLER_GID}::${user_root}::::::::::::" | \ + sudo tee -a "$PASSWD_FILE" >/dev/null + echo "Created FTP user '$username'" + fi + + rebuild_db +} + case "${1:-}" in - info) cmd_info ;; - add) cmd_add "$2" "$3" "${4:-n}" "${5:-}" ;; - delete) cmd_delete "$2" ;; + setup) cmd_setup ;; + info) cmd_info ;; + add) cmd_add "$2" "$3" "${4:-}" ;; + delete) cmd_delete "$2" ;; + userexists) cmd_userexists "$2" ;; + passwd) cmd_passwd "$2" "$3" ;; *) - echo "Usage: dolphin-ftp-share {info|add|delete}" >&2 + echo "Usage: dolphin-ftp-share {setup|info|add|delete|userexists|passwd}" >&2 echo "" >&2 echo "Commands:" >&2 + echo " setup Initial Pure-FTPd setup" >&2 echo " info List all shares" >&2 - echo " add Add/update a share" >&2 + echo " add Add/update a share" >&2 echo " delete Remove a share" >&2 + echo " userexists Check if FTP user exists" >&2 + echo " passwd Set/create FTP password" >&2 exit 1 ;; esac