Browse Source

Fixed my issue kinda...

main
Atridad Lahutton 1 month ago
parent
commit
4da163f70f
  1. 119
      .gitignore
  2. 8
      .jshintrc
  3. 20
      LICENSE
  4. 72
      README.md
  5. 267
      bin/cloudron
  6. 83
      bin/cloudron-appstore
  7. 68
      bin/cloudron-backup
  8. 51
      bin/cloudron-env
  9. BIN
      logo.png
  10. 4424
      package-lock.json
  11. 53
      package.json
  12. 1720
      src/actions.js
  13. 543
      src/appstore-actions.js
  14. 285
      src/backup-tools.js
  15. 355
      src/build-actions.js
  16. 44
      src/completion.js
  17. 95
      src/config.js
  18. 45
      src/helper.js
  19. 9
      src/templates/CloudronManifest.json.ejs
  20. 3
      src/templates/Dockerfile.ejs
  21. 5
      src/templates/dockerignore.ejs
  22. 294
      test/cloudron-test.js
  23. 1
      test/mocha.opts

119
.gitignore

@ -1,120 +1 @@
# ---> Node
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
.env.production
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

8
.jshintrc

@ -0,0 +1,8 @@
{
"node": true,
"browser": true,
"unused": true,
"globalstrict": true,
"esversion": 8,
"predef": [ "angular", "$" ]
}

20
LICENSE

@ -0,0 +1,20 @@
Copyright (c) 2015-2018 Cloudron UG
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

72
README.md

@ -1,2 +1,72 @@
# cloudron-cli-for-ci
# The Cloudron CLI tool
The [Cloudron](https://cloudron.io) CLI tool allows you to install, configure and test apps on your Cloudron.
It is also used to submit your app to the Cloudron Store. The `machine` subcommand can be used for
various maintenance tasks on a selfhosted Cloudron.
Read the Cloudron.io [documentation](https://cloudron.io/documentation.html) for in-depth information.
## Installation
Installing the CLI tool requires [node.js](https://nodejs.org/) and
[npm](https://www.npmjs.com/). The CLI tool can be installed using the
following command:
```
npm install -g cloudron
```
Depending on your setup, you may need to run this as root.
You should now be able to run the `cloudron help` command in a shell.
## Subcommands
```
completion Shows completion for you shell
backup create [options] Create app backup
backup list [options] List app backups
build [options] Build an app
clone [options] Clone an existing app to a new location
createOAuthAppCredentials [options] Create oauth app credentials for local development
exec [options] [cmd...] Exec a command in application
inspect [options] Inspect a Cloudron returning raw JSON
init Creates a new CloudronManifest.json and Dockerfile
install [options] Install or update app into cloudron
list List installed applications
login [options] [cloudron] Login to cloudron
logout Logout off cloudron
logs [options] Application logs
machine Cloudron instance tooling
open Open the app in the Browser
published [options] List published apps
pull [options] <remote> <local> pull remote file/dir. Use trailing slash to indicate remote directory.
push [options] <local> <remote> push local file
restore [options] Restore app from last known backup
restart [options] Restart the installed application
status [options] Application info
submit Submit app to the store for review
upload [options] Upload app to the store for testing
versions [options] List published versions
uninstall [options] Uninstall app from cloudron
unpublish [options] Unpublish app or app version from the store
```
## Tab completion
To add tab completion to your shell, the cloudron tool can generate it on the fly for the shell you are using. Currently tested on `bash` and `zsh`.
Just run the following in your shell
```
. <(cloudron completion)
```
This command loads the completions into your current shell. Adding it to your ~/.bashrc or ~/.zshrc will make the completions available everywhere.
## Tests
The tests can run against a Cloudron as follows:
```
CLOUDRON=<domain> USERNAME=<username> PASSWORD=<password> mocha tests/
```

267
bin/cloudron

@ -0,0 +1,267 @@
#!/usr/bin/env node
'use strict';
require('supererror');
require('colors');
const actions = require('../src/actions.js'),
buildActions = require('../src/build-actions.js'),
completion = require('../src/completion.js'),
config = require('../src/config.js'),
program = require('commander'),
semver = require('semver'),
util = require('util');
const version = require('../package.json').version;
// ensure node version
if (!semver.satisfies(process.version, require('../package.json').engines.node)) {
console.error('Your nodejs version is not compatible. Please installe nodejs', require('../package.json').engines.node);
process.exit(1);
}
// completion is useful in shell configs, so don't block here
if (process.argv[2] !== 'completion') {
if (Date.now() - (config.get('lastCliUpdateCheck') || 0) > 24*60*60*1000) {
// check if cli tool is up-to-date
var res = require('superagent-sync').get('https://registry.npmjs.org/cloudron').retry(0).end();
if (res.statusCode === 200 && res.body['dist-tags'].latest !== version) {
var updateCommand = 'npm install -g cloudron@' + res.body['dist-tags'].latest;
process.stderr.write(util.format('A new version of Cloudron CLI is available. Please update with: %s\n'.yellow.bold, updateCommand.white));
}
config.set('lastCliUpdateCheck', Date.now());
}
}
function collectArgs(value, collected) {
collected.push(value);
return collected;
}
program.version(version)
.option('--server <server>', 'Cloudron domain')
.option('--token <token>', 'Cloudron token')
.option('--allow-selfsigned', 'Accept self signed SSL certificate')
.option('--accept-selfsigned', 'Accept self signed SSL certificate');
program.command('appstore', 'Cloudron appstore commands');
program.command('backup', 'App backup commands');
program.command('completion')
.description('Shows completion for your shell')
.action(completion);
program.command('build')
.description('Build an app')
.option('--build-arg <namevalue>', 'Build arg passed to docker. Can be used multiple times', collectArgs, [])
.option('--build-service-token <token>', 'Build service token')
.option('-f, --file <dockerfile>', 'Name of the Dockerfile')
.option('--set-repository [repository url]', 'Change the repository')
.option('--set-build-service [buildservice url]', 'Set build service app URL')
.option('--local', 'Build docker images locally')
.option('--no-cache', 'Do not use cache')
.option('--no-push', 'Do not push built image to registry')
.option('--raw', 'Raw output build log')
.option('--tag <docker image tag>', 'Docker image tag. Note that this does not include the repository name')
.action(buildActions.build);
program.command('cancel')
.description('Cancels any active or pending app task')
.option('--app <id/location>', 'App id or location')
.action(actions.cancel);
program.command('clone')
.description('Clone an existing app to a new location')
.option('--app <id/location>', 'App id or location')
.option('--backup <backup>', 'Backup id or "latest"')
.option('--location <domain>', 'Subdomain or full domain')
.action(actions.clone);
program.command('configure')
.description('Change location of an app')
.option('--app <id/location>', 'App id or location')
.option('--no-wait', 'Wait for healthcheck to succeed [false]', false)
.option('-p, --port-bindings [PORT=port,...]', 'Query port bindings')
.option('-l, --location <location>', 'Location')
.action(actions.configure);
program.command('createOAuthAppCredentials')
.option('--redirect-uri [uri]', 'Redirect Uri', 'http://localhost:4000')
.option('--scope [scopes]', 'Scopes (comma separated)', 'apps,appstore,clients,cloudron,domains,mail,profile,settings,subscription,users')
.option('--shell', 'Print shell friendly output')
.description('Create oauth app credentials for local development')
.action(actions.createOAuthAppCredentials);
program.command('debug [cmd...]')
.description('Put app in debug mode and run [cmd] as entrypoint. If cmd is "default" the main app entrypoint is run.')
.option('--app <id/location>', 'App id or location')
.option('--disable', 'Disable debug mode.')
.option('--readonly', 'Mount filesystem readonly. Default is read/write in debug mode.')
.option('--limit-memory', 'Enforces app memory limit. Default is to not limit memory in debug mode.')
.action(actions.debug);
program.command('env', 'App environment commands');
program.command('exec [cmd...]')
.description('Exec a command in an application')
.option('-t,--tty', 'Allocate tty')
.option('--app <id/location>', 'App id or location')
.action(actions.exec)
.on('--help', function() {
console.log(' Examples:');
console.log();
console.log(' $ cloudron exec --app myapp # run an interactive shell');
console.log(' $ cloudron exec --app myapp ls # run command');
console.log(' $ cloudron exec --app myapp -- ls -l # use -- to indicate end of options');
console.log();
});
program.command('export')
.description('Export app to a backup location')
.option('--app <id/location>', 'App id or location')
.option('--snapshot', 'Only snapshot the app, do not upload the snapshot', false)
.action(actions.exportApp);
program.command('import')
.description('Import app from an external backup')
.option('--app <id/location>', 'App id or location')
.option('--backup-id <backup>', 'Backup id')
.option('--backup-format <tgz|rsync>', 'Backup format (default: tgz)')
.option('--backup-config <json>', 'Backup config in JSON format')
.option('--backup-path <file|dir>', 'Absolute path to a backup on the filesystem')
.option('--backup-key <key>', 'Encryption key')
.option('--in-place', 'Re-import from current app directory without downloading any backup', false)
.action(actions.importApp);
program.command('inspect')
.description('Inspect a Cloudron returning raw JSON')
.action(actions.inspect);
program.command('init')
.description('Creates a new CloudronManifest.json and Dockerfile')
.action(actions.init);
program.command('install')
.description('Install or update app')
.option('--app <id/location>', 'App id or location. OBSOLETE: Use "update" instead.')
.option('--image <docker image>', 'Docker image')
.option('--no-wait', 'Wait for healthcheck to succeed [false]', false)
.option('-p, --port-bindings [PORT=port,...]', 'Query port bindings')
.option('-l, --location <domain>', 'Subdomain or full domain')
.option('--appstore-id <appid[@version]>', 'Use app from the store')
.option('--no-sso', 'Disable Cloudron SSO [false]', false)
.option('--debug [cmd]', 'Enable debug mode')
.option('--readonly', 'Mount filesystem readonly. Default is read/write in debug mode.')
.action(actions.install);
program.command('list')
.description('List installed applications')
.option('-q, --quiet', 'Only display app IDs')
.action(actions.list);
program.command('login [cloudron]')
.description('Login to cloudron')
.option('-u, --username <username>', 'Username')
.option('-p, --password <password>', 'Password')
.action(actions.login);
program.command('logout')
.description('Logout from cloudron')
.action(actions.logout);
program.command('logs')
.description('Application or System logs')
.option('-f, --tail', 'Follow')
.option('-l, --lines <lines>', 'Number of lines to show (default: 500)')
.option('--app <id/location>', 'App id or location')
.option('--system [service-name]', 'Show System logs or optionally service logs')
.action(actions.logs);
program.command('open')
.description('Open the app in the Browser')
.option('--app <id/location>', 'App id or location')
.action(actions.open);
program.command('pull <remote> <local>')
.description('pull remote file/dir. Use trailing slash to indicate remote directory.')
.option('--app <id/location>', 'App id or location')
.action(actions.pull);
program.command('push <local> <remote>')
.description('push a single local file or directory to a remote directory')
.option('--app <id/location>', 'App id or location')
.action(actions.push)
.on('--help', function() {
console.log();
console.log(' Examples:');
console.log();
console.log(' $ cloudron push --app myapp file.txt /app/data/file.txt # pushes file.txt');
console.log(' $ cloudron push --app myapp file.txt /app/data/ # pushes file.txt. trailing slash is important');
console.log(' $ cloudron push --app myapp dir /app/data # pushes dir/* as /app/data/dir/*');
console.log(' $ cloudron push --app myapp dir/. /app/data # pushes dir/* as /app/data/*');
console.log(' $ cloudron push --app myapp dir/subdir /app/data # pushes dir/subdir/* as /app/data/subdir/*');
console.log(' $ cloudron push --app myapp . /app/data # pushes .* as /app/data/*');
console.log();
});
program.command('repair')
.description('Repair an installed application (re-configure)')
.option('--app <id/location>', 'App id or location')
.option('--image <docker image>', 'Docker image')
.option('--no-wait', 'Wait for healthcheck to succeed [false]', false)
.action(actions.repair);
program.command('restore')
.description('Restore app from known backup')
.option('--app <id/location>', 'App id or location')
.option('--backup <backup>', 'Backup id')
.option('--no-wait', 'Wait for healthcheck to succeed [false]', false)
.action(actions.restore);
program.command('restart')
.description('Restart an installed application')
.option('--app <id/location>', 'App id or location')
.option('--no-wait', 'Wait for healthcheck to succeed [false]', false)
.action(actions.restart);
program.command('start')
.description('Start an installed application')
.option('--app <id/location>', 'App id or location')
.option('--no-wait', 'Wait for healthcheck to succeed [false]', false)
.action(actions.start);
program.command('stop')
.description('Stop an installed application')
.option('--app <id/location>', 'App id or location')
.action(actions.stop);
program.command('status')
.description('Application info')
.option('--app <id/location>', 'App id or location')
.action(actions.status);
program.command('uninstall')
.description('Uninstall app from cloudron')
.option('--app <id/location>', 'App id or location')
.action(actions.uninstall);
program.command('update')
.description('Update app')
.option('--app <id/location>', 'App id or location')
.option('--appstore-id <appid[@version]>', 'Use app from the store')
.option('--image <docker image>', 'Docker image')
.option('--no-backup', 'Skip backup [false]', false)
.option('--no-wait', 'Wait for healthcheck to succeed [false]', false)
.option('--no-force', 'Match appstore id and manifest id before updating', true)
.action(actions.update);
// deal first with global flags!
program.parse(process.argv);
var knownCommand = program.commands.some(function (command) { return command._name === process.argv[2] || command._alias === process.argv[2]; });
if (!knownCommand) {
console.log('Unknown command: ' + process.argv[2].bold + '.\nTry ' + 'cloudron help'.yellow);
process.exit(1);
}

83
bin/cloudron-appstore

@ -0,0 +1,83 @@
#!/usr/bin/env node
'use strict';
require('supererror');
require('colors');
var program = require('commander'),
appstoreActions = require('../src/appstore-actions.js');
program.version(require('../package.json').version);
program.command('login')
.description('Login to the appstore')
.option('-e, --email <email>', 'Email address')
.option('-p, --password <password>', 'Password (unsafe)')
.action(appstoreActions.login);
program.command('logout')
.description('Logout from the appstore')
.action(appstoreActions.logout);
program.command('info')
.description('List info of published app')
.option('--appstore-id <appid@version>', 'Appstore id and version')
.action(appstoreActions.info);
program.command('published')
.description('List published apps from this account')
.option('-i --image', 'Display docker image')
.action(appstoreActions.listPublishedApps);
program.command('approve')
.description('Approve a submitted app version')
.option('--appstore-id <appid@version>', 'Appstore id and version')
.action(appstoreActions.approve);
program.command('revoke')
.description('Revoke a published app version')
.option('--appstore-id <appid@version>', 'Appstore id and version')
.action(appstoreActions.revoke);
program.command('submit')
.description('Submit app to the store for review')
.action(appstoreActions.submit);
program.command('unpublish')
.description('Delete app or app version from the store')
.option('--appstore-id <id@[version]>', 'Unpublish app')
.option('-f, --force', 'Do not ask anything')
.action(appstoreActions.unpublish);
program.command('upload')
.description('Upload app to the store for testing')
.option('-i, --image <image>', 'Docker image')
.option('-f, --force', 'Update existing version')
.action(appstoreActions.upload);
program.command('versions')
.description('List published versions')
.option('--appstore-id <id>', 'Appstore id')
.option('--raw', 'Dump versions as json')
.action(appstoreActions.listVersions);
if (!process.argv.slice(2).length) {
program.outputHelp();
} else { // https://github.com/tj/commander.js/issues/338
// deal first with global flags!
program.parse(process.argv);
if (process.argv[2] === 'help') {
return program.outputHelp();
}
var knownCommand = program.commands.some(function (command) { return command._name === process.argv[2] || command._alias === process.argv[2]; });
if (!knownCommand) {
console.log('Unknown command: ' + process.argv[2].bold + '.\nTry ' + 'cloudron appstore help'.yellow);
process.exit(1);
}
return;
}
program.parse(process.argv);

68
bin/cloudron-backup

@ -0,0 +1,68 @@
#!/usr/bin/env node
'use strict';
require('supererror');
require('colors');
var program = require('commander'),
actions = require('../src/actions.js'),
backupTools = require('../src/backup-tools.js');
program.version(require('../package.json').version);
program.command('create')
.description('Create new app backup')
.option('--app <id>', 'App id')
.action(actions.backupCreate);
program.command('list')
.description('List all backups for specified app')
.option('--raw', 'Print raw json output')
.option('--app <id>', 'App id')
.action(actions.backupList);
program.command('decrypt <file>')
.description('Decrypt a directory')
.option('--password <password>', 'password')
.action(backupTools.decrypt);
program.command('decrypt-dir <indir> <outdir>')
.description('Decrypt a directory')
.option('--password <password>', 'password')
.action(backupTools.decryptDir);
program.command('decrypt-filename <path>')
.description('Encrypt a file name')
.option('--password <password>', 'password')
.action(backupTools.decryptFilename);
program.command('encrypt <input>')
.description('Encrypt a file')
.option('--password <password>', 'password')
.action(backupTools.encrypt);
program.command('encrypt-filename <path>')
.description('Encrypt a file name')
.option('--password <password>', 'password')
.action(backupTools.encryptFilename);
if (!process.argv.slice(2).length) {
program.outputHelp();
} else { // https://github.com/tj/commander.js/issues/338
// deal first with global flags!
program.parse(process.argv);
if (process.argv[2] === 'help') {
return program.outputHelp();
}
var knownCommand = program.commands.some(function (command) { return command._name === process.argv[2] || command._alias === process.argv[2]; });
if (!knownCommand) {
console.log('Unknown command: ' + process.argv[2].bold + '.\nTry ' + 'cloudron backup help'.yellow);
process.exit(1);
}
return;
}
program.parse(process.argv);

51
bin/cloudron-env

@ -0,0 +1,51 @@
#!/usr/bin/env node
'use strict';
require('supererror');
require('colors');
var program = require('commander'),
actions = require('../src/actions.js');
program.version(require('../package.json').version);
program.command('get <name>')
.description('Get environment variables')
.option('--app <id>', 'App id')
.action(actions.envGet);
program.command('list')
.description('List environment variables')
.option('--app <id>', 'App id')
.action(actions.envList);
program.command('set <KEY=value...>')
.description('Set environment variables')
.option('--app <id>', 'App id')
.action(actions.envSet);
program.command('unset <KEY...>')
.description('Unset environment variables')
.option('--app <id>', 'App id')
.action(actions.envUnset);
if (!process.argv.slice(2).length) {
program.outputHelp();
} else { // https://github.com/tj/commander.js/issues/338
// deal first with global flags!
program.parse(process.argv);
if (process.argv[2] === 'help') {
return program.outputHelp();
}
var knownCommand = program.commands.some(function (command) { return command._name === process.argv[2] || command._alias === process.argv[2]; });
if (!knownCommand) {
console.log('Unknown command: ' + process.argv[2].bold + '.\nTry ' + 'cloudron env help'.yellow);
process.exit(1);
}
return;
}
program.parse(process.argv);

BIN
logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

4424
package-lock.json

File diff suppressed because it is too large

53
package.json

@ -0,0 +1,53 @@
{
"name": "cloudron-for-ci",
"version": "4.12.8",
"license": "MIT",
"description": "Cloudron Commandline Tool",
"main": "main.js",
"homepage": "https://git.cloudron.io/cloudron/cloudron-cli",
"repository": {
"type": "git",
"url": "https://git.cloudron.io/cloudron/cloudron-cli.git"
},
"scripts": {
"test": "./node_modules/.bin/mocha test/*-test.js"
},
"bin": {
"cloudron": "./bin/cloudron"
},
"author": "Cloudron Developers <support@cloudron.io>",
"dependencies": {
"async": "^3.2.1",
"cloudron-manifestformat": "^5.10.2",
"colors": "^1.4.0",
"commander": "^6.1.0",
"debug": "^4.3.2",
"easy-table": "^1.1.1",
"ejs": "^3.1.6",
"eventsource": "^1.1.0",
"micromatch": "^4.0.4",
"mkdirp": "^1.0.4",
"once": "^1.4.0",
"open": "^8.2.1",
"progress": "^2.0.3",
"progress-stream": "^2.0.0",
"readline-sync": "^1.4.10",
"request": "^2.88.2",
"safetydance": "^2.2.0",
"split": "^1.0.1",
"superagent": "^6.1.0",
"superagent-sync": "^0.2.1",
"supererror": "^0.7.2",
"tar-fs": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.12.0.tgz",
"underscore": "^1.13.1"
},
"engines": {
"node": ">= 14.x.x"
},
"devDependencies": {
"expect.js": "^0.3.1",
"memorystream": "^0.3.1",
"mocha": "^9.1.1",
"rimraf": "^3.0.2"
}
}

1720
src/actions.js

File diff suppressed because it is too large

543
src/appstore-actions.js

@ -0,0 +1,543 @@
/* jshint node:true */
'use strict';
var superagent = require('superagent'),
util = require('util'),
path = require('path'),
assert = require('assert'),
fs = require('fs'),
safe = require('safetydance'),
Table = require('easy-table'),
readlineSync = require('readline-sync'),
config = require('./config.js'),
helper = require('./helper.js'),
exit = helper.exit,
manifestFormat = require('cloudron-manifestformat');
require('colors');
exports = module.exports = {
login: login,
logout: logout,
info: info,
listVersions: listVersions,
submit: submit,
upload: upload,
revoke: revoke,
approve: approve,
unpublish: unpublish,
listPublishedApps: listPublishedApps
};
const NO_MANIFEST_FOUND_ERROR_STRING = 'No CloudronManifest.json found';
function createUrl(api) {
return config.appStoreOrigin() + api;
}
// the app argument allows us in the future to get by name or id
function getAppstoreId(appstoreId, callback) {
if (appstoreId) {
var parts = appstoreId.split('@');
return callback(null, parts[0], parts[1]);
}
var manifestFilePath = helper.locateManifest();
if (!manifestFilePath) return callback('No CloudronManifest.json found');
var manifest = safe.JSON.parse(safe.fs.readFileSync(manifestFilePath));
if (!manifest) callback(util.format('Unable to read manifest %s. Error: %s', manifestFilePath, safe.error));
return callback(null, manifest.id, manifest.version);
}
// takes a function returning a superagent request instance and will reauthenticate in case the token is invalid
function superagentEnd(requestFactory, callback) {
requestFactory().end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode === 401) return authenticate({ error: true }, superagentEnd.bind(null, requestFactory, callback));
if (result.statusCode === 403) return callback(new Error(result.type === 'application/javascript' ? JSON.stringify(result.body) : result.text));
callback(error, result);
});
}
function authenticate(options, callback) {
if (!options.hideBanner) {
const webDomain = config.appStoreOrigin().replace('https://api.', '');
console.log(`${webDomain} login`.bold + ` (If you do not have one, sign up at https://${webDomain}/console.html#/register)`);
}
var email = options.email || readlineSync.question('Email: ', {});
var password = options.password || readlineSync.question('Password: ', { noEchoBack: true });
config.setAppStoreToken(null);
superagent.post(createUrl('/api/v1/login')).auth(email, password).send({ totpToken: options.totpToken }).end(function (error, result) {
if (error && !error.response) exit(error);
if (result.statusCode === 401 && result.body.message.indexOf('TOTP') !== -1) {
if (result.body.message === 'TOTP token missing') console.log('A 2FA TOTP Token is required for this account.'.red);
options.totpToken = readlineSync.question('2FA token: ', {});
options.email = email;
options.password = password;
options.hideBanner = true;
return authenticate(options, callback);
}
if (result.statusCode !== 200) {
console.log('Login failed.'.red);
options.hideBanner = true;
options.email = '';
options.password = '';
return authenticate(options, callback);
}
config.setAppStoreToken(result.body.accessToken);
console.log('Login successful.'.green);
if (typeof callback === 'function') callback();
});
}
function login(options) {
authenticate(options);
}
function logout() {
config.setAppStoreToken(null);
console.log('Done.'.green);
}
function info(options) {
getAppstoreId(options.appstoreId, function (error, id, version) {
if (error) exit(error);
superagentEnd(function () {
return superagent.get(createUrl('/api/v1/developers/apps/' + id + '/versions/' + version)).query({ accessToken: config.appStoreToken() });
}, function (error, result) {
if (error && !error.response) exit(util.format('Failed to list apps: %s', error.message.red));
if (result.statusCode !== 200) exit(util.format('Failed to list apps: %s message: %s', result.statusCode, result.text));
var manifest = result.body.manifest;
console.log('id: %s', manifest.id.bold);
console.log('title: %s', manifest.title.bold);
console.log('tagline: %s', manifest.tagline.bold);
console.log('description: %s', manifest.description.bold);
console.log('website: %s', manifest.website.bold);
console.log('contactEmail: %s', manifest.contactEmail.bold);
});
});
}
function listVersions(options) {
helper.verifyArguments(arguments);
getAppstoreId(options.appstoreId, function (error, id) {
if (error) exit(error);
superagentEnd(function () {
return superagent.get(createUrl('/api/v1/developers/apps/' + id + '/versions')).query({ accessToken: config.appStoreToken() });
}, function (error, result) {
if (error && !error.response) exit(util.format('Failed to list versions: %s', error.message.red));
if (result.statusCode === 404) exit('This app is not listed in appstore');
if (result.statusCode !== 200) exit(util.format('Failed to list versions: %s message: %s', result.statusCode, result.text));
if (result.body.versions.length === 0) return console.log('No versions found.');
if (options.raw) return console.log(JSON.stringify(result.body.versions, null, 2));
var versions = result.body.versions.reverse();
// var manifest = versions[0].manifest;
var t = new Table();
versions.forEach(function (version) {
t.cell('Version', version.manifest.version);
t.cell('Creation Date', version.creationDate);
t.cell('Image', version.manifest.dockerImage);
t.cell('Publish state', version.publishState);
t.newRow();
});
console.log();
console.log(t.toString());
});
});
}
function addApp(manifest, baseDir, callback) {
assert(typeof manifest === 'object');
assert(typeof baseDir === 'string');
assert(typeof callback === 'function');
superagentEnd(function () {
return superagent.post(createUrl('/api/v1/developers/apps'))
.query({ accessToken: config.appStoreToken() })
.send({ id: manifest.id });
}, function (error, result) {
if (error && !error.response) return exit(util.format('Failed to create app: %s', error.message.red));
if (result.statusCode !== 201 && result.statusCode !== 409) {
return exit(util.format('Failed to create app: %s message: %s', result.statusCode, result.text));
}
if (result.statusCode === 201) {
console.log('New application added to the appstore with id %s.'.green, manifest.id);
}
callback();
});
}
function parseChangelog(file, version) {
var changelog = '';
var data = safe.fs.readFileSync(file, 'utf8');
if (!data) return null;
var lines = data.split('\n');
version = version.replace(/-.*/, ''); // remove any prerelease
for (var i = 0; i < lines.length; i++) {
if (lines[i] === '[' + version + ']') break;
}
for (i = i + 1; i < lines.length; i++) {
if (lines[i] === '') continue;
if (lines[i][0] === '[') break;
changelog += lines[i] + '\n';
}
return changelog;
}
function addVersion(manifest, baseDir, callback) {
assert.strictEqual(typeof manifest, 'object');
assert.strictEqual(typeof baseDir, 'string');
var iconFilePath = null;
if (manifest.icon) {
var iconFile = manifest.icon; // backward compat
if (iconFile.slice(0, 7) === 'file://') iconFile = iconFile.slice(7);
iconFilePath = path.isAbsolute(iconFile) ? iconFile : path.join(baseDir, iconFile);
if (!fs.existsSync(iconFilePath)) return callback(new Error('icon not found at ' + iconFilePath));
}
if (manifest.description.slice(0, 7) === 'file://') {
var descriptionFilePath = manifest.description.slice(7);
descriptionFilePath = path.isAbsolute(descriptionFilePath) ? descriptionFilePath : path.join(baseDir, descriptionFilePath);
manifest.description = safe.fs.readFileSync(descriptionFilePath, 'utf8');
if (!manifest.description && safe.error) return callback(new Error('Could not read/parse description ' + safe.error.message));
if (!manifest.description) return callback(new Error('Description cannot be empty'));
}
if (manifest.postInstallMessage && manifest.postInstallMessage.slice(0, 7) === 'file://') {
var postInstallFilePath = manifest.postInstallMessage.slice(7);
postInstallFilePath = path.isAbsolute(postInstallFilePath) ? postInstallFilePath : path.join(baseDir, postInstallFilePath);
manifest.postInstallMessage = safe.fs.readFileSync(postInstallFilePath, 'utf8');
if (!manifest.postInstallMessage && safe.error) return callback(new Error('Could not read/parse postInstall ' + safe.error.message));
if (!manifest.postInstallMessage) return callback(new Error('PostInstall file specified but it is empty'));
}
if (manifest.changelog.slice(0, 7) === 'file://') {
var changelogPath = manifest.changelog.slice(7);
changelogPath = path.isAbsolute(changelogPath) ? changelogPath : path.join(baseDir, changelogPath);
manifest.changelog = parseChangelog(changelogPath, manifest.version);
if (!manifest.changelog) return callback(new Error('Bad changelog format or missing changelog for this version'));
}
superagentEnd(function () {
var req = superagent.post(createUrl('/api/v1/developers/apps/' + manifest.id + '/versions'));
req.query({ accessToken: config.appStoreToken() });
if (iconFilePath) req.attach('icon', iconFilePath);
req.attach('manifest', Buffer.from(JSON.stringify(manifest)), 'manifest');
return req;
}, function (error, result) {
if (error && !error.response) return callback(new Error(util.format('Failed to publish version: %s', error.message)));
if (result.statusCode === 409) return callback('This version already exists. Use --force to overwrite.');
if (result.statusCode !== 204) return callback(new Error(util.format('Failed to publish version (statusCode %s): \n%s', result.statusCode, result.body && result.body.message ? result.body.message.red : result.text)));
callback();
});
}
function updateVersion(manifest, baseDir, callback) {
assert.strictEqual(typeof manifest, 'object');
assert.strictEqual(typeof baseDir, 'string');
var iconFilePath = null;
if (manifest.icon) {
var iconFile = manifest.icon; // backward compat
if (iconFile.slice(0, 7) === 'file://') iconFile = iconFile.slice(7);
iconFilePath = path.isAbsolute(iconFile) ? iconFile : path.join(baseDir, iconFile);
if (!fs.existsSync(iconFilePath)) return callback(new Error('icon not found at ' + iconFilePath));
}
if (manifest.description.slice(0, 7) === 'file://') {
var descriptionFilePath = manifest.description.slice(7);
descriptionFilePath = path.isAbsolute(descriptionFilePath) ? descriptionFilePath : path.join(baseDir, descriptionFilePath);
manifest.description = safe.fs.readFileSync(descriptionFilePath, 'utf8');
if (!manifest.description) return callback(new Error('Could not read description ' + safe.error.message));
}
if (manifest.postInstallMessage && manifest.postInstallMessage.slice(0, 7) === 'file://') {
var postInstallFilePath = manifest.postInstallMessage.slice(7);
postInstallFilePath = path.isAbsolute(postInstallFilePath) ? postInstallFilePath : path.join(baseDir, postInstallFilePath);
manifest.postInstallMessage = safe.fs.readFileSync(postInstallFilePath, 'utf8');
if (!manifest.postInstallMessage) return callback(new Error('Could not read/parse postInstall ' + safe.error.message));
}
if (manifest.changelog.slice(0, 7) === 'file://') {
var changelogPath = manifest.changelog.slice(7);
changelogPath = path.isAbsolute(changelogPath) ? changelogPath : path.join(baseDir, changelogPath);
manifest.changelog = parseChangelog(changelogPath, manifest.version);
if (!manifest.changelog) return callback(new Error('Could not read changelog or missing version changes'));
}
superagentEnd(function () {
var req = superagent.put(createUrl('/api/v1/developers/apps/' + manifest.id + '/versions/' + manifest.version));
req.query({ accessToken: config.appStoreToken() });
if (iconFilePath) req.attach('icon', iconFilePath);
req.attach('manifest', Buffer.from(JSON.stringify(manifest)), 'manifest');
return req;
}, function (error, result) {
if (error && !error.response) return callback(new Error(util.format('Failed to publish version: %s', error.message)));
if (result.statusCode !== 204) {
return callback(new Error(util.format('Failed to publish version (statusCode %s): \n%s', result.statusCode, result.body && result.body.message ? result.body.message.red : result.text)));
}
callback();
});
}
function delVersion(manifest, force) {
assert(typeof manifest === 'object');
assert(typeof force === 'boolean');
if (!force) {
console.log('This will delete the version %s of app %s from the appstore!'.red, manifest.version.bold, manifest.id.bold);
var reallyDelete = readlineSync.question(util.format('Really do this? [y/N]: '), {});
if (reallyDelete.toUpperCase() !== 'Y') exit();
}
superagentEnd(function () {
return superagent.del(createUrl('/api/v1/developers/apps/' + manifest.id + '/versions/' + manifest.version)).query({ accessToken: config.appStoreToken() });
}, function (error, result) {
if (error && !error.response) return exit(util.format('Failed to unpublish version: %s', error.message.red));
if (result.statusCode !== 204) exit(util.format('Failed to unpublish version (statusCode %s): \n%s', result.statusCode, result.body && result.body.message ? result.body.message.red : result.text));
console.log('version unpublished.'.green);
});
}
function revokeVersion(appstoreId, version) {
assert.strictEqual(typeof appstoreId, 'string');
assert.strictEqual(typeof version, 'string');
superagentEnd(function () {
return superagent.post(createUrl('/api/v1/developers/apps/' + appstoreId + '/versions/' + version + '/revoke'))
.query({ accessToken: config.appStoreToken() })
.send({ });
}, function (error, result) {
if (error && !error.response) return exit(util.format('Failed to revoke version: %s', error.message.red));
if (result.statusCode !== 200) exit(util.format('Failed to revoke version (statusCode %s): \n%s', result.statusCode, result.body && result.body.message ? result.body.message.red : result.text));
console.log('version revoked.'.green);
});
}
function approveVersion(appstoreId, version) {
assert.strictEqual(typeof appstoreId, 'string');
assert.strictEqual(typeof version, 'string');
superagentEnd(function () {
return superagent.post(createUrl('/api/v1/developers/apps/' + appstoreId + '/versions/' + version + '/approve'))
.query({ accessToken: config.appStoreToken() })
.send({ });
}, function (error, result) {
if (error && !error.response) return exit(util.format('Failed to approve version: %s', error.message.red));
if (result.statusCode !== 200) exit(util.format('Failed to approve version (statusCode %s): \n%s', result.statusCode, result.body && result.body.message ? result.body.message.red : result.text));
console.log('Approved.'.green);
console.log('');
});
}
function delApp(appId, force) {
assert(typeof appId === 'string');
assert(typeof force === 'boolean');
if (!force) {
console.log('This will delete app %s from the appstore!'.red, appId.bold);
var reallyDelete = readlineSync.question(util.format('Really do this? [y/N]: '), {});
if (reallyDelete.toUpperCase() !== 'Y') exit();
}
superagentEnd(function () {
return superagent.del(createUrl('/api/v1/developers/apps/' + appId)).query({ accessToken: config.appStoreToken() });
}, function (error, result) {
if (error && !error.response) return exit(util.format('Failed to unpublish app: %s', error.message.red));
if (result.statusCode !== 204) exit(util.format('Failed to unpublish app (statusCode %s): \n%s', result.statusCode, result.body && result.body.message ? result.body.message.red : result.text));
console.log('App unpublished.'.green);
});
}
function submitAppForReview(manifest, callback) {
assert(typeof manifest === 'object');
assert(typeof callback === 'function');
superagentEnd(function () {
return superagent.post(createUrl('/api/v1/developers/apps/' + manifest.id + '/versions/' + manifest.version + '/submit'))
.query({ accessToken: config.appStoreToken() })
.send({ });
}, function (error, result) {
if (error && !error.response) exit(util.format('Failed to submit app for review: %s', error.message.red));
if (result.statusCode === 404) {
console.log('No version %s found. Please use %s first.', manifest.version.bold, 'cloudron appstore upload'.cyan);
exit('Failed to submit app for review.'.red);
}
if (result.statusCode !== 200) return exit(util.format('Failed to submit app (statusCode %s): \n%s', result.statusCode, result.body && result.body.message ? result.body.message.red : result.text));
console.log('App submitted for review.'.green);
console.log('You will receive an email when approved.');
callback();
});
}
function upload(options) {
helper.verifyArguments(arguments);
// try to find the manifest of this project
var manifestFilePath = helper.locateManifest();
if (!manifestFilePath) return exit(NO_MANIFEST_FOUND_ERROR_STRING);
var result = manifestFormat.parseFile(manifestFilePath);
if (result.error) return exit(result.error.message);
let manifest = result.manifest;
const sourceDir = path.dirname(manifestFilePath);
const appConfig = config.getAppConfig(sourceDir);
// image can be passed in options for buildbot
const dockerImage = options.image ? options.image : appConfig.dockerImage;
manifest.dockerImage = dockerImage;
if (!manifest.dockerImage) exit('No docker image found, run `cloudron build` first');
// ensure we remove the docker hub handle
if (manifest.dockerImage.indexOf('docker.io/') === 0) manifest.dockerImage = manifest.dockerImage.slice('docker.io/'.length);
var error = manifestFormat.checkAppstoreRequirements(manifest);
if (error) return exit(error);
// ensure the app is known on the appstore side
addApp(manifest, path.dirname(manifestFilePath), function () {
console.log(`Uploading ${manifest.id}@${manifest.version} (dockerImage: ${manifest.dockerImage}) for testing`);
var func = options.force ? updateVersion : addVersion;
func(manifest, path.dirname(manifestFilePath), function (error) {
if (error) return exit(error);
});
});
}
function submit() {
helper.verifyArguments(arguments);
// try to find the manifest of this project
var manifestFilePath = helper.locateManifest();
if (!manifestFilePath) return exit(NO_MANIFEST_FOUND_ERROR_STRING);
var result = manifestFormat.parseFile(manifestFilePath);
if (result.error) return exit(result.error.message);
var manifest = result.manifest;
submitAppForReview(manifest, exit);
}
function unpublish(options) {
helper.verifyArguments(arguments);
getAppstoreId(options.appstoreId, function (error, id, version) {
if (error) exit(error);
if (!version) {
console.log('Unpublishing ' + options.app);
delApp(options.app, !!options.force);
return;
}
console.log('Unpublishing ' + id + '@' + version);
delVersion(id, !!options.force);
});
}
function revoke(options) {
helper.verifyArguments(arguments);
getAppstoreId(options.appstoreId, function (error, id, version) {
if (error) return exit(error);
if (!version) return exit('--appstore-id must be of the format id@version');
console.log('Revoking ' + id + '@' + version);
revokeVersion(id, version);
});
}
function approve(options) {
getAppstoreId(options.appstoreId, function (error, id, version) {
if (error) return exit(error);
if (!version) return exit('--appstore-id must be of the format id@version');
console.log('Approving ' + id + '@' + version);
approveVersion(id, version);
});
}
// TODO currently no pagination, only needed once we have users with more than 100 apps
function listPublishedApps(options) {
helper.verifyArguments(arguments);
superagentEnd(function () {
return superagent.get(createUrl('/api/v1/developers/apps?per_page=100'))
.query({ accessToken: config.appStoreToken() })
.send({ });
}, function (error, result) {
if (error && !error.response) return exit(util.format('Failed to get list of published apps: %s', error.message.red));
if (result.statusCode !== 200) return exit(util.format('Failed to get list of published apps (statusCode %s): \n%s', result.statusCode, result.body && result.body.message ? result.body.message.red : result.text));
if (result.body.apps.length === 0) return console.log('No apps published.');
var t = new Table();
result.body.apps.forEach(function (app) {
t.cell('Id', app.id);
t.cell('Title', app.manifest.title);
t.cell('Latest Version', app.manifest.version);
t.cell('Publish State', app.publishState);
t.cell('Creation Date', new Date(app.creationDate));
if (options.image) t.cell('Image', app.manifest.dockerImage);
t.newRow();
});
console.log();
console.log(t.toString());
});
}

285
src/backup-tools.js

@ -0,0 +1,285 @@
#!/usr/bin/env node
'use strict';
exports = module.exports = {
encrypt,
decrypt,
encryptFilename,
decryptFilename,
decryptDir
};
const assert = require('assert'),
async = require('async'),
crypto = require('crypto'),
debug = require('debug')('cloudron-backup'),
fs = require('fs'),
path = require('path'),
safe = require('safetydance'),
TransformStream = require('stream').Transform;
function encryptFilePath(filePath, encryption) {
assert.strictEqual(typeof filePath, 'string');
assert.strictEqual(typeof encryption, 'object');
var encryptedParts = filePath.split('/').map(function (part) {
let hmac = crypto.createHmac('sha256', Buffer.from(encryption.filenameHmacKey, 'hex'));
const iv = hmac.update(part).digest().slice(0, 16); // iv has to be deterministic, for our sync (copy) logic to work
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(encryption.filenameKey, 'hex'), iv);
let crypt = cipher.update(part);
crypt = Buffer.concat([ iv, crypt, cipher.final() ]);
return crypt.toString('base64') // ensures path is valid
.replace(/\//g, '-') // replace '/' of base64 since it conflicts with path separator
.replace(/=/g,''); // strip trailing = padding. this is only needed if we concat base64 strings, which we don't
});
return encryptedParts.join('/');
}
function decryptFilePath(filePath, encryption) {
assert.strictEqual(typeof filePath, 'string');
assert.strictEqual(typeof encryption, 'object');
let decryptedParts = [];
for (let part of filePath.split('/')) {
part = part + Array(part.length % 4).join('='); // add back = padding
part = part.replace(/-/g, '/'); // replace with '/'
try {
const buffer = Buffer.from(part, 'base64');
const iv = buffer.slice(0, 16);
const decrypt = crypto.createDecipheriv('aes-256-cbc', Buffer.from(encryption.filenameKey, 'hex'), iv);
const plainText = decrypt.update(buffer.slice(16));
const plainTextString = Buffer.concat([ plainText, decrypt.final() ]).toString('utf8');
const hmac = crypto.createHmac('sha256', Buffer.from(encryption.filenameHmacKey, 'hex'));
if (!hmac.update(plainTextString).digest().slice(0, 16).equals(iv)) return { error: new Error(`mac error decrypting part ${part} of path ${filePath}`) };
decryptedParts.push(plainTextString);
} catch (error) {
debug(`Error decrypting file ${filePath} part ${part}:`, error);
return null;
}
}
return { decryptedFilePath: decryptedParts.join('/') };
}
class EncryptStream extends TransformStream {
constructor(encryption) {
super();
this._headerPushed = false;
this._iv = crypto.randomBytes(16);
this._cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(encryption.dataKey, 'hex'), this._iv);
this._hmac = crypto.createHmac('sha256', Buffer.from(encryption.dataHmacKey, 'hex'));
}
pushHeaderIfNeeded() {
if (!this._headerPushed) {
const magic = Buffer.from('CBV2');
this.push(magic);
this._hmac.update(magic);
this.push(this._iv);
this._hmac.update(this._iv);
this._headerPushed = true;
}
}
_transform(chunk, ignoredEncoding, callback) {
this.pushHeaderIfNeeded();
try {
const crypt = this._cipher.update(chunk);
debug('Pushed:', crypt.toString('hex'));
this._hmac.update(crypt);
callback(null, crypt);
} catch (error) {
callback(error);
}
}
_flush(callback) {
try {
this.pushHeaderIfNeeded(); // for 0-length files
const crypt = this._cipher.final();
this.push(crypt);
debug('Pushed:', crypt.toString('hex'));
this._hmac.update(crypt);
const mac = this._hmac.digest();
debug('Pushed mac:', mac.toString('hex'));
callback(null, mac);
} catch (error) {
callback(error);
}
}
}
class DecryptStream extends TransformStream {
constructor(encryption) {
super();
this._key = Buffer.from(encryption.dataKey, 'hex');
this._header = Buffer.alloc(0);
this._decipher = null;
this._hmac = crypto.createHmac('sha256', Buffer.from(encryption.dataHmacKey, 'hex'));
this._buffer = Buffer.alloc(0);
}
_transform(chunk, ignoredEncoding, callback) {
const needed = 20 - this._header.length; // 4 for magic, 16 for iv
debug('got chunk', chunk.length);
if (this._header.length !== 20) { // not gotten IV yet
this._header = Buffer.concat([this._header, chunk.slice(0, needed)]);
if (this._header.length !== 20) return callback();
if (!this._header.slice(0, 4).equals(new Buffer.from('CBV2'))) return callback(new Error('Invalid magic in header'));
const iv = this._header.slice(4);
this._decipher = crypto.createDecipheriv('aes-256-cbc', this._key, iv);
this._hmac.update(this._header);
}
debug('needed is', needed);
this._buffer = Buffer.concat([ this._buffer, chunk.slice(needed) ]);
debug('buffer is ', this._buffer.length);
if (this._buffer.length < 32) return callback();
try {
const cipherText = this._buffer.slice(0, -32);
debug('Got:', cipherText.toString('hex'));
this._hmac.update(cipherText);
const plainText = this._decipher.update(cipherText);
this._buffer = this._buffer.slice(-32);
callback(null, plainText);
} catch (error) {
callback(error);
}
}
_flush (callback) {
if (this._buffer.length !== 32) return callback(new Error('Invalid password or tampered file (not enough data)'));
try {
debug('Expected mac:', this._buffer.toString('hex'));
const mac = this._hmac.digest();
debug('Computed Mac:', mac.toString('hex'));
if (!mac.equals(this._buffer)) return callback(new Error('Invalid password or tampered file (mac mismatch)'));
const plainText = this._decipher.final();
callback(null, plainText);
} catch (error) {
callback(error);
}
}
}
function exit(msgOrError) {
if (typeof msgOrError === 'string') process.stderr.write(`Error: ${msgOrError}\n`);
else if (msgOrError instanceof Error) process.stderr.write(`Error: ${msgOrError.message}\n`);
process.exit(msgOrError ? 1 : 0);
}
function aesKeysFromPassword(password) {
const derived = crypto.scryptSync(password, Buffer.from('CLOUDRONSCRYPTSALT', 'utf8'), 128);
return {
dataKey: derived.slice(0, 32).toString('hex'),
dataHmacKey: derived.slice(32, 64).toString('hex'),
filenameKey: derived.slice(64, 96).toString('hex'),
filenameHmacKey: derived.slice(96).toString('hex')
};
}
function encrypt(input, options) {
if (!options.password) return exit('--password is needed');
const encryption = aesKeysFromPassword(options.password);
let inStream = fs.createReadStream(input);
let outStream = process.stdout;
let encryptStream = new EncryptStream(encryption);
inStream.on('error', exit);
encryptStream.on('error', exit);
inStream.pipe(encryptStream).pipe(outStream);
}
function encryptFilename(filePath, options) {
if (!options.password) return exit('--password is needed');
const encryption = aesKeysFromPassword(options.password);
console.log(encryptFilePath(filePath, encryption));
}
function decrypt(input, options) {
if (!options.password) return exit('--password is needed');
const fd = safe.fs.openSync(input, 'r');
if (!fd) return exit(safe.error);
let header = Buffer.alloc(4);
if (!safe.fs.readSync(fd, header, 0, 4, 0)) return exit(safe.error);
if (!header.equals(Buffer.from('CBV2'))) return exit('Legacy stream decryption not implemented yet');
safe.fs.closeSync(fd);
const encryption = aesKeysFromPassword(options.password);
let inStream = fs.createReadStream(input);
let outStream = process.stdout;
let decryptStream = new DecryptStream(encryption);
inStream.on('error', exit);
decryptStream.on('error', exit);
inStream.pipe(decryptStream).pipe(outStream);
}
function decryptDir(inDir, outDir, options) {
if (!options.password) return exit('--password is needed');
const encryption = aesKeysFromPassword(options.password);
const inDirAbs = path.resolve(process.cwd(), inDir);
const outDirAbs = path.resolve(process.cwd(), outDir);
let tbd = [ '' ]; // only has paths relative to inDirAbs
async.whilst((done) => done(null, tbd.length !== 0), function iteratee(whilstCallback) {
const cur = tbd.pop();
const entries = fs.readdirSync(path.join(inDirAbs, cur), { withFileTypes: true });
async.eachSeries(entries, function (entry, iteratorCallback) {
if (entry.isDirectory()) {
tbd.push(path.join(cur, entry.name));
return iteratorCallback();
} else if (!entry.isFile()) {
return iteratorCallback();
}
const encryptedFilePath = path.join(cur, entry.name);
const { error, decryptedFilePath } = decryptFilePath(encryptedFilePath, encryption);
if (error) return iteratorCallback(error);
let inStream = fs.createReadStream(path.join(inDirAbs, cur, entry.name));
fs.mkdirSync(path.dirname(path.join(outDirAbs, decryptedFilePath)), { recursive: true });
let outStream = fs.createWriteStream(path.join(outDirAbs, decryptedFilePath));
let decryptStream = new DecryptStream(encryption);
inStream.on('error', iteratorCallback);
decryptStream.on('error', iteratorCallback);
inStream.pipe(decryptStream).pipe(outStream).on('finish', iteratorCallback);
}, whilstCallback);
}, exit);
}
function decryptFilename(filePath, options) {
if (!options.password) return exit('--password is needed');
const encryption = aesKeysFromPassword(options.password);
const { error, decryptedFilePath } = decryptFilePath(filePath, encryption);
if (error) return exit(error);
console.log(decryptedFilePath);
}

355