diff --git a/setup/start.sh b/setup/start.sh
index ac2999fa5..0e44d7ac4 100755
--- a/setup/start.sh
+++ b/setup/start.sh
@@ -148,6 +148,11 @@ if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.servi
fi
systemctl start nginx
+echo "==> Configuring proftpd"
+# link nginx config to system config
+unlink /etc/proftpd/proftpd.conf 2>/dev/null || rm -rf /etc/proftpd/proftpd.conf
+ln -s "${PLATFORM_DATA_DIR}/proftpd.conf" /etc/proftpd/proftpd.conf
+
# restart mysql to make sure it has latest config
if [[ ! -f /etc/mysql/mysql.cnf ]] || ! diff -q "${script_dir}/start/mysql.cnf" /etc/mysql/mysql.cnf >/dev/null; then
# wait for all running mysql jobs
diff --git a/setup/start/cloudron-firewall.sh b/setup/start/cloudron-firewall.sh
index 4734964ad..ef954b47f 100755
--- a/setup/start/cloudron-firewall.sh
+++ b/setup/start/cloudron-firewall.sh
@@ -10,7 +10,7 @@ iptables -t filter -F CLOUDRON # empty any existing rules
# allow ssh, http, https, ping, dns
iptables -t filter -I CLOUDRON -m state --state RELATED,ESTABLISHED -j ACCEPT
# caas has ssh on port 202
-iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,25,80,202,443,587,993,4190 -j ACCEPT
+iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,25,80,202,222,443,587,993,4190 -j ACCEPT
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-request -j ACCEPT
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-reply -j ACCEPT
diff --git a/src/infra_version.js b/src/infra_version.js
index 567efaf6a..6090d1cd1 100644
--- a/src/infra_version.js
+++ b/src/infra_version.js
@@ -6,7 +6,7 @@
exports = module.exports = {
// a version change recreates all containers with latest docker config
- 'version': '48.12.1',
+ 'version': '48.14.0',
'baseImages': [
{ repo: 'cloudron/base', tag: 'cloudron/base:1.0.0@sha256:147a648a068a2e746644746bbfb42eb7a50d682437cead3c67c933c546357617' }
diff --git a/src/ldap.js b/src/ldap.js
index c2626585d..7e4634f2a 100644
--- a/src/ldap.js
+++ b/src/ldap.js
@@ -19,6 +19,8 @@ var assert = require('assert'),
mail = require('./mail.js'),
MailError = mail.MailError,
mailboxdb = require('./mailboxdb.js'),
+ path = require('path'),
+ paths = require('./paths.js'),
safe = require('safetydance');
var gServer = null;
@@ -481,6 +483,74 @@ function authenticateUserMailbox(req, res, next) {
});
}
+function authenticateProftpd(req, res, next) {
+ debug('proftpd addon auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
+
+ if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
+
+ var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
+ var parts = email.split('@');
+ if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
+
+ var username = parts[0];
+ var appDomain = parts[1];
+
+ // TODO sync this with platform.js proftpd config generation
+ if (username === 'admin' && appDomain === 'cloudron') {
+ if (req.credentials !== 'password') return next(new ldap.InvalidCredentialsError(req.dn.toString()));
+ return res.end();
+ }
+
+ // actual user bind
+ users.verifyWithUsername(username, req.credentials, function (error) {
+ if (error) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
+
+ debug('proftpd addon auth: success');
+
+ res.end();
+ });
+}
+
+function userSearchProftpd(req, res, next) {
+ debug('proftpd user search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
+
+ if (req.filter.attribute !== 'username' || !req.filter.value) return next(new ldap.NoSuchObjectError(req.dn.toString()));
+
+ var parts = req.filter.value.split('@');
+ if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
+
+ var username = parts[0];
+ var appDomain = parts[1];
+
+ apps.getAll(function (error, result) {
+ if (error) return next(new ldap.OperationsError(error.toString()));
+
+ var app = result.find(function (a) { return a.fqdn === appDomain; });
+ if (!app) return next(new ldap.NoSuchObjectError(req.dn.toString()));
+
+ users.getByUsername(username, function (error, result) {
+ if (error) return next(new ldap.OperationsError(error.toString()));
+
+ var dn = ldap.parseDN(`cn=${username}@${appDomain},ou=proftpd,dc=cloudron`);
+
+ var obj = {
+ dn: dn.toString(),
+ attributes: {
+ homeDirectory: path.join(paths.APPS_DATA_DIR, app.id, 'data'),
+ objectclass: ['user'],
+ objectcategory: 'person',
+ cn: result.id,
+ uid: `${result.username}@${appDomain}`, // for bind after search
+ uidNumber: 1000, // unix uid for ftp access
+ gidNumber: 1000 // unix gid for ftp access
+ }
+ };
+
+ finalSend([ obj ], req, res, next);
+ });
+ });
+}
+
function authenticateMailAddon(req, res, next) {
debug('mail addon auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
@@ -555,6 +625,9 @@ function start(callback) {
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailAddon); // dovecot
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailAddon); // haraka
+ gServer.bind('ou=proftpd,dc=cloudron', authenticateProftpd); // proftdp
+ gServer.search('ou=proftpd,dc=cloudron', userSearchProftpd);
+
gServer.compare('cn=users,ou=groups,dc=cloudron', authenticateApp, groupUsersCompare);
gServer.compare('cn=admins,ou=groups,dc=cloudron', authenticateApp, groupAdminsCompare);
diff --git a/src/paths.js b/src/paths.js
index 6384ddf31..0c04ac4e5 100644
--- a/src/paths.js
+++ b/src/paths.js
@@ -25,6 +25,7 @@ exports = module.exports = {
UPDATE_DIR: path.join(config.baseDir(), 'platformdata/update'),
SNAPSHOT_INFO_FILE: path.join(config.baseDir(), 'platformdata/backup/snapshot-info.json'),
DYNDNS_INFO_FILE: path.join(config.baseDir(), 'platformdata/dyndns-info.json'),
+ PROFTPD_CONFIG_FILE: path.join(config.baseDir(), 'platformdata/proftpd.conf'),
// this is not part of appdata because an icon may be set before install
APP_ICONS_DIR: path.join(config.baseDir(), 'boxdata/appicons'),
diff --git a/src/platform.js b/src/platform.js
index 09215828e..bc7ce1159 100644
--- a/src/platform.js
+++ b/src/platform.js
@@ -12,13 +12,12 @@ var addons = require('./addons.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
- config = require('./config.js'),
debug = require('debug')('box:platform'),
+ ejs = require('ejs'),
fs = require('fs'),
graphs = require('./graphs.js'),
infra = require('./infra_version.js'),
locker = require('./locker.js'),
- mail = require('./mail.js'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
@@ -29,6 +28,8 @@ var addons = require('./addons.js'),
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
+var PROFTPD_CONFIG_EJS = fs.readFileSync(__dirname + '/proftpd.ejs', { encoding: 'utf8' });
+
function start(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -62,6 +63,7 @@ function start(callback) {
startApps.bind(null, existingInfra),
graphs.startGraphite.bind(null, existingInfra),
addons.startServices.bind(null, existingInfra),
+ configureProftpd.bind(null, existingInfra),
fs.writeFile.bind(fs, paths.INFRA_VERSION_FILE, JSON.stringify(infra, null, 4))
], function (error) {
if (error) return callback(error);
@@ -165,3 +167,19 @@ function startApps(existingInfra, callback) {
callback();
}
}
+
+function configureProftpd(existingInfra, callback) {
+ var data = {
+ sftpPort: 222,
+ ldapUrl: 'ldap://172.18.0.1:3002',
+ ldapUsersBaseDn: 'ou=proftpd,dc=cloudron',
+ ldapBindUsername: 'cn=admin@cloudron,ou=proftpd,dc=cloudron',
+ ldapBindPassword: 'password'
+ };
+
+ console.log('------ configure proftpd', data, paths.PROFTPD_CONFIG_FILE);
+
+ if (!safe.fs.writeFileSync(paths.PROFTPD_CONFIG_FILE, ejs.render(PROFTPD_CONFIG_EJS, data))) return callback(safe.error);
+
+ callback(null);
+}
diff --git a/setup/start/proftpd.conf b/src/proftpd.ejs
similarity index 63%
rename from setup/start/proftpd.conf
rename to src/proftpd.ejs
index f75bd1cba..55e3d383c 100644
--- a/setup/start/proftpd.conf
+++ b/src/proftpd.ejs
@@ -6,7 +6,7 @@ UseIPv6 off
# If set on you can experience a longer connection delay in many cases.
IdentLookups off
-ServerName "##SERVER_NAME"
+ServerName Cloudron
ServerType standalone
DeferWelcome off
@@ -24,8 +24,8 @@ ListOptions "-l"
DenyFilter \*.*/
-# Use this to jail all users in their homes
-# DefaultRoot ~
+# Use this to jail all users in their homes, will be homeDirectory LDAP attribute
+DefaultRoot ~
# Users require a valid shell listed in /etc/shells to login.
# Use this directive to release that constrain.
@@ -59,26 +59,26 @@ SystemLog /run/proftpd/proftpd.log
WtmpLog off
-QuotaEngine off
+ QuotaEngine off
-Ratios off
+ Ratios off
# Delay engine reduces impact of the so-called Timing Attack described in
# http://www.securityfocus.com/bid/11430/discuss
# It is on by default.
-DelayEngine on
+ DelayEngine on
-ControlsEngine off
-ControlsMaxClients 2
-ControlsLog /var/log/proftpd/controls.log
-ControlsInterval 5
-ControlsSocket /var/run/proftpd/proftpd.sock
+ ControlsEngine off
+ ControlsMaxClients 2
+ ControlsLog /var/log/proftpd/controls.log
+ ControlsInterval 5
+ ControlsSocket /var/run/proftpd/proftpd.sock
@@ -87,43 +87,31 @@ ControlsSocket /var/run/proftpd/proftpd.sock
LoadModule mod_ldap.c
-# https://forums.proftpd.org/smf/index.php?topic=6368.0
-LDAPServer "##LDAP_URL/??sub"
-LDAPBindDN "##LDAP_BIND_DN" "##LDAP_BIND_PASSWORD"
-LDAPUsers "##LDAP_USERS_BASE_DN" (username=%u)
+ # https://forums.proftpd.org/smf/index.php?topic=6368.0
+ LDAPServer "<%= ldapUrl %>/??sub"
+ LDAPBindDN "<%= ldapBindUsername %>" "<%= ldapBindPassword %>"
+ LDAPUsers "<%= ldapUsersBaseDn %>" (username=%u)
-LDAPForceDefaultUID on
-LDAPDefaultUID ##LDAP_UID
-LDAPForceDefaultGID on
-LDAPDefaultGID ##LDAP_GID
-
-LDAPForceGeneratedHomedir on
-LDAPGenerateHomedir on
-LDAPGenerateHomedirPrefix /app/data
-LDAPGenerateHomedirPrefixNoUsername on
-
-#LDAPUseTLS off
-#LDAPLog /run/proftpd/ldap.log
+ LDAPLog /var/log/proftpd/ldap.log
-SFTPEngine on
-Port ##SFTP_PORT
-SFTPLog /run/proftpd/sftp.log
+ SFTPEngine on
+ Port <%= sftpPort %>
+ SFTPLog /var/log/proftpd/sftp.log
-# Configure both the RSA and DSA host keys, using the same host key
-# files that OpenSSH uses.
-SFTPHostKey /app/data/.sftpd/ssh_host_rsa_key
-SFTPHostKey /app/data/.sftpd/ssh_host_dsa_key
+ # Configure both the RSA and DSA host keys, using the same host key
+ # files that OpenSSH uses.
+ SFTPHostKey /etc/ssh/ssh_host_rsa_key
-SFTPAuthMethods password
+ SFTPAuthMethods password
-# Enable compression
-SFTPCompression delayed
+ # Enable compression
+ SFTPCompression delayed
-RequireValidShell off
+ RequireValidShell off
- HideNoAccess yes
+ HideNoAccess yes
diff --git a/src/users.js b/src/users.js
index 3e9a297c5..78cc351f0 100644
--- a/src/users.js
+++ b/src/users.js
@@ -16,6 +16,7 @@ exports = module.exports = {
remove: removeUser,
get: get,
getByResetToken: getByResetToken,
+ getByUsername: getByUsername,
getAllAdmins: getAllAdmins,
resetPasswordByIdentifier: resetPasswordByIdentifier,
setPassword: setPassword,
@@ -385,6 +386,18 @@ function getByResetToken(email, resetToken, callback) {
});
}
+function getByUsername(username, callback) {
+ assert.strictEqual(typeof username, 'string');
+ assert.strictEqual(typeof callback, 'function');
+
+ userdb.getByUsername(username.toLowerCase(), function (error, result) {
+ if (error && error.reason == DatabaseError.NOT_FOUND) return callback(new UsersError(UsersError.NOT_FOUND));
+ if (error) return callback(new UsersError(UsersError.INTERNAL_ERROR, error));
+
+ get(result.id, callback);
+ });
+}
+
function updateUser(userId, data, auditSource, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof data, 'object');