diff --git a/README.md b/README.md
index c80a28e..3cba906 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ This GUI for ProFTPd was written to support a basic user management feature when
There is no build-in security, so you have to protect the directory with something else, like Apache Basic Authentication.
-It's possible to use either of SHA1 and pbkdf2 with either of MySQL/MariaDB and sqlite3. pbkdf2 is supported since ProFTPd 1.3.5.
+It's possible to use either of SHA1, MYSQL_Backend and pbkdf2 with either of MySQL/MariaDB and sqlite3. pbkdf2 is supported since ProFTPd 1.3.5.
You can look at some [screenshots](screenshots/README.md) to see if this is the tool you need.
@@ -40,6 +40,11 @@ You can look at some [screenshots](screenshots/README.md) to see if this is the
If you want to upgrade the hashing algorithm you have to change all passwords after changing the configs (both ProFTPd and ProFTPd Admin).
+## Migration
+
+If you want to migrate from ProFTPd Admin 1.0 (http://proftpd-adm.sourceforge.net/), please use migrate_proftpd_admin_1_to_2.sh script (think to edit it to comply with your setup)
+Data will be read from original DB and reordered in a new DB. Please follow installation steps before starting the migration script.
+
## Installation
### Using MySQL and SHA1
@@ -86,6 +91,51 @@ SQLNamedQuery files-in-count UPDATE "files_in_used=files_in_used+1 WHE
8. Start ProFTPd.
9. Go to http://yourwebspace/proftpdadmin/ and start using it!
+### Using MySQL backend password hashing
+
+1. Install ProFTPd with MySQL support
+ - Debian: apt-get install proftpd-mod-mysql
+ - Gentoo: USE="mysql" emerge proftpd
+2. Create a MySQL database (use something like phpMyAdmin for this), for example: "proftpd".
+3. Use tables.sql to populate the database (you can use phpMyAdmin for this).
+4. Add the following to your proftpd.conf and sql.conf (edit to your needs):
+
+```
+CreateHome on 775
+AuthOrder mod_sql.c
+
+SQLBackend mysql
+SQLEngine on
+SQLPasswordEngine on
+SQLAuthenticate on
+SQLAuthTypes Backend
+
+SQLConnectInfo database@localhost username password
+SQLUserInfo users userid passwd uid gid homedir shell
+SQLGroupInfo groups groupname gid members
+SQLUserWhereClause "disabled != 1"
+SQLLog PASS updatecount
+SQLNamedQuery updatecount UPDATE "login_count=login_count+1, last_login=now() WHERE userid='%u'" users
+
+ # Used to track xfer traffic per user (without invoking a quota)
+SQLLog RETR bytes-out-count
+SQLNamedQuery bytes-out-count UPDATE "bytes_out_used=bytes_out_used+%b WHERE userid='%u'" users
+SQLLog RETR files-out-count
+SQLNamedQuery files-out-count UPDATE "files_out_used=files_out_used+1 WHERE userid='%u'" users
+
+SQLLog STOR bytes-in-count
+SQLNamedQuery bytes-in-count UPDATE "bytes_in_used=bytes_in_used+%b WHERE userid='%u'" users
+SQLLog STOR files-in-count
+SQLNamedQuery files-in-count UPDATE "files_in_used=files_in_used+1 WHERE userid='%u'" users
+```
+
+5. Extract all files to your webspace (into a subdirectory like "proftpdadmin").
+6. Secure access to this directory (for example: create a .htaccess file if using apache)
+7. Edit the configs/config_example.php file to your needs and rename it to config.php.
+8. Start ProFTPd.
+9. Go to http://yourwebspace/proftpdadmin/ and start using it!
+
+
### Using sqlite3 and pbkdf2
1. Install ProFTPd with sqlite3 support
diff --git a/add_group.php b/add_group.php
index 334916f..2693ff3 100644
--- a/add_group.php
+++ b/add_group.php
@@ -21,6 +21,10 @@
$field_groupname = $cfg['field_groupname'];
$field_members = $cfg['field_members'];
$errors = array();
+$action = explode('?', $_SERVER['REQUEST_URI'], 2)[0];
+
+/* find the right message for gid */
+$gidMessage = $ac->get_gid_message();
if (!empty($_REQUEST["action"]) && $_REQUEST["action"] == "create") {
/* group name validation */
@@ -37,15 +41,10 @@
if (empty($_REQUEST[$field_gid]) || !$ac->is_valid_id($_REQUEST[$field_gid])) {
array_push($errors, 'Invalid GID; GID must be a positive integer.');
}
- if ($cfg['max_gid'] != -1 && $cfg['min_gid'] != -1) {
- if ($_REQUEST[$field_gid] > $cfg['max_gid'] || $_REQUEST[$field_gid] < $cfg['min_gid']) {
- array_push($errors, 'Invalid GID; GID must be between ' . $cfg['min_gid'] . ' and ' . $cfg['max_gid'] . '.');
- }
- } else if ($cfg['max_gid'] != -1 && $_REQUEST[$field_gid] > $cfg['max_gid']) {
- array_push($errors, 'Invalid GID; GID must be at most ' . $cfg['max_gid'] . '.');
- } else if ($cfg['min_gid'] != -1 && $_REQUEST[$field_gid] < $cfg['min_gid']) {
- array_push($errors, 'Invalid GID; GID must be at least ' . $cfg['min_gid'] . '.');
+ if (($cfg['max_gid'] != -1 && $_REQUEST[$field_gid] > $cfg['max_gid']) or ($cfg['min_gid'] != -1 && $_REQUEST[$field_gid] < $cfg['min_gid'])) {
+ array_push($errors, 'Invalid GID; '.$gidMessage);
}
+
/* gid uniqueness validation */
if ($ac->check_gid($_REQUEST[$field_gid])) {
array_push($errors, 'GID already exists; GID must be unique.');
@@ -65,6 +64,29 @@
}
}
+/* Form values */
+if (isset($errormsg)) {
+ /* This is a failed attempt */
+ $groupname = $_REQUEST[$field_groupname];
+ $gid = $_REQUEST[$field_gid];
+} else {
+ /* Default values */
+ $groupname = "";
+ if (empty($cfg['default_gid'])) {
+ $gid = $ac->get_last_gid();
+ if ($gid == 0 && $cfg['min_gid'] != -1) {
+ $gid = $cfg['min_gid'];
+ } else {
+ $gid = $gid + 1;
+ }
+ } else {
+ $gid = $cfg['default_gid'];
+ }
+ if (!empty($_REQUEST["error"]) && $_REQUEST["error"] == "createLoginWithoutGroup") {
+ $errormsg = "There are no groups in the database; please create at least one group before creating users.";
+ }
+}
+
include ("includes/header.php");
?>
@@ -77,12 +99,12 @@
-
@@ -216,10 +227,17 @@
-
+
Minimum length characters.
+
+
+
+
+
+
+
diff --git a/configs/config_example.php b/configs/config_example.php
index bb6a8cf..c8e265a 100644
--- a/configs/config_example.php
+++ b/configs/config_example.php
@@ -40,10 +40,14 @@
$cfg['field_members'] = "members";
$cfg['default_uid'] = ""; //if empty next incremental will be default
+$cfg['default_gid'] = ""; //if empty next incremental will be default
$cfg['default_homedir'] = "/srv/ftp";
-// Use either SHA1 or MD5 or any other supported by your MySQL-Server and ProFTPd
+$cfg['default_shell'] = "/bin/false";
+// Use either SHA1 or SHA256 or SHA512 or MD5 or any other supported by your MySQL-Server and ProFTPd
// "pbkdf2" is supported if you are using ProFTPd 1.3.5.
// "crypt" uses the unix crypt() function.
+// "MYSQL_Backend" uses the PASSWORD() function from MySQL to hash the password. Useful when migrating from ProFTPd Admin 1.0
+// Emtpy value means cleartext storage
// "OpenSSL:sha1" other digest-names also possible; see: http://www.proftpd.org/docs/directives/configuration_full.html#SQLAUTHTYPES
$cfg['passwd_encryption'] = "SHA1";
$cfg['min_passwd_length'] = "6";
diff --git a/edit_group.php b/edit_group.php
index ff42a4b..c84e215 100644
--- a/edit_group.php
+++ b/edit_group.php
@@ -45,6 +45,9 @@
}
}
+/* find the right message for gid */
+$gidMessage = $ac->get_gid_message();
+
if (empty($errormsg) && !empty($_REQUEST["action"]) && $_REQUEST["action"] == "update") {
$errors = array();
/* gid validation */
@@ -57,14 +60,8 @@
array_push($errors, 'GID already exists; GID must be unique.');
}
/* gid range validation */
- if ($cfg['max_gid'] != -1 && $cfg['min_gid'] != -1) {
- if ($_REQUEST[$field_newgid] > $cfg['max_gid'] || $_REQUEST[$field_newgid] < $cfg['min_gid']) {
- array_push($errors, 'Invalid GID; GID must be between ' . $cfg['min_gid'] . ' and ' . $cfg['max_gid'] . '.');
- }
- } else if ($cfg['max_gid'] != -1 && $_REQUEST[$field_newgid] > $cfg['max_gid']) {
- array_push($errors, 'Invalid GID; GID must be at most ' . $cfg['max_gid'] . '.');
- } else if ($cfg['min_gid'] != -1 && $_REQUEST[$field_newgid] < $cfg['min_gid']) {
- array_push($errors, 'Invalid GID; GID must be at least ' . $cfg['min_gid'] . '.');
+ if (($cfg['max_gid'] != -1 && $_REQUEST[$field_newgid] > $cfg['max_gid']) or ($cfg['min_gid'] != -1 && $_REQUEST[$field_newgid] < $cfg['min_gid'])) {
+ array_push($errors, 'Invalid GID; '.$gidMessage);
}
if (count($errors) == 0) {
/* data validation passed */
@@ -115,7 +112,7 @@
$user = $ac->get_user_by_id($u_id); ?>
-
+
@@ -185,7 +182,7 @@
-
Positive integer.
+
diff --git a/edit_user.php b/edit_user.php
index b18da80..ba06f74 100644
--- a/edit_user.php
+++ b/edit_user.php
@@ -23,6 +23,7 @@
$field_ugid = $cfg['field_ugid'];
$field_ad_gid = 'ad_gid';
$field_passwd = $cfg['field_passwd'];
+$field_passwd2 = $cfg['field_passwd2'];
$field_homedir = $cfg['field_homedir'];
$field_shell = $cfg['field_shell'];
$field_title = $cfg['field_title'];
@@ -45,6 +46,9 @@
die();
}
+/* find the right message for uid */
+$uidMessage = $ac->get_uid_message();
+
$groups = $ac->get_groups();
$id = $_REQUEST[$field_id];
@@ -77,14 +81,8 @@
if (empty($_REQUEST[$field_uid]) || !$ac->is_valid_id($_REQUEST[$field_uid])) {
array_push($errors, 'Invalid UID; must be a positive integer.');
}
- if ($cfg['max_uid'] != -1 && $cfg['min_uid'] != -1) {
- if ($_REQUEST[$field_uid] > $cfg['max_uid'] || $_REQUEST[$field_uid] < $cfg['min_uid']) {
- array_push($errors, 'Invalid UID; UID must be between ' . $cfg['min_uid'] . ' and ' . $cfg['max_uid'] . '.');
- }
- } else if ($cfg['max_uid'] != -1 && $_REQUEST[$field_uid] > $cfg['max_uid']) {
- array_push($errors, 'Invalid UID; UID must be at most ' . $cfg['max_uid'] . '.');
- } else if ($cfg['min_uid'] != -1 && $_REQUEST[$field_uid] < $cfg['min_uid']) {
- array_push($errors, 'Invalid UID; UID must be at least ' . $cfg['min_uid'] . '.');
+ if (($cfg['max_uid'] != -1 && $_REQUEST[$field_uid] > $cfg['max_uid']) or ($cfg['min_uid'] != -1 && $_REQUEST[$field_uid] < $cfg['min_uid'])) {
+ array_push($errors, 'Invalid UID; '.$uidMessage );
}
/* gid validation */
if (empty($_REQUEST[$field_ugid]) || !$ac->is_valid_id($_REQUEST[$field_ugid])) {
@@ -94,6 +92,11 @@
if (strlen($_REQUEST[$field_passwd]) > 0 && strlen($_REQUEST[$field_passwd]) < $cfg['min_passwd_length']) {
array_push($errors, 'Password is too short; minimum length is '.$cfg['min_passwd_length'].' characters.');
}
+ /* password confirmation validation */
+ if ($_REQUEST[$field_passwd] != $_REQUEST[$field_passwd2]) {
+ array_push($errors, 'Passwords are not matching');
+}
+
/* home directory validation */
if (strlen($_REQUEST[$field_homedir]) <= 1) {
array_push($errors, 'Invalid home directory; home directory cannot be empty.');
@@ -288,7 +291,7 @@
-
Positive integer.
+
@@ -317,10 +320,17 @@
-
+
Minimum length characters.
+
+
+
+
+
+
+
diff --git a/groups.php b/groups.php
index fc964ac..753f2be 100644
--- a/groups.php
+++ b/groups.php
@@ -19,6 +19,13 @@
$groups = $ac->get_groups();
+if (!empty($_REQUEST["error"]) && $_REQUEST["error"] == "removeGroup") {
+ $errormsg = 'Group "'.$_REQUEST["groupname"].'" removal failed; see log files for more information.';
+}
+if (!empty($_REQUEST["info"]) && $_REQUEST["info"] == "removeGroup") {
+ $infomsg = 'Group "'.$_REQUEST["groupname"].'" removed successfully.';
+}
+
include ("includes/header.php");
?>
diff --git a/includes/AdminClass.php b/includes/AdminClass.php
index 98b4a4e..8e01b26 100644
--- a/includes/AdminClass.php
+++ b/includes/AdminClass.php
@@ -16,6 +16,9 @@
require "hash_pbkdf2_compat.php";
} elseif ($cfg['passwd_encryption'] == "crypt") {
require "unix_crypt.php";
+} elseif ($cfg['passwd_encryption'] == "Backend") {
+ require "mysql_backend.php";
+
}
include_once "ez_sql_core.php";
@@ -188,6 +191,51 @@ function get_last_uid() {
return $result;
}
+ /**
+ * returns the last index number of the group table
+ * @return Integer
+ */
+ function get_last_gid() {
+ $format = 'SELECT MAX(%s) FROM %s';
+ $query = sprintf($format, $this->config['field_gid'], $this->config['table_groups']);
+ $result = $this->dbConn->get_var($query);
+ return $result;
+ }
+
+ /**
+ * returns the right message for uid
+ * @return String
+ */
+ function get_uid_message() {
+ /* find the right message for uid */
+ $uidMessage = "Positive integer.";
+ if ($this->config['max_uid'] != -1 && $this->config['min_uid'] != -1) {
+ $uidMessage = 'UID must be between ' . $this->config['min_uid'] . ' and ' . $this->config['max_uid'] . '.';
+ } else if ($this->config['max_uid'] != -1) {
+ $uidMessage = 'UID must be at most ' . $this->config['max_uid'] . '.';
+ } else if ($this->config['min_uid'] != -1) {
+ $uidMessage = 'UID must be at least ' . $this->config['min_uid'] . '.';
+ }
+ return $uidMessage;
+ }
+
+ /**
+ * returns the right message for gid
+ * @return String
+ */
+ function get_gid_message() {
+ /* find the right message for gid */
+ $gidMessage = "Positive integer.";
+ if ($this->config['max_gid'] != -1 && $this->config['min_gid'] != -1) {
+ $gidMessage = 'GID must be between ' . $this->config['min_gid'] . ' and ' . $this->config['max_gid'] . '.';
+ } else if ($this->config['max_gid'] != -1) {
+ $gidMessage = 'GID must be at most ' . $this->config['max_gid'] . '.';
+ } else if ($this->config['min_gid'] != -1) {
+ $gidMessage = 'GID must be at least ' . $this->config['min_gid'] . '.';
+ }
+ return $gidMessage;
+ }
+
/**
* Checks if the given groupname is already in the database
* @param String $groupname
@@ -301,9 +349,16 @@ function add_user($userdata) {
} else if ($passwd_encryption == 'crypt') {
$passwd = unix_crypt($userdata[$field_passwd]);
$passwd = '"'.$passwd.'"';
+ } else if ($passwd_encryption == 'Backend') {
+ $passwd = mysql_backend($userdata[$field_passwd]);
+ $passwd = '"'.$passwd.'"';
} else if (strpos($passwd_encryption, "OpenSSL:") === 0) {
$passwd_digest = substr($passwd_encryption, strpos($passwd_encryption, ':')+1);
$passwd = 'CONCAT("{'.$passwd_digest.'}",TO_BASE64(UNHEX('.$passwd_digest.'("'.$userdata[$field_passwd].'"))))';
+ } else if ($passwd_encryption == 'SHA256') {
+ $passwd = "SHA2('$userdata[$field_passwd]', 256)";
+ } else if ($passwd_encryption == 'SHA512') {
+ $passwd = "SHA2('$userdata[$field_passwd]', 512)";
} else {
$passwd = $passwd_encryption.'("'.$userdata[$field_passwd].'")';
}
@@ -587,10 +642,19 @@ function update_user($userdata) {
} else if ($passwd_encryption == 'crypt') {
$passwd = unix_crypt($userdata[$field_passwd]);
$passwd_format = ' %s="%s", ';
+ } else if ($passwd_encryption == 'MYSQL_Backend') {
+ $passwd = mysql_backend($userdata[$field_passwd]);
+ $passwd_format = ' %s="%s", ';
} else if (strpos($passwd_encryption, "OpenSSL:") === 0) {
$passwd_digest = substr($passwd_encryption, strpos($passwd_encryption, ':')+1);
$passwd = 'CONCAT("{'.$passwd_digest.'}",TO_BASE64(UNHEX('.$passwd_digest.'("'.$userdata[$field_passwd].'"))))';
$passwd_format = ' %s=%s, ';
+ } else if ($passwd_encryption == 'SHA256') {
+ $passwd = "SHA2('$userdata[$field_passwd]', 256)";
+ $passwd_format = ' %s=%s, ';
+ } else if ($passwd_encryption == 'SHA512') {
+ $passwd = "SHA2('$userdata[$field_passwd]', 512)";
+ $passwd_format = ' %s=%s, ';
} else {
$passwd = $passwd_encryption.'("'.$userdata[$field_passwd].'")';
$passwd_format = ' %s=%s, ';
@@ -653,5 +717,5 @@ function generate_random_string($length = 6) {
function is_valid_id($id) {
return is_numeric($id) && (int)$id > 0 && $id == round($id);
}
-}
+ }
?>
diff --git a/includes/mysql_backend.php b/includes/mysql_backend.php
new file mode 100644
index 0000000..28c5de9
--- /dev/null
+++ b/includes/mysql_backend.php
@@ -0,0 +1,10 @@
+
+ */
+
+function mysql_backend($password) {
+ return '*'.strtoupper(hash('sha1',pack('H*',hash('sha1', $password))));
+}
diff --git a/migrate_proftpd_admin_1_to_2.sh b/migrate_proftpd_admin_1_to_2.sh
new file mode 100644
index 0000000..e1d4375
--- /dev/null
+++ b/migrate_proftpd_admin_1_to_2.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+mysql_old_user=CHANGE_ME
+mysql_old_password=CHANGE_ME
+mysql_old_db=CHANGE_ME
+mysql_old_host=CHANGE_ME
+mysql_new_user=CHANGE_ME
+mysql_new_password=CHANGE_ME
+mysql_new_db=CHANGE_ME
+mysql_new_host=CHANGE_ME
+tmp_file=/tmp/$mysql_old_db_usertable_$$
+
+#Dump original data to temporary file
+echo "Dumping original data from $mysql_old_db to $tmp_file"
+mysql -u $mysql_old_user -p$mysql_old_password -h $mysql_old_host -D $mysql_old_db -e "select * from usertable INTO OUTFILE '$tmp_file' FIELDS TERMINATED BY ','"
+
+#Read data from temporary file
+while read line ; do
+ userid=$(echo $line | cut -d ',' -f 1)
+ passwd=$(echo $line | cut -d ',' -f 2)
+ homedir=$(echo $line | cut -d ',' -f 3)
+ shell=$(echo $line | cut -d ',' -f 4)
+ uid=$(echo $line | cut -d ',' -f 5)
+ gid=$(echo $line | cut -d ',' -f 6)
+ login_count=$(echo $line | cut -d ',' -f 7)
+ last_login=$(echo $line | cut -d ',' -f 8)
+ disabled=$(echo $line | cut -d ',' -f 11)
+ name=$(echo $line | cut -d ',' -f 12)
+ email=$(echo $line | cut -d ',' -f 13)
+ comment=''
+ title=''
+ company=''
+ bytes_in_used=0
+ bytes_out_used=0
+ files_in_used=0
+ files_out_used=0
+ last_modified=$(date '+%F %T')
+#Write values in destination DB
+ mysql -u $mysql_new_user -p$mysql_new_password -h $mysql_new_host -D $mysql_new_db << EOF
+insert into users values('$uid','$userid','$uid','$gid','$passwd','$homedir','$comment','$disabled','$shell','$email','$name','$title','$company','$bytes_in_used','$bytes_out_used','$files_in_used','$files_out_used','$login_count','$last_login','$last_modified');
+EOF
+done <$tmp_file
+rm $tmp_file
diff --git a/remove_group.php b/remove_group.php
index 74a3e4f..3220ab1 100644
--- a/remove_group.php
+++ b/remove_group.php
@@ -51,36 +51,16 @@
if (empty($errormsg) && !empty($_REQUEST["action"]) && $_REQUEST["action"] == "reallyremove") {
/* data validation passed */
if ($ac->delete_group_by_gid($gid)) {
- $infomsg = 'Group "'.$groupname.'" removed successfully.';
+ header('Location: groups.php?info=removeGroup&groupname='.$groupname);
} else {
- $errormsg = 'Group "'.$groupname.'" removal failed; see log files for more information.';
+ header('Location: groups.php?error=removeGroup&groupname='.$groupname);
}
+ exit();
}
include ("includes/header.php");
-?>
-
+include ("includes/messages.php"); ?>
-
-
-