diff --git a/baseimage/initializeBaseUbuntuImage.sh b/baseimage/initializeBaseUbuntuImage.sh
index 01ba87467..90b18315a 100755
--- a/baseimage/initializeBaseUbuntuImage.sh
+++ b/baseimage/initializeBaseUbuntuImage.sh
@@ -44,8 +44,6 @@ apt-get -y install \
mysql-server-5.7 \
nginx-full \
openssh-server \
- proftpd-basic \
- proftpd-mod-ldap \
pwgen \
resolvconf \
sudo \
diff --git a/scripts/installer.sh b/scripts/installer.sh
index 3e934d270..dbf6c0eea 100755
--- a/scripts/installer.sh
+++ b/scripts/installer.sh
@@ -56,23 +56,6 @@ if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then
rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
fi
-echo "==> installer: updating proftpd"
-while ! command -v proftpd; do
- echo "Install proftpd"
- if ! apt install -y debconf-utils; then
- echo "==> installer: Failed to install debconf-utils. Retry"
- sleep 1
- continue
- fi
- echo "proftpd-basic shared/proftpd/inetd_or_standalone select standalone" | debconf-set-selections
- if ! apt install -y proftpd-basic proftpd-mod-ldap; then
- echo "==> installer: Failed to install proftpd. Retry"
- sleep 1
- continue
- fi
- systemctl stop proftpd
-done
-
echo "==> installer: updating node"
if [[ "$(node --version)" != "v10.15.1" ]]; then
mkdir -p /usr/local/node-10.15.1
diff --git a/setup/start.sh b/setup/start.sh
index 013619a50..ab3e5c4f6 100755
--- a/setup/start.sh
+++ b/setup/start.sh
@@ -148,11 +148,6 @@ if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.servi
fi
systemctl start nginx
-echo "==> Configuring proftpd"
-rm -f /etc/proftpd/proftpd.conf
-cp "${script_dir}/start/proftpd.conf" /etc/proftpd/proftpd.conf
-systemctl restart proftpd
-
# 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 ef954b47f..4734964ad 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,222,443,587,993,4190 -j ACCEPT
+iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,25,80,202,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/setup/start/proftpd.conf b/setup/start/proftpd.conf
deleted file mode 100644
index c0130480e..000000000
--- a/setup/start/proftpd.conf
+++ /dev/null
@@ -1,116 +0,0 @@
-# Includes DSO modules
-Include /etc/proftpd/modules.conf
-
-# Set off to disable IPv6 support which is annoying on IPv4 only boxes.
-UseIPv6 off
-# If set on you can experience a longer connection delay in many cases.
-IdentLookups off
-
-ServerName Cloudron
-ServerType standalone
-DeferWelcome off
-
-MultilineRFC2228 on
-DefaultServer on
-ShowSymlinks on
-
-TimeoutNoTransfer 600
-TimeoutStalled 600
-TimeoutIdle 1200
-
-DisplayLogin welcome.msg
-DisplayChdir .message true
-ListOptions "-l"
-
-DenyFilter \*.*/
-
-# 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.
-# RequireValidShell off
-
-# Port 21 is the standard FTP port.
-Port 0
-
-# To prevent DoS attacks, set the maximum number of child processes
-# to 30. If you need to allow more than 30 concurrent connections
-# at once, simply increase this value. Note that this ONLY works
-# in standalone mode, in inetd mode you should use an inetd server
-# that allows you to limit maximum number of processes per service
-# (such as xinetd)
-MaxInstances 10
-
-# Set the user and group that the server normally runs at.
-User www-data
-Group www-data
-
-# Umask 022 is a good standard umask to prevent new files and dirs
-# (second parm) from being group and world writable.
-Umask 022 022
-# Normally, we want files to be overwriteable.
-AllowOverwrite on
-
-TransferLog /run/proftpd/xferlog
-SystemLog /run/proftpd/proftpd.log
-
-# disable ssh login log
-WtmpLog off
-
-
- QuotaEngine 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
-
-
-
- ControlsEngine off
- ControlsMaxClients 2
- ControlsLog /var/log/proftpd/controls.log
- ControlsInterval 5
- ControlsSocket /var/run/proftpd/proftpd.sock
-
-
-
- AdminControlsEngine off
-
-
-LoadModule mod_ldap.c
-
- # https://forums.proftpd.org/smf/index.php?topic=6368.0
- LDAPServer "ldap://localhost:3002/??sub"
- LDAPUsers "ou=proftpd,dc=cloudron" (username=%u)
-
- LDAPLog /var/log/proftpd/ldap.log
-
-
-
- SFTPEngine on
- Port 222
- SFTPLog /var/log/proftpd/sftp.log
-
- # 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
-
- # Enable compression
- SFTPCompression delayed
-
- RequireValidShell off
-
-
-
- HideNoAccess yes
-
diff --git a/setup/start/sudoers b/setup/start/sudoers
index 84c220a1b..a5d85136a 100644
--- a/setup/start/sudoers
+++ b/setup/start/sudoers
@@ -50,5 +50,3 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartdocker.s
Defaults!/home/yellowtent/box/src/scripts/restartunbound.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartunbound.sh
-Defaults!/home/yellowtent/box/src/scripts/restartproftpd.sh env_keep="HOME BOX_ENV"
-yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartproftpd.sh
diff --git a/src/addons.js b/src/addons.js
index 4f35c6560..a34276d2d 100644
--- a/src/addons.js
+++ b/src/addons.js
@@ -212,9 +212,9 @@ const KNOWN_SERVICES = {
restart: restartUnbound,
defaultMemoryLimit: 0
},
- proftpd: {
- status: statusProftpd,
- restart: restartProftpd,
+ sftp: {
+ status: statusSftp,
+ restart: restartContainer.bind(null, 'sftp'),
defaultMemoryLimit: 0
},
graphite: {
@@ -1726,22 +1726,27 @@ function restartUnbound(callback) {
callback(null);
}
-function statusProftpd(callback) {
+function statusSftp(callback) {
assert.strictEqual(typeof callback, 'function');
- shell.exec('statusProftpd', 'systemctl is-active proftpd', function (error) {
- callback(null, { status: error ? exports.SERVICE_STATUS_STOPPED : exports.SERVICE_STATUS_ACTIVE });
+ docker.inspect('sftp', function (error, container) {
+ if (error && error.reason === DockerError.NOT_FOUND) return callback(new AddonsError(AddonsError.NOT_ACTIVE, error));
+ if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
+
+ docker.memoryUsage('sftp', function (error, result) {
+ if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
+
+ var tmp = {
+ status: container.State.Running ? exports.SERVICE_STATUS_ACTIVE : exports.SERVICE_STATUS_STOPPED,
+ memoryUsed: result.memory_stats.usage,
+ memoryPercent: parseInt(100 * result.memory_stats.usage / result.memory_stats.limit)
+ };
+
+ callback(null, tmp);
+ });
});
}
-function restartProftpd(callback) {
- assert.strictEqual(typeof callback, 'function');
-
- shell.sudo('restartProftpd', [ path.join(__dirname, 'scripts/restartproftpd.sh') ], {}, NOOP_CALLBACK);
-
- callback(null);
-}
-
function statusGraphite(callback) {
assert.strictEqual(typeof callback, 'function');
diff --git a/src/infra_version.js b/src/infra_version.js
index 6090d1cd1..e9ec9f5d8 100644
--- a/src/infra_version.js
+++ b/src/infra_version.js
@@ -20,6 +20,7 @@ exports = module.exports = {
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.0.2@sha256:95e006390ddce7db637e1672eb6f3c257d3c2652747424f529b1dee3cbe6728c' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.0.0@sha256:8a88dd334b62b578530a014ca1a2425a54cb9df1e475f5d3a36806e5cfa22121' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.2.0@sha256:20e4d2508dcf712eb56481067993ae39bf541d793d44f99f6a41d630ad941d9e' },
- 'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.2@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' }
+ 'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.2@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' },
+ 'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:0.0.1@sha256:2e620f62cf2868ee29eafdc574eca012990a21898f77ab47f2f86e5788134f20' }
}
};
diff --git a/src/ldap.js b/src/ldap.js
index ca1b67361..f426e0877 100644
--- a/src/ldap.js
+++ b/src/ldap.js
@@ -18,7 +18,6 @@ var assert = require('assert'),
MailError = mail.MailError,
mailboxdb = require('./mailboxdb.js'),
path = require('path'),
- paths = require('./paths.js'),
safe = require('safetydance'),
users = require('./users.js'),
UsersError = users.UsersError;
@@ -479,12 +478,8 @@ function authenticateUserMailbox(req, res, next) {
});
}
-function authenticateProftpd(req, res, next) {
- debug('proftpd addon auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
-
- var sourceIp = req.connection.ldap.id.split(':')[0];
- if (sourceIp.split('.').length !== 4) return next(new ldap.InsufficientAccessRightsError('Missing source identifier'));
- if (sourceIp !== '127.0.0.1') return next(new ldap.InsufficientAccessRightsError('Source not authorized'));
+function authenticateSftp(req, res, next) {
+ debug('sftp 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()));
@@ -496,18 +491,14 @@ function authenticateProftpd(req, res, next) {
users.verifyWithUsername(parts[0], req.credentials, function (error) {
if (error) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
- debug('proftpd addon auth: success');
+ debug('sftp 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);
-
- var sourceIp = req.connection.ldap.id.split(':')[0];
- if (sourceIp.split('.').length !== 4) return next(new ldap.InsufficientAccessRightsError('Missing source identifier'));
- if (sourceIp !== '127.0.0.1') return next(new ldap.InsufficientAccessRightsError('Source not authorized'));
+function userSearchSftp(req, res, next) {
+ debug('sftp 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()));
@@ -525,7 +516,6 @@ function userSearchProftpd(req, res, next) {
if (typeof app.manifest.addons.localstorage.ftp.uid !== 'string') return next(new ldap.UnavailableError('Bad uid'));
const uidNumber = parseInt(app.manifest.addons.localstorage.ftp.uid.split('/')[0], 10);
-
if (!Number.isInteger(uidNumber)) {
console.error('addon localstorage ftp uid must be an integer', app);
return next(new ldap.UnavailableError('Not supported'));
@@ -539,9 +529,9 @@ function userSearchProftpd(req, res, next) {
if (!hasAccess) return next(new ldap.InsufficientAccessRightsError('Not authorized'));
var obj = {
- dn: ldap.parseDN(`cn=${username}@${appFqdn},ou=proftpd,dc=cloudron`).toString(),
+ dn: ldap.parseDN(`cn=${username}@${appFqdn},ou=sftp,dc=cloudron`).toString(),
attributes: {
- homeDirectory: path.join(paths.APPS_DATA_DIR, app.id, 'data'),
+ homeDirectory: path.join('/app/data', app.id, 'data'),
objectclass: ['user'],
objectcategory: 'person',
cn: user.id,
@@ -631,8 +621,8 @@ 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.bind('ou=sftp,dc=cloudron', authenticateSftp); // sftp
+ gServer.search('ou=sftp,dc=cloudron', userSearchSftp);
gServer.compare('cn=users,ou=groups,dc=cloudron', authenticateApp, groupUsersCompare);
gServer.compare('cn=admins,ou=groups,dc=cloudron', authenticateApp, groupAdminsCompare);
diff --git a/src/platform.js b/src/platform.js
index 60ced0cb0..7fcd447a1 100644
--- a/src/platform.js
+++ b/src/platform.js
@@ -21,6 +21,7 @@ var addons = require('./addons.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
+ sftp = require('./sftp.js'),
shell = require('./shell.js'),
taskmanager = require('./taskmanager.js'),
_ = require('underscore');
@@ -59,6 +60,7 @@ function start(callback) {
// mark app state before we start addons. this gives the db import logic a chance to mark an app as errored
startApps.bind(null, existingInfra),
graphs.startGraphite.bind(null, existingInfra),
+ sftp.startSftp.bind(null, existingInfra),
addons.startServices.bind(null, existingInfra),
fs.writeFile.bind(fs, paths.INFRA_VERSION_FILE, JSON.stringify(infra, null, 4))
], function (error) {
@@ -136,7 +138,7 @@ function stopContainers(existingInfra, callback) {
} else {
assert(typeof infra.images, 'object');
var changedAddons = [ ];
- for (var imageName in infra.images) {
+ for (var imageName in existingInfra.images) { // do not use infra.images because we can only stop things which are existing
if (infra.images[imageName].tag !== existingInfra.images[imageName].tag) changedAddons.push(imageName);
}
diff --git a/src/scripts/restartproftpd.sh b/src/scripts/restartproftpd.sh
deleted file mode 100755
index b343cbdc5..000000000
--- a/src/scripts/restartproftpd.sh
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/bin/bash
-
-set -eu -o pipefail
-
-if [[ ${EUID} -ne 0 ]]; then
- echo "This script should be run as root." > /dev/stderr
- exit 1
-fi
-
-if [[ $# == 1 && "$1" == "--check" ]]; then
- echo "OK"
- exit 0
-fi
-
-if [[ "${BOX_ENV}" == "cloudron" ]]; then
- systemctl restart proftpd
-fi
-
diff --git a/src/sftp.js b/src/sftp.js
new file mode 100644
index 000000000..2913d5e29
--- /dev/null
+++ b/src/sftp.js
@@ -0,0 +1,39 @@
+'use strict';
+
+exports = module.exports = {
+ startSftp: startSftp
+};
+
+var assert = require('assert'),
+ infra = require('./infra_version.js'),
+ paths = require('./paths.js'),
+ shell = require('./shell.js');
+
+function startSftp(existingInfra, callback) {
+ assert.strictEqual(typeof existingInfra, 'object');
+ assert.strictEqual(typeof callback, 'function');
+
+ const tag = infra.images.sftp.tag;
+ const memoryLimit = 256;
+
+ if (existingInfra.version === infra.version && infra.images.graphite.tag === existingInfra.images.graphite.tag) return callback();
+
+ const cmd = `docker run --restart=always -d --name="sftp" \
+ --net cloudron \
+ --net-alias sftp \
+ --log-driver syslog \
+ --log-opt syslog-address=udp://127.0.0.1:2514 \
+ --log-opt syslog-format=rfc5424 \
+ --log-opt tag=sftp \
+ -m ${memoryLimit}m \
+ --memory-swap ${memoryLimit * 2}m \
+ --dns 172.18.0.1 \
+ --dns-search=. \
+ -p 222:22 \
+ -v "${paths.APPS_DATA_DIR}:/app/data" \
+ -v "/etc/ssh:/etc/ssh:ro" \
+ --label isCloudronManaged=true \
+ --read-only -v /tmp -v /run "${tag}"`;
+
+ shell.exec('startSftp', cmd, callback);
+}
diff --git a/src/test/checkInstall b/src/test/checkInstall
index 4ea11f0b0..5518de3d7 100755
--- a/src/test/checkInstall
+++ b/src/test/checkInstall
@@ -18,7 +18,6 @@ scripts=("${SOURCE_DIR}/src/scripts/clearvolume.sh" \
"${SOURCE_DIR}/src/scripts/restart.sh" \
"${SOURCE_DIR}/src/scripts/restartdocker.sh" \
"${SOURCE_DIR}/src/scripts/restartunbound.sh" \
- "${SOURCE_DIR}/src/scripts/restartproftpd.sh" \
"${SOURCE_DIR}/src/scripts/update.sh" \
"${SOURCE_DIR}/src/scripts/collectlogs.sh" \
"${SOURCE_DIR}/src/scripts/configurecollectd.sh" \