Umbau auf Pure-FTPd mit PureDB Virtual Users und Passwort-Dialog

- 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) <noreply@anthropic.com>
This commit is contained in:
duffyduck 2026-04-04 19:29:36 +02:00
parent 34a94f0d82
commit 326d8e97ac
6 changed files with 460 additions and 142 deletions

124
README.md
View File

@ -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) - **FTP-Tab** in Dolphins Ordner-Eigenschaften (Rechtsklick → Eigenschaften)
- Freigabe per Checkbox aktivieren/deaktivieren - Freigabe per Checkbox aktivieren/deaktivieren
- Freigabename konfigurierbar (Standard = Ordnername) - Freigabename konfigurierbar (Standard = Ordnername)
- Anonymen Zugang erlauben - **Per-User Berechtigungen**: Read Only / Read-Write pro Benutzer
- Berechtigungen pro Benutzer: Read Only / Read-Write - Jeder Benutzer sieht **nur seine freigegebenen Ordner** per FTP
- Nutzt das Helper-Script `dolphin-ftp-share` — **kein Root nötig!** - Read-Only wird direkt am Dateisystem durchgesetzt (read-only Bind-Mount)
- Erkennt bestehende Freigaben und lädt deren Einstellungen - Automatische Einrichtung mit `dolphin-ftp-share setup`
## Voraussetzungen ## Voraussetzungen
- Ein FTP-Server (z.B. `vsftpd`) - Pure-FTPd als FTP-Server
- Das Helper-Script `dolphin-ftp-share` im PATH - Das Helper-Script `dolphin-ftp-share` im PATH
- Build-Dependencies (siehe unten) - Build-Dependencies (siehe unten)
### FTP-Server installieren (vsftpd) ## Einrichtung
### 1. Pure-FTPd installieren
```bash ```bash
sudo apt install vsftpd sudo apt install pure-ftpd
``` ```
### vsftpd konfigurieren ### 2. Helper-Script installieren
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
```bash ```bash
sudo cp scripts/dolphin-ftp-share /usr/local/bin/ sudo cp scripts/dolphin-ftp-share /usr/local/bin/
sudo chmod +x /usr/local/bin/dolphin-ftp-share 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). Das Setup konfiguriert Pure-FTPd komplett und erstellt die sudoers-Regel:
Damit das ohne Passwort-Eingabe funktioniert:
```bash ```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 <benutzername> <neues-passwort>
``` ```
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 ## Versionen
@ -165,8 +148,12 @@ sudo rm /usr/lib/x86_64-linux-gnu/qt5/plugins/kf5/propertiesdialog/ftpshareplugi
# KF6 # KF6
sudo rm /usr/lib/x86_64-linux-gnu/qt6/plugins/kf6/propertiesdialog/ftpshareplugin.so 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 /usr/local/bin/dolphin-ftp-share
sudo rm /etc/sudoers.d/dolphin-ftp-share
# Pure-FTPd (optional)
sudo apt remove pure-ftpd
``` ```
## Projektstruktur ## Projektstruktur
@ -196,28 +183,31 @@ kde-dolphin-ftp-sharing-tab/
## Wie es funktioniert ## 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/<name>/`)
- 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 <name> <path> <anonymous> <acl>` — Freigabe erstellen ### Kommandos
- `dolphin-ftp-share delete <name>` — Freigabe löschen
- `dolphin-ftp-share info` — bestehende Freigaben auflisten
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. | Kommando | Beschreibung |
|---|---|
### Konfigurationsformat | `dolphin-ftp-share setup` | Einmalige Pure-FTPd Einrichtung |
| `dolphin-ftp-share add <name> <path> <acl>` | Freigabe erstellen |
Jede Freigabe wird als `.conf`-Datei gespeichert: | `dolphin-ftp-share delete <name>` | Freigabe löschen |
| `dolphin-ftp-share info` | Bestehende Freigaben auflisten |
```ini | `dolphin-ftp-share passwd <user> <pass>` | FTP-Passwort ändern |
path=/pfad/zum/ordner
anonymous=y
user_acl=user1:rw,user2:ro
```
### Berechtigungen ### Berechtigungen
| Level | Beschreibung | | Level | Beschreibung |
|---|---| |---|---|
| `---` | Kein Zugriff — Ordner ist für diesen Benutzer nicht sichtbar |
| `ro` | Read Only — nur Download | | `ro` | Read Only — nur Download |
| `rw` | Read-Write — Upload und Download | | `rw` | Read-Write — Upload und Download |

View File

@ -5,7 +5,9 @@
#include <QFormLayout> #include <QFormLayout>
#include <QGroupBox> #include <QGroupBox>
#include <QInputDialog>
#include <QLabel> #include <QLabel>
#include <QMessageBox>
#include <QProcess> #include <QProcess>
#include <QScrollArea> #include <QScrollArea>
#include <QVBoxLayout> #include <QVBoxLayout>
@ -37,6 +39,26 @@ static int indexForAcl(const QString &acl)
return 0; 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) // Get list of local users (UID >= 1000, excluding nobody)
static QStringList getLocalUsers() static QStringList getLocalUsers()
{ {
@ -89,13 +111,8 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
nameLayout->addRow(i18n("Name:"), m_nameEdit); nameLayout->addRow(i18n("Name:"), m_nameEdit);
mainLayout->addLayout(nameLayout); mainLayout->addLayout(nameLayout);
// Anonymous access
m_anonymousCheckBox = new QCheckBox(i18n("Allow Anonymous Access"));
mainLayout->addWidget(m_anonymousCheckBox);
// User permissions section // User permissions section
auto *usersGroup = new QGroupBox(); auto *usersGroup = new QGroupBox(i18n("User Permissions"));
usersGroup->setFlat(true);
auto *usersLayout = new QVBoxLayout(usersGroup); auto *usersLayout = new QVBoxLayout(usersGroup);
// Scroll area for user list // Scroll area for user list
@ -123,6 +140,18 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
usersLayout->addWidget(scrollArea); usersLayout->addWidget(scrollArea);
mainLayout->addWidget(usersGroup); 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(); mainLayout->addStretch();
// Connect signals // Connect signals
@ -130,8 +159,6 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
this, &FtpSharePlugin::onShareToggled); this, &FtpSharePlugin::onShareToggled);
connect(m_nameEdit, &QLineEdit::textChanged, connect(m_nameEdit, &QLineEdit::textChanged,
this, [this]() { setDirty(true); }); this, [this]() { setDirty(true); });
connect(m_anonymousCheckBox, &QCheckBox::toggled,
this, [this]() { setDirty(true); });
// Load existing share info // Load existing share info
loadCurrentShare(); loadCurrentShare();
@ -146,7 +173,6 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
void FtpSharePlugin::onShareToggled(bool checked) void FtpSharePlugin::onShareToggled(bool checked)
{ {
m_nameEdit->setEnabled(checked); m_nameEdit->setEnabled(checked);
m_anonymousCheckBox->setEnabled(checked);
m_usersWidget->setEnabled(checked); m_usersWidget->setEnabled(checked);
setDirty(true); setDirty(true);
} }
@ -167,12 +193,10 @@ void FtpSharePlugin::loadCurrentShare()
// Format: // Format:
// [sharename] // [sharename]
// path=/some/path // path=/some/path
// anonymous=y
// user_acl=user1:rw,user2:ro // user_acl=user1:rw,user2:ro
QString currentSection; QString currentSection;
QString sharePath; QString sharePath;
QString shareAcl; QString shareAcl;
QString anonymous;
const QStringList lines = output.split(QLatin1Char('\n')); const QStringList lines = output.split(QLatin1Char('\n'));
for (const QString &line : lines) { for (const QString &line : lines) {
@ -185,13 +209,10 @@ void FtpSharePlugin::loadCurrentShare()
currentSection = line.mid(1, line.indexOf(QLatin1Char(']')) - 1); currentSection = line.mid(1, line.indexOf(QLatin1Char(']')) - 1);
sharePath.clear(); sharePath.clear();
shareAcl.clear(); shareAcl.clear();
anonymous.clear();
} else if (line.startsWith(QLatin1String("path="))) { } else if (line.startsWith(QLatin1String("path="))) {
sharePath = line.mid(5); sharePath = line.mid(5);
} else if (line.startsWith(QLatin1String("user_acl="))) { } else if (line.startsWith(QLatin1String("user_acl="))) {
shareAcl = line.mid(9); 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_originalShareName = currentSection;
m_shareCheckBox->setChecked(true); m_shareCheckBox->setChecked(true);
m_nameEdit->setText(currentSection); m_nameEdit->setText(currentSection);
m_anonymousCheckBox->setChecked(anonymous == QLatin1String("y"));
// Parse ACL: "user1:rw,user2:ro" // Parse ACL: "user1:rw,user2:ro"
if (!shareAcl.isEmpty()) { if (!shareAcl.isEmpty()) {
@ -257,6 +277,41 @@ void FtpSharePlugin::applyChanges()
proc.waitForFinished(5000); 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 // Build ACL string
QStringList aclParts; QStringList aclParts;
for (const auto &up : m_userPerms) { for (const auto &up : m_userPerms) {
@ -267,14 +322,11 @@ void FtpSharePlugin::applyChanges()
} }
const QString aclString = aclParts.join(QLatin1Char(',')); const QString aclString = aclParts.join(QLatin1Char(','));
const QString anonymous = m_anonymousCheckBox->isChecked()
? QStringLiteral("y")
: QStringLiteral("n");
// dolphin-ftp-share add <name> <path> <anonymous> <acl> // dolphin-ftp-share add <name> <path> <acl>
QProcess proc; QProcess proc;
proc.start(QStringLiteral("dolphin-ftp-share"), proc.start(QStringLiteral("dolphin-ftp-share"),
{QStringLiteral("add"), shareName, m_path, anonymous, aclString}); {QStringLiteral("add"), shareName, m_path, aclString});
proc.waitForFinished(5000); proc.waitForFinished(5000);
if (proc.exitCode() == 0) { 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" #include "ftpshareplugin.moc"

View File

@ -5,6 +5,7 @@
#include <QCheckBox> #include <QCheckBox>
#include <QComboBox> #include <QComboBox>
#include <QLineEdit> #include <QLineEdit>
#include <QPushButton>
#include <QWidget> #include <QWidget>
struct UserPermission { struct UserPermission {
@ -22,6 +23,7 @@ public:
private: private:
void loadCurrentShare(); void loadCurrentShare();
void onShareToggled(bool checked); void onShareToggled(bool checked);
void onChangePassword();
QString m_path; QString m_path;
QString m_defaultShareName; QString m_defaultShareName;
@ -29,8 +31,8 @@ private:
QWidget *m_page = nullptr; QWidget *m_page = nullptr;
QCheckBox *m_shareCheckBox = nullptr; QCheckBox *m_shareCheckBox = nullptr;
QLineEdit *m_nameEdit = nullptr; QLineEdit *m_nameEdit = nullptr;
QCheckBox *m_anonymousCheckBox = nullptr;
QWidget *m_usersWidget = nullptr; QWidget *m_usersWidget = nullptr;
QComboBox *m_pwUserCombo = nullptr;
QVector<UserPermission> m_userPerms; QVector<UserPermission> m_userPerms;

View File

@ -5,7 +5,9 @@
#include <QFormLayout> #include <QFormLayout>
#include <QGroupBox> #include <QGroupBox>
#include <QInputDialog>
#include <QLabel> #include <QLabel>
#include <QMessageBox>
#include <QProcess> #include <QProcess>
#include <QScrollArea> #include <QScrollArea>
#include <QVBoxLayout> #include <QVBoxLayout>
@ -37,6 +39,26 @@ static int indexForAcl(const QString &acl)
return 0; 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) // Get list of local users (UID >= 1000, excluding nobody)
static QStringList getLocalUsers() static QStringList getLocalUsers()
{ {
@ -89,13 +111,8 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
nameLayout->addRow(i18n("Name:"), m_nameEdit); nameLayout->addRow(i18n("Name:"), m_nameEdit);
mainLayout->addLayout(nameLayout); mainLayout->addLayout(nameLayout);
// Anonymous access
m_anonymousCheckBox = new QCheckBox(i18n("Allow Anonymous Access"));
mainLayout->addWidget(m_anonymousCheckBox);
// User permissions section // User permissions section
auto *usersGroup = new QGroupBox(); auto *usersGroup = new QGroupBox(i18n("User Permissions"));
usersGroup->setFlat(true);
auto *usersLayout = new QVBoxLayout(usersGroup); auto *usersLayout = new QVBoxLayout(usersGroup);
// Scroll area for user list // Scroll area for user list
@ -123,6 +140,18 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
usersLayout->addWidget(scrollArea); usersLayout->addWidget(scrollArea);
mainLayout->addWidget(usersGroup); 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(); mainLayout->addStretch();
// Connect signals // Connect signals
@ -130,8 +159,6 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
this, &FtpSharePlugin::onShareToggled); this, &FtpSharePlugin::onShareToggled);
connect(m_nameEdit, &QLineEdit::textChanged, connect(m_nameEdit, &QLineEdit::textChanged,
this, [this]() { setDirty(true); }); this, [this]() { setDirty(true); });
connect(m_anonymousCheckBox, &QCheckBox::toggled,
this, [this]() { setDirty(true); });
// Load existing share info // Load existing share info
loadCurrentShare(); loadCurrentShare();
@ -146,7 +173,6 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
void FtpSharePlugin::onShareToggled(bool checked) void FtpSharePlugin::onShareToggled(bool checked)
{ {
m_nameEdit->setEnabled(checked); m_nameEdit->setEnabled(checked);
m_anonymousCheckBox->setEnabled(checked);
m_usersWidget->setEnabled(checked); m_usersWidget->setEnabled(checked);
setDirty(true); setDirty(true);
} }
@ -167,12 +193,10 @@ void FtpSharePlugin::loadCurrentShare()
// Format: // Format:
// [sharename] // [sharename]
// path=/some/path // path=/some/path
// anonymous=y
// user_acl=user1:rw,user2:ro // user_acl=user1:rw,user2:ro
QString currentSection; QString currentSection;
QString sharePath; QString sharePath;
QString shareAcl; QString shareAcl;
QString anonymous;
const QStringList lines = output.split(QLatin1Char('\n')); const QStringList lines = output.split(QLatin1Char('\n'));
for (const QString &line : lines) { for (const QString &line : lines) {
@ -185,13 +209,10 @@ void FtpSharePlugin::loadCurrentShare()
currentSection = line.mid(1, line.indexOf(QLatin1Char(']')) - 1); currentSection = line.mid(1, line.indexOf(QLatin1Char(']')) - 1);
sharePath.clear(); sharePath.clear();
shareAcl.clear(); shareAcl.clear();
anonymous.clear();
} else if (line.startsWith(QLatin1String("path="))) { } else if (line.startsWith(QLatin1String("path="))) {
sharePath = line.mid(5); sharePath = line.mid(5);
} else if (line.startsWith(QLatin1String("user_acl="))) { } else if (line.startsWith(QLatin1String("user_acl="))) {
shareAcl = line.mid(9); 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_originalShareName = currentSection;
m_shareCheckBox->setChecked(true); m_shareCheckBox->setChecked(true);
m_nameEdit->setText(currentSection); m_nameEdit->setText(currentSection);
m_anonymousCheckBox->setChecked(anonymous == QLatin1String("y"));
// Parse ACL: "user1:rw,user2:ro" // Parse ACL: "user1:rw,user2:ro"
if (!shareAcl.isEmpty()) { if (!shareAcl.isEmpty()) {
@ -257,6 +277,41 @@ void FtpSharePlugin::applyChanges()
proc.waitForFinished(5000); 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 // Build ACL string
QStringList aclParts; QStringList aclParts;
for (const auto &up : m_userPerms) { for (const auto &up : m_userPerms) {
@ -267,14 +322,11 @@ void FtpSharePlugin::applyChanges()
} }
const QString aclString = aclParts.join(QLatin1Char(',')); const QString aclString = aclParts.join(QLatin1Char(','));
const QString anonymous = m_anonymousCheckBox->isChecked()
? QStringLiteral("y")
: QStringLiteral("n");
// dolphin-ftp-share add <name> <path> <anonymous> <acl> // dolphin-ftp-share add <name> <path> <acl>
QProcess proc; QProcess proc;
proc.start(QStringLiteral("dolphin-ftp-share"), proc.start(QStringLiteral("dolphin-ftp-share"),
{QStringLiteral("add"), shareName, m_path, anonymous, aclString}); {QStringLiteral("add"), shareName, m_path, aclString});
proc.waitForFinished(5000); proc.waitForFinished(5000);
if (proc.exitCode() == 0) { 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" #include "ftpshareplugin.moc"

View File

@ -5,6 +5,7 @@
#include <QCheckBox> #include <QCheckBox>
#include <QComboBox> #include <QComboBox>
#include <QLineEdit> #include <QLineEdit>
#include <QPushButton>
#include <QWidget> #include <QWidget>
struct UserPermission { struct UserPermission {
@ -22,6 +23,7 @@ public:
private: private:
void loadCurrentShare(); void loadCurrentShare();
void onShareToggled(bool checked); void onShareToggled(bool checked);
void onChangePassword();
QString m_path; QString m_path;
QString m_defaultShareName; QString m_defaultShareName;
@ -29,8 +31,8 @@ private:
QWidget *m_page = nullptr; QWidget *m_page = nullptr;
QCheckBox *m_shareCheckBox = nullptr; QCheckBox *m_shareCheckBox = nullptr;
QLineEdit *m_nameEdit = nullptr; QLineEdit *m_nameEdit = nullptr;
QCheckBox *m_anonymousCheckBox = nullptr;
QWidget *m_usersWidget = nullptr; QWidget *m_usersWidget = nullptr;
QComboBox *m_pwUserCombo = nullptr;
QVector<UserPermission> m_userPerms; QVector<UserPermission> m_userPerms;

View File

@ -1,21 +1,108 @@
#!/bin/bash #!/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: # Usage:
# dolphin-ftp-share setup Initial Pure-FTPd setup
# dolphin-ftp-share info List all shares # dolphin-ftp-share info List all shares
# dolphin-ftp-share add <name> <path> <anon> <acl> Add/update a share # dolphin-ftp-share add <name> <path> <acl> Add/update a share
# dolphin-ftp-share delete <name> Remove a share # dolphin-ftp-share delete <name> Remove a share
# # dolphin-ftp-share passwd <username> <password> Set FTP password
# 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.
set -e set -e
SHARE_DIR="${HOME}/.local/share/dolphin-ftp-shares" SHARE_DIR="${HOME}/.local/share/dolphin-ftp-shares"
FTP_ROOT="${HOME}/.local/share/dolphin-ftp-root" 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 <<EOF
${REAL_USER} ALL=(root) NOPASSWD: /usr/bin/mount --bind *
${REAL_USER} ALL=(root) NOPASSWD: /usr/bin/mount -o remount?ro?bind *
${REAL_USER} ALL=(root) NOPASSWD: /usr/bin/umount ${REAL_FTP_ROOT}/*
${REAL_USER} ALL=(root) NOPASSWD: /usr/bin/pure-pw *
EOF
sudo chmod 440 /etc/sudoers.d/dolphin-ftp-share
sudo systemctl restart pure-ftpd
echo ""
echo "Setup complete!"
echo "Share folders via Dolphin: right-click folder → Properties → FTP tab"
echo "Default FTP password = username. Change with:"
echo " dolphin-ftp-share passwd <user> <newpassword>"
}
cmd_info() { cmd_info() {
[ -d "$SHARE_DIR" ] || exit 0 [ -d "$SHARE_DIR" ] || exit 0
@ -31,33 +118,49 @@ cmd_info() {
cmd_add() { cmd_add() {
local name="$1" local name="$1"
local path="$2" local path="$2"
local anon="$3" local acl="$3"
local acl="$4"
if [ -z "$name" ] || [ -z "$path" ]; then if [ -z "$name" ] || [ -z "$path" ]; then
echo "Error: name and path are required" >&2 echo "Error: name and path are required" >&2
exit 1 exit 1
fi 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" <<EOF cat > "$SHARE_DIR/$name.conf" <<EOF
path=$path path=$path
anonymous=$anon
user_acl=$acl user_acl=$acl
EOF EOF
# Create mount point directory # Process per-user ACL: "user1:ro,user2:rw"
mkdir -p "$FTP_ROOT/$name" if [ -n "$acl" ]; then
IFS=',' read -ra entries <<< "$acl"
for entry in "${entries[@]}"; do
local username="${entry%%:*}"
local perm="${entry##*:}"
local user_root="$FTP_ROOT/users/$username"
local mount_point="$user_root/$name"
# Unmount if already mounted ensure_virtual_user "$username"
mountpoint -q "$FTP_ROOT/$name" 2>/dev/null && sudo umount "$FTP_ROOT/$name" 2>/dev/null || true
# Bind-mount the shared folder into the FTP root mkdir -p "$mount_point"
sudo mount --bind "$path" "$FTP_ROOT/$name"
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() { cmd_delete() {
@ -68,32 +171,77 @@ cmd_delete() {
exit 1 exit 1
fi fi
# Unmount bind mount unmount_share "$name"
if mountpoint -q "$FTP_ROOT/$name" 2>/dev/null; then
sudo umount "$FTP_ROOT/$name"
fi
rm -f "$SHARE_DIR/$name.conf" 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 "$SHARE_DIR" 2>/dev/null || true
rmdir "$FTP_ROOT" 2>/dev/null || true
echo "Share '$name' deleted" 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 case "${1:-}" in
info) cmd_info ;; setup) cmd_setup ;;
add) cmd_add "$2" "$3" "${4:-n}" "${5:-}" ;; info) cmd_info ;;
delete) cmd_delete "$2" ;; 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 "" >&2
echo "Commands:" >&2 echo "Commands:" >&2
echo " setup Initial Pure-FTPd setup" >&2
echo " info List all shares" >&2 echo " info List all shares" >&2
echo " add <name> <path> <anon> <acl> Add/update a share" >&2 echo " add <name> <path> <acl> Add/update a share" >&2
echo " delete <name> Remove a share" >&2 echo " delete <name> Remove a share" >&2
echo " userexists <username> Check if FTP user exists" >&2
echo " passwd <username> <password> Set/create FTP password" >&2
exit 1 exit 1
;; ;;
esac esac