2019-04-04 20:46:01 -07:00
'use strict' ;
exports = module . exports = {
2021-01-21 12:53:38 -08:00
start ,
DEFAULT _MEMORY _LIMIT : 256 * 1024 * 1024
2019-04-04 20:46:01 -07:00
} ;
2021-05-11 17:50:48 -07:00
const apps = require ( './apps.js' ) ,
2025-08-14 11:17:38 +05:30
assert = require ( 'node:assert' ) ,
2021-11-16 21:50:09 -08:00
blobs = require ( './blobs.js' ) ,
2021-06-24 16:19:30 -07:00
BoxError = require ( './boxerror.js' ) ,
2020-07-24 14:44:41 +02:00
debug = require ( 'debug' ) ( 'box:sftp' ) ,
2021-10-19 15:51:22 -07:00
docker = require ( './docker.js' ) ,
2020-10-19 17:26:20 -07:00
hat = require ( './hat.js' ) ,
2019-04-04 20:46:01 -07:00
infra = require ( './infra_version.js' ) ,
2024-08-08 14:12:06 +02:00
mounts = require ( './mounts.js' ) ,
2025-08-14 11:17:38 +05:30
path = require ( 'node:path' ) ,
2020-11-26 11:45:43 -08:00
paths = require ( './paths.js' ) ,
2020-08-09 12:10:20 -07:00
safe = require ( 'safetydance' ) ,
2021-10-19 15:51:22 -07:00
services = require ( './services.js' ) ,
2024-10-14 19:10:31 +02:00
shell = require ( './shell.js' ) ( 'sftp' ) ,
2021-06-24 16:19:30 -07:00
volumes = require ( './volumes.js' ) ;
2019-04-04 20:46:01 -07:00
2021-11-16 21:50:09 -08:00
async function ensureKeys ( ) {
2023-03-09 10:55:14 +01:00
for ( const keyType of [ 'rsa' , 'ed25519' ] ) {
const privateKey = await blobs . get ( ` sftp_ ${ keyType } _private_key ` ) ;
const publicKey = await blobs . get ( ` sftp_ ${ keyType } _public_key ` ) ;
const publicKeyFile = path . join ( paths . SFTP _KEYS _DIR , ` ssh_host_ ${ keyType } _key.pub ` ) ;
const privateKeyFile = path . join ( paths . SFTP _KEYS _DIR , ` ssh_host_ ${ keyType } _key ` ) ;
if ( ! privateKey || ! publicKey ) {
debug ( ` ensureSecrets: generating new sftp keys of type ${ keyType } ` ) ;
2023-04-27 20:03:41 +02:00
safe . fs . unlinkSync ( publicKeyFile ) ;
safe . fs . unlinkSync ( privateKeyFile ) ;
2024-10-16 10:25:07 +02:00
const [ error ] = await safe ( shell . spawn ( 'ssh-keygen' , [ '-m' , 'PEM' , '-t' , keyType , '-f' , ` ${ paths . SFTP _KEYS _DIR } /ssh_host_ ${ keyType } _key ` , '-q' , '-N' , '' ] , { } ) ) ;
2024-02-20 22:57:36 +01:00
if ( error ) throw new BoxError ( BoxError . OPENSSL _ERROR , ` Could not generate sftp ${ keyType } keys: ${ error . message } ` ) ;
2023-03-09 10:55:14 +01:00
const newPublicKey = safe . fs . readFileSync ( publicKeyFile ) ;
await blobs . set ( ` sftp_ ${ keyType } _public_key ` , newPublicKey ) ;
const newPrivateKey = safe . fs . readFileSync ( privateKeyFile ) ;
await blobs . set ( ` sftp_ ${ keyType } _private_key ` , newPrivateKey ) ;
} else {
if ( ! safe . fs . writeFileSync ( publicKeyFile , publicKey ) ) throw new BoxError ( BoxError . FS _ERROR , ` Could not save sftp public ${ keyType } key: ${ safe . error . message } ` ) ;
if ( ! safe . fs . writeFileSync ( privateKeyFile , privateKey , { mode : 0o600 } ) ) throw new BoxError ( BoxError . FS _ERROR , ` Could not save sftp private ${ keyType } key: ${ safe . error . message } ` ) ;
}
2021-11-16 21:50:09 -08:00
}
}
2021-09-26 22:48:14 -07:00
async function start ( existingInfra ) {
assert . strictEqual ( typeof existingInfra , 'object' ) ;
2020-07-24 14:44:41 +02:00
2021-09-26 22:48:14 -07:00
debug ( 'start: re-creating container' ) ;
2020-07-24 14:44:41 +02:00
2023-08-03 12:52:47 +05:30
const serviceConfig = await services . getServiceConfig ( 'sftp' ) ;
2023-08-08 10:42:16 +05:30
const image = infra . images . sftp ;
2021-01-21 12:53:38 -08:00
const memoryLimit = serviceConfig . memoryLimit || exports . DEFAULT _MEMORY _LIMIT ;
2020-10-19 17:26:20 -07:00
const cloudronToken = hat ( 8 * 128 ) ;
2019-04-04 20:46:01 -07:00
2021-11-16 21:50:09 -08:00
await ensureKeys ( ) ;
2021-09-26 18:36:33 -07:00
const resolvedAppDataDir = safe . fs . realpathSync ( paths . APPS _DATA _DIR ) ;
if ( ! resolvedAppDataDir ) throw new BoxError ( BoxError . FS _ERROR , ` Could not resolve apps data dir: ${ safe . error . message } ` ) ;
2021-06-24 16:19:30 -07:00
2021-09-25 17:07:07 -07:00
const dataDirs = [ { hostDir : resolvedAppDataDir , mountDir : '/mnt/appsdata' } ] ;
2021-08-25 19:41:46 -07:00
2021-09-25 17:07:07 -07:00
// custom app data directories
const allApps = await apps . list ( ) ;
for ( const app of allApps ) {
2023-11-13 21:58:44 +01:00
if ( ! app . manifest . addons ? . localstorage || ! app . storageVolumeId ) continue ;
2021-08-25 19:41:46 -07:00
2022-06-01 22:44:52 -07:00
const hostDir = await apps . getStorageDir ( app ) , mountDir = ` /mnt/app- ${ app . id } ` ; // see also sftp:userSearchSftp
2024-06-06 15:22:33 +02:00
if ( hostDir === null || ! safe . fs . existsSync ( hostDir ) ) { // this can fail if external mount does not have permissions for yellowtent user
2021-08-25 19:41:46 -07:00
// do not create host path when cloudron is restoring. this will then create dir with root perms making restore logic fail
debug ( ` Ignoring app data dir ${ hostDir } for ${ app . id } since it does not exist ` ) ;
2021-09-25 17:07:07 -07:00
continue ;
2021-08-25 19:41:46 -07:00
}
dataDirs . push ( { hostDir , mountDir } ) ;
2021-09-25 17:07:07 -07:00
}
2021-08-25 19:41:46 -07:00
2021-09-25 17:07:07 -07:00
// volume directories
2021-12-23 22:35:20 -08:00
dataDirs . push ( { hostDir : '/mnt/volumes' , mountDir : '/mnt/volumes' } ) ; // managed volumes
2021-09-25 17:07:07 -07:00
const allVolumes = await volumes . list ( ) ;
for ( const volume of allVolumes ) {
2024-08-08 14:12:06 +02:00
if ( mounts . isManagedProvider ( volume . mountType ) ) continue ; // skip managed volume. these are acessed via /mnt/volumes mount above
2021-08-25 19:41:46 -07:00
if ( ! safe . fs . existsSync ( volume . hostPath ) ) {
debug ( ` Ignoring volume host path ${ volume . hostPath } since it does not exist ` ) ;
2021-09-25 17:07:07 -07:00
continue ;
2021-08-25 19:41:46 -07:00
}
2021-12-24 10:43:39 -08:00
dataDirs . push ( { hostDir : volume . hostPath , mountDir : ` /mnt/volume- ${ volume . id } ` } ) ;
2021-09-25 17:07:07 -07:00
}
2021-08-25 19:41:46 -07:00
2021-09-25 17:19:58 -07:00
// mail data dir
2021-09-26 18:36:33 -07:00
const resolvedMailDataDir = safe . fs . realpathSync ( paths . MAIL _DATA _DIR ) ;
if ( ! resolvedMailDataDir ) throw new BoxError ( BoxError . FS _ERROR , ` Could not resolve mail data dir: ${ safe . error . message } ` ) ;
dataDirs . push ( { hostDir : resolvedMailDataDir , mountDir : '/mnt/maildata' } ) ;
2021-09-25 17:19:58 -07:00
2021-10-01 12:09:13 -07:00
const readOnly = ! serviceConfig . recoveryMode ? '--read-only' : '' ;
const cmd = serviceConfig . recoveryMode ? '/bin/bash -c \'echo "Debug mode. Sleeping" && sleep infinity\'' : '' ;
2024-08-08 14:12:06 +02:00
const volumeMounts = dataDirs . map ( v => ` -v " ${ v . hostDir } : ${ v . mountDir } " ` ) . join ( ' ' ) ;
2025-06-14 17:51:35 +02:00
const runCmd = ` docker run --restart=unless-stopped -d --name=sftp \
2021-08-25 19:41:46 -07:00
-- hostname sftp \
-- net cloudron \
-- net - alias sftp \
-- log - driver syslog \
2024-03-21 17:30:50 +01:00
-- log - opt syslog - address = unix : //${paths.SYSLOG_SOCKET_FILE} \
2021-08-25 19:41:46 -07:00
-- log - opt syslog - format = rfc5424 \
-- log - opt tag = sftp \
2024-04-09 18:59:40 +02:00
- m $ { memoryLimit } \
-- memory - swap - 1 \
2021-08-25 19:41:46 -07:00
- p 222 : 22 \
2024-08-08 14:12:06 +02:00
$ { volumeMounts } \
2024-02-22 10:34:56 +01:00
- e CLOUDRON _SFTP _TOKEN = $ { cloudronToken } \
- v $ { paths . SFTP _KEYS _DIR } : / e t c / s s h : r o \
2021-08-25 19:41:46 -07:00
-- label isCloudronManaged = true \
2024-02-22 10:34:56 +01:00
$ { readOnly } - v / tmp - v / run $ { image } $ { cmd } ` ;
2021-08-25 19:41:46 -07:00
2024-02-28 18:47:53 +01:00
debug ( 'startSftp: stopping and deleting previous sftp container' ) ;
await docker . stopContainer ( 'sftp' ) ;
await docker . deleteContainer ( 'sftp' ) ;
debug ( 'startSftp: starting sftp container' ) ;
2024-12-19 16:59:28 +01:00
await shell . bash ( runCmd , { encoding : 'utf8' } ) ;
2024-12-14 20:47:35 +01:00
if ( existingInfra . version !== 'none' && existingInfra . images . sftp !== image ) await docker . deleteImage ( existingInfra . images . sftp ) ;
2019-04-04 20:46:01 -07:00
}