Merge pull request #1685 from balena-io/1681-fix-ssh-msys

Fix balena ssh to local device (MSYS/Windows and command pipe to service)
This commit is contained in:
Paulo Castro 2020-03-31 00:34:00 +01:00 committed by GitHub
commit 6c0b3a5e53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 188 additions and 311 deletions

View File

@ -89,20 +89,30 @@ HTTP(S) proxies can be configured through any of the following methods, in prece
`BALENARC_PROXY`.
> Note: The `balena ssh` command has additional setup requirements to work behind a proxy.
> Check the [installation instructions](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).
> Check the [installation instructions](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md),
> and ensure that the proxy server is configured to allow proxy requests to ssh port 22, using
> SSL encryption. For example, in the case of the [Squid](http://www.squid-cache.org/) proxy
> server, it should be configured with the following rules in the `squid.conf` file:
> `acl SSL_ports port 22`
> `acl Safe_ports port 22`
Some installations of the balena CLI also include support for the `BALENARC_NO_PROXY` environment
variable, which allows proxy exclusion patterns to be defined. The current support status is listed
below. Eventually, all installation types will have support for it.
#### Proxy exclusion
OS | Installation type | BALENARC_NO_PROXY environment variable support
-- | ----------------- | ----------------------------------------------
Windows | standalone zip | Supported with CLI v11.24.0 and later
Windows | native/GUI | Not supported
macOS | standalone zip | Not supported
macOS | native/GUI | Supported with CLI v11.24.0 and later
Linux | standalone zip | Not supported
Any | npm | Supported with Node.js >= v10.16.0 and CLI >= v11.24.0
The `BALENARC_NO_PROXY` variable may be used to exclude specified destinations from proxying.
> * This feature requires balena CLI version 11.30.8 or later. In the case of the npm [installation
> option](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md), it also requires
> Node.js version 10.16.0 or later.
> * To exclude a `balena ssh` target from proxying (IP address or `.local` hostname), the
> `--noproxy` option should be specified in addition to the `BALENARC_NO_PROXY` variable.
By default (if `BALENARC_NO_PROXY` is not defined), all [private IPv4
addresses](https://en.wikipedia.org/wiki/Private_network) and `'*.local'` hostnames are excluded
from proxying. Other hostnames that resolve to private IPv4 addresses are **not** excluded by
default, because matching takes place before name resolution.
`localhost` and `127.0.0.1` are always excluded from proxying, regardless of the value of
BALENARC_NO_PROXY.
The format of the `BALENARC_NO_PROXY` environment variable is a comma-separated list of patterns
that are matched against hostnames or IP addresses. For example:
@ -111,18 +121,10 @@ that are matched against hostnames or IP addresses. For example:
export BALENARC_NO_PROXY='*.local,dev*.mycompany.com,192.168.*'
```
Matched patterns are excluded from proxying. Matching takes place _before_ name resolution, so a
pattern like `'192.168.*'` will **not** match a hostname like `proxy.company.com` even if the
hostname resolves to an IP address like `192.168.1.2`. Pattern matching expressions are documented
at [matcher](https://www.npmjs.com/package/matcher#usage).
By default, if BALENARC_NO_PROXY is not defined, all [private IPv4
addresses](https://en.wikipedia.org/wiki/Private_network) and `'*.local'` are excluded from
proxying. Other hostnames that may resolve to private IPv4 addresses are **not** excluded by
default, as matching takes place _before_ name resolution. In addition, `localhost` and `127.0.0.1`
are always excluded from proxying, regardless of the value of BALENARC_NO_PROXY. These default
exclusions only apply to the CLI installations where BALENARC_NO_PROXY is supported, as listed in
the table above.
Matched patterns are excluded from proxying. Wildcard expressions are documented at
[matcher](https://www.npmjs.com/package/matcher#usage). Matching takes place _before_ name
resolution, so a pattern like `'192.168.*'` will **not** match a hostname that resolves to an IP
address like `192.168.1.2`.
## Command reference documentation

View File

@ -82,20 +82,30 @@ HTTP(S) proxies can be configured through any of the following methods, in prece
`BALENARC_PROXY`.
> Note: The `balena ssh` command has additional setup requirements to work behind a proxy.
> Check the [installation instructions](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).
> Check the [installation instructions](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md),
> and ensure that the proxy server is configured to allow proxy requests to ssh port 22, using
> SSL encryption. For example, in the case of the [Squid](http://www.squid-cache.org/) proxy
> server, it should be configured with the following rules in the `squid.conf` file:
> `acl SSL_ports port 22`
> `acl Safe_ports port 22`
Some installations of the balena CLI also include support for the `BALENARC_NO_PROXY` environment
variable, which allows proxy exclusion patterns to be defined. The current support status is listed
below. Eventually, all installation types will have support for it.
#### Proxy exclusion
OS | Installation type | BALENARC_NO_PROXY environment variable support
-- | ----------------- | ----------------------------------------------
Windows | standalone zip | Supported with CLI v11.24.0 and later
Windows | native/GUI | Not supported
macOS | standalone zip | Not supported
macOS | native/GUI | Supported with CLI v11.24.0 and later
Linux | standalone zip | Not supported
Any | npm | Supported with Node.js >= v10.16.0 and CLI >= v11.24.0
The `BALENARC_NO_PROXY` variable may be used to exclude specified destinations from proxying.
> * This feature requires balena CLI version 11.30.8 or later. In the case of the npm [installation
> option](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md), it also requires
> Node.js version 10.16.0 or later.
> * To exclude a `balena ssh` target from proxying (IP address or `.local` hostname), the
> `--noproxy` option should be specified in addition to the `BALENARC_NO_PROXY` variable.
By default (if `BALENARC_NO_PROXY` is not defined), all [private IPv4
addresses](https://en.wikipedia.org/wiki/Private_network) and `'*.local'` hostnames are excluded
from proxying. Other hostnames that resolve to private IPv4 addresses are **not** excluded by
default, because matching takes place before name resolution.
`localhost` and `127.0.0.1` are always excluded from proxying, regardless of the value of
BALENARC_NO_PROXY.
The format of the `BALENARC_NO_PROXY` environment variable is a comma-separated list of patterns
that are matched against hostnames or IP addresses. For example:
@ -104,18 +114,10 @@ that are matched against hostnames or IP addresses. For example:
export BALENARC_NO_PROXY='*.local,dev*.mycompany.com,192.168.*'
```
Matched patterns are excluded from proxying. Matching takes place _before_ name resolution, so a
pattern like `'192.168.*'` will **not** match a hostname like `proxy.company.com` even if the
hostname resolves to an IP address like `192.168.1.2`. Pattern matching expressions are documented
at [matcher](https://www.npmjs.com/package/matcher#usage).
By default, if BALENARC_NO_PROXY is not defined, all [private IPv4
addresses](https://en.wikipedia.org/wiki/Private_network) and `'*.local'` are excluded from
proxying. Other hostnames that may resolve to private IPv4 addresses are **not** excluded by
default, as matching takes place _before_ name resolution. In addition, `localhost` and `127.0.0.1`
are always excluded from proxying, regardless of the value of BALENARC_NO_PROXY. These default
exclusions only apply to the CLI installations where BALENARC_NO_PROXY is supported, as listed in
the table above.
Matched patterns are excluded from proxying. Wildcard expressions are documented at
[matcher](https://www.npmjs.com/package/matcher#usage). Matching takes place _before_ name
resolution, so a pattern like `'192.168.*'` will **not** match a hostname that resolves to an IP
address like `192.168.1.2`.
## Support, FAQ and troubleshooting
@ -1205,7 +1207,8 @@ support) please check:
#### --port, -p <port>
SSH gateway port
SSH server port number (default 22222) if the target is an IP address or .local
hostname. Otherwise, port number for the balenaCloud gateway (default 22).
#### --verbose, -v
@ -1213,8 +1216,7 @@ Increase verbosity
#### --noproxy
Don't use the proxy configuration for this connection. This flag
only make sense if you've configured a proxy globally.
Bypass global proxy configuration for the ssh connection
## tunnel <deviceOrApplication>

View File

@ -58,5 +58,3 @@ exports.pipeContainerStream = Promise.method ({ deviceIp, name, outStream, follo
if err is '404'
return console.log(getChalk().red.bold("Container '#{name}' not found."))
throw err
exports.getSubShellCommand = require('../../utils/helpers').getSubShellCommand

View File

@ -1,113 +0,0 @@
###
Copyright 2017 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
###
{ hostOSAccess } = require('../command-options')
_ = require('lodash')
localHostOSAccessOption = _.cloneDeep(hostOSAccess)
localHostOSAccessOption.description = 'get a shell into the host OS'
module.exports =
signature: 'local ssh [deviceIp]'
description: 'Get a shell into a balenaOS device'
help: '''
Warning: 'balena local ssh' requires an openssh-compatible client to be correctly
installed in your shell environment. For more information (including Windows
support) please check the README here: https://github.com/balena-io/balena-cli
Use this command to get a shell into the running application container of
your device.
The '--host' option will get you a shell into the Host OS of the balenaOS device.
No option will return a list of containers to enter or you can explicitly select
one by passing its name to the --container option
Examples:
$ balena local ssh
$ balena local ssh --host
$ balena local ssh --container chaotic_water
$ balena local ssh --container chaotic_water --port 22222
$ balena local ssh --verbose
'''
options: [
signature: 'verbose'
boolean: true
description: 'increase verbosity'
alias: 'v'
,
localHostOSAccessOption,
signature: 'container'
parameter: 'container'
default: null
description: 'name of container to access'
alias: 'c'
,
signature: 'port'
parameter: 'port'
description: 'ssh port number (default: 22222)'
alias: 'p'
]
root: true
action: (params, options) ->
child_process = require('child_process')
Promise = require 'bluebird'
_ = require('lodash')
{ forms } = require('balena-sync')
{ selectContainerFromDevice, getSubShellCommand } = require('./common')
{ exitWithExpectedError } = require('../../utils/patterns')
if (options.host is true and options.container?)
exitWithExpectedError('Please pass either --host or --container option')
if not options.port?
options.port = 22222
verbose = if options.verbose then '-vvv' else ''
Promise.try ->
if not params.deviceIp?
return forms.selectLocalBalenaOsDevice()
return params.deviceIp
.then (deviceIp) ->
_.assign(options, { deviceIp })
return if options.host
if not options.container?
return selectContainerFromDevice(deviceIp)
return options.container
.then (container) ->
command = "ssh \
#{verbose} \
-t \
-p #{options.port} \
-o LogLevel=ERROR \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
root@#{options.deviceIp}"
if not options.host
shellCmd = '''/bin/sh -c $"'if [ -e /bin/bash ]; then exec /bin/bash; else exec /bin/sh; fi'"'''
dockerCmd = "'$(if [ -f /usr/bin/balena ]; then echo \"balena\"; else echo \"docker\"; fi)'"
command += " #{dockerCmd} exec -ti #{container} #{shellCmd}"
subShellCommand = getSubShellCommand(command)
child_process.spawn subShellCommand.program, subShellCommand.args,
stdio: 'inherit'

View File

@ -1,5 +1,5 @@
/*
Copyright 2016-2019 Balena
Copyright 2016-2020 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -17,7 +17,6 @@ import * as BalenaSdk from 'balena-sdk';
import { CommandDefinition } from 'capitano';
import { stripIndent } from 'common-tags';
import { BalenaDeviceNotFound } from 'balena-errors';
import { getBalenaSdk } from '../utils/lazy';
import { validateDotLocalUrl, validateIPAddress } from '../utils/validation';
@ -27,7 +26,7 @@ async function getContainerId(
serviceName: string,
sshOpts: {
port?: number;
proxyCommand?: string;
proxyCommand?: string[];
proxyUrl: string;
username: string;
},
@ -70,7 +69,7 @@ async function getContainerId(
}
containerId = body.services[serviceName];
} else {
console.log(stripIndent`
console.error(stripIndent`
Using legacy method to detect container ID. This will be slow.
To speed up this process, please update your device to an OS
which has a supervisor version of at least v8.6.0.
@ -80,30 +79,29 @@ async function getContainerId(
// container
const { child_process } = await import('mz');
const escapeRegex = await import('lodash/escapeRegExp');
const { getSubShellCommand } = await import('../utils/helpers');
const { which } = await import('../utils/helpers');
const { deviceContainerEngineBinary } = await import('../utils/device/ssh');
const command = generateVpnSshCommand({
const sshBinary = await which('ssh');
const sshArgs = generateVpnSshCommand({
uuid,
verbose: false,
port: sshOpts.port,
command: `host ${uuid} '"${deviceContainerEngineBinary}" ps --format "{{.ID}} {{.Names}}"'`,
command: `host ${uuid} "${deviceContainerEngineBinary}" ps --format "{{.ID}} {{.Names}}"`,
proxyCommand: sshOpts.proxyCommand,
proxyUrl: sshOpts.proxyUrl,
username: sshOpts.username,
});
const subShellCommand = getSubShellCommand(command);
const subprocess = child_process.spawn(
subShellCommand.program,
subShellCommand.args,
{
stdio: [null, 'pipe', null],
},
);
if (process.env.DEBUG) {
console.error(`[debug] [${sshBinary}, ${sshArgs.join(', ')}]`);
}
const subprocess = child_process.spawn(sshBinary, sshArgs, {
stdio: [null, 'pipe', null],
});
const containers = await new Promise<string>((resolve, reject) => {
let output = '';
subprocess.stdout.on('data', chunk => (output += chunk.toString()));
const output: string[] = [];
subprocess.stdout.on('data', chunk => output.push(chunk.toString()));
subprocess.on('close', (code: number) => {
if (code !== 0) {
reject(
@ -112,7 +110,7 @@ async function getContainerId(
),
);
} else {
resolve(output);
resolve(output.join(''));
}
});
});
@ -143,17 +141,21 @@ function generateVpnSshCommand(opts: {
port?: number;
username: string;
proxyUrl: string;
proxyCommand?: string;
proxyCommand?: string[];
}) {
return (
`ssh ${
opts.verbose ? '-vvv' : ''
} -t -o LogLevel=ERROR -o StrictHostKeyChecking=no ` +
`-o UserKnownHostsFile=/dev/null ` +
`${opts.proxyCommand != null ? opts.proxyCommand : ''} ` +
`${opts.port != null ? `-p ${opts.port}` : ''} ` +
`${opts.username}@ssh.${opts.proxyUrl} ${opts.command}`
);
return [
...(opts.verbose ? ['-vvv'] : []),
'-t',
...['-o', 'LogLevel=ERROR'],
...['-o', 'StrictHostKeyChecking=no'],
...['-o', 'UserKnownHostsFile=/dev/null'],
...(opts.proxyCommand && opts.proxyCommand.length
? ['-o', `ProxyCommand=${opts.proxyCommand.join(' ')}`]
: []),
...(opts.port ? ['-p', opts.port.toString()] : []),
`${opts.username}@ssh.${opts.proxyUrl}`,
opts.command,
];
}
export const ssh: CommandDefinition<
@ -207,7 +209,9 @@ export const ssh: CommandDefinition<
{
signature: 'port',
parameter: 'port',
description: 'SSH gateway port',
description: stripIndent`
SSH server port number (default 22222) if the target is an IP address or .local
hostname. Otherwise, port number for the balenaCloud gateway (default 22).`,
alias: 'p',
},
{
@ -219,24 +223,19 @@ export const ssh: CommandDefinition<
{
signature: 'noproxy',
boolean: true,
description: stripIndent`
Don't use the proxy configuration for this connection. This flag
only make sense if you've configured a proxy globally.`,
description: 'Bypass global proxy configuration for the ssh connection',
},
],
action: async (params, options) => {
const applicationOrDevice =
params.applicationOrDevice_raw || params.applicationOrDevice;
const bash = await import('bash');
const { getProxyConfig, getSubShellCommand, which } = await import(
const { ExpectedError } = await import('../errors');
const { getProxyConfig, which, whichSpawn } = await import(
'../utils/helpers'
);
const { child_process } = await import('mz');
const {
exitIfNotLoggedIn,
exitWithExpectedError,
getOnlineTargetUuid,
} = await import('../utils/patterns');
const { checkLoggedIn, getOnlineTargetUuid } = await import(
'../utils/patterns'
);
const sdk = getBalenaSdk();
const verbose = options.verbose === true;
@ -259,34 +258,30 @@ export const ssh: CommandDefinition<
}
// this will be a tunnelled SSH connection...
await exitIfNotLoggedIn();
await checkLoggedIn();
const uuid = await getOnlineTargetUuid(sdk, applicationOrDevice);
let version: string | undefined;
let id: number | undefined;
try {
const device = await sdk.models.device.get(uuid, {
$select: ['id', 'supervisor_version', 'is_online'],
});
id = device.id;
version = device.supervisor_version;
} catch (e) {
if (e instanceof BalenaDeviceNotFound) {
exitWithExpectedError(`Could not find device: ${uuid}`);
}
}
const device = await sdk.models.device.get(uuid, {
$select: ['id', 'supervisor_version', 'is_online'],
});
id = device.id;
version = device.supervisor_version;
const [whichProxytunnel, username, proxyUrl] = await Promise.all([
useProxy ? which('proxytunnel', false) : undefined,
sdk.auth.whoami(),
// note that `proxyUrl` refers to the balenaCloud "resin-proxy"
// service, currently "balena-devices.com", rather than some
// local proxy server URL
sdk.settings.get('proxyUrl'),
]);
const getSshProxyCommand = () => {
if (!useProxy) {
return '';
if (!proxyConfig) {
return;
}
if (!whichProxytunnel) {
console.warn(stripIndent`
Proxy is enabled but the \`proxytunnel\` binary cannot be found.
@ -295,27 +290,32 @@ export const ssh: CommandDefinition<
for the \`ssh\` requests.
Attempting the unproxied request for now.`);
return '';
return;
}
const p = proxyConfig!;
const tunnelOptions: Dictionary<string> = {
proxy: `${p.host}:${p.port}`,
dest: '%h:%p',
};
const p = proxyConfig;
if (p.username && p.password) {
tunnelOptions.user = p.username;
tunnelOptions.pass = p.password;
// proxytunnel understands these variables for proxy authentication.
// Setting the variables instead of command-line options avoids the
// need for shell-specific escaping of special characters like '$'.
process.env.PROXYUSER = p.username;
process.env.PROXYPASS = p.password;
}
const ProxyCommand = `proxytunnel ${bash.args(tunnelOptions, '--', '=')}`;
return `-o ${bash.args({ ProxyCommand }, '', '=')}`;
return [
'proxytunnel',
`--proxy=${p.host}:${p.port}`,
// ssh replaces these %h:%p variables in the ProxyCommand option
// https://linux.die.net/man/5/ssh_config
'--dest=%h:%p',
...(verbose ? ['--verbose'] : []),
];
};
const proxyCommand = getSshProxyCommand();
const proxyCommand = useProxy ? getSshProxyCommand() : undefined;
if (username == null) {
exitWithExpectedError(
throw new ExpectedError(
`Opening an SSH connection to a remote device requires you to be logged in.`,
);
}
@ -356,9 +356,6 @@ export const ssh: CommandDefinition<
username: username!,
});
const subShellCommand = getSubShellCommand(command);
await child_process.spawn(subShellCommand.program, subShellCommand.args, {
stdio: 'inherit',
});
await whichSpawn('ssh', command);
},
};

View File

@ -1,5 +1,5 @@
/*
Copyright 2016-2019 Balena
Copyright 2016-2020 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -27,12 +27,11 @@ export const deviceContainerEngineBinary = `$(if [ -f /usr/bin/balena ]; then ec
export async function performLocalDeviceSSH(
opts: DeviceSSHOpts,
): Promise<void> {
const childProcess = await import('child_process');
const reduce = await import('lodash/reduce');
const { getSubShellCommand } = await import('../helpers');
const { exitWithExpectedError } = await import('../patterns');
const { whichSpawn } = await import('../helpers');
const { ExpectedError } = await import('../../errors');
const { stripIndent } = await import('common-tags');
const os = await import('os');
const { isatty } = await import('tty');
let command = '';
@ -57,10 +56,9 @@ export async function performLocalDeviceSSH(
try {
allContainers = await docker.listContainers();
} catch (_e) {
exitWithExpectedError(stripIndent`
throw new ExpectedError(stripIndent`
Could not access docker daemon on device ${opts.address}.
Please ensure the device is in local mode.`);
return;
}
const serviceNames: string[] = [];
@ -80,7 +78,7 @@ export async function performLocalDeviceSSH(
.filter(c => c != null);
if (containers.length === 0) {
exitWithExpectedError(
throw new ExpectedError(
`Could not find a service on device with name ${opts.service}. ${
serviceNames.length > 0
? `Available services:\n${reduce(
@ -93,36 +91,30 @@ export async function performLocalDeviceSSH(
);
}
if (containers.length > 1) {
exitWithExpectedError(stripIndent`
throw new ExpectedError(stripIndent`
Found more than one container with a service name ${opts.service}.
This state is not supported, please contact support.
`);
}
// Getting a command to work on all platforms is a pain,
// so we just define slightly different ones for windows
if (os.platform() !== 'win32') {
const shellCmd = `/bin/sh -c "if [ -e /bin/bash ]; then exec /bin/bash; else exec /bin/sh; fi"`;
command = `'${deviceContainerEngineBinary}' exec -ti ${
containers[0]!.id
} '${shellCmd}'`;
} else {
const shellCmd = `/bin/sh -c "if [ -e /bin/bash ]; then exec /bin/bash; else exec /bin/sh; fi"`;
command = `${deviceContainerEngineBinary} exec -ti ${
containers[0]!.id
} ${shellCmd}`;
}
const containerId = containers[0]!.id;
const shellCmd = `/bin/sh -c "if [ -e /bin/bash ]; then exec /bin/bash; else exec /bin/sh; fi"`;
// stdin (fd=0) is not a tty when data is piped in, for example
// echo 'ls -la; exit;' | balena ssh 192.168.0.20 service1
// See https://www.balena.io/blog/balena-monthly-roundup-january-2020/#charliestipsntricks
// https://assets.balena.io/newsletter/2020-01/pipe.png
const ttyFlag = isatty(0) ? '-t' : '';
command = `${deviceContainerEngineBinary} exec -i ${ttyFlag} ${containerId} ${shellCmd}`;
}
// Generate the SSH command
const sshCommand = `ssh \
${opts.verbose ? '-vvv' : ''} \
-t \
-p ${opts.port ? opts.port : 22222} \
-o LogLevel=ERROR \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
root@${opts.address} ${command}`;
const subShell = getSubShellCommand(sshCommand);
childProcess.spawn(subShell.program, subShell.args, { stdio: 'inherit' });
return whichSpawn('ssh', [
...(opts.verbose ? ['-vvv'] : []),
'-t',
...['-p', opts.port ? opts.port.toString() : '22222'],
...['-o', 'LogLevel=ERROR'],
...['-o', 'StrictHostKeyChecking=no'],
...['-o', 'UserKnownHostsFile=/dev/null'],
`root@${opts.address}`,
...(command ? [command] : []),
]);
}

View File

@ -16,9 +16,10 @@ limitations under the License.
import { InitializeEmitter, OperationState } from 'balena-device-init';
import * as BalenaSdk from 'balena-sdk';
import Bluebird = require('bluebird');
import _ = require('lodash');
import os = require('os');
import * as Bluebird from 'bluebird';
import { spawn, SpawnOptions } from 'child_process';
import * as _ from 'lodash';
import * as os from 'os';
import * as ShellEscape from 'shell-escape';
import { ExpectedError } from '../errors';
@ -187,35 +188,6 @@ export function getApplication(applicationName: string) {
return balena.models.application.get(applicationName, extraOptions);
}
/**
* Choose between 'cmd.exe' and '/bin/sh' for running the given command string,
* depending on the value of `os.platform()`.
* When writing new code, consider whether it would be possible to avoid using a
* shell at all, using the which() function in this module to obtain a program's
* full path, executing the program directly and passing the arguments as an
* array instead of a long string. Avoiding a shell has several benefits:
* - Avoids the need to shell-escape arguments, especially nested commands.
* - Bypasses the incompatibilities between cmd.exe and /bin/sh.
* - Reduces the security risks of lax input validation.
* Code example avoiding a shell:
* const program = await which('ssh');
* const args = ['root@192.168.1.1', 'cat /etc/os-release'];
* const child = spawn(program, args);
*/
export function getSubShellCommand(command: string) {
if (os.platform() === 'win32') {
return {
program: 'cmd.exe',
args: ['/s', '/c', command],
};
} else {
return {
program: '/bin/sh',
args: ['-c', command],
};
}
}
/**
* Call `func`, and if func() throws an error or returns a promise that
* eventually rejects, retry it `times` many times, each time printing a
@ -406,6 +378,40 @@ export async function which(
return programPath;
}
/**
* Call which(programName) and spawn() with the given arguments.
* Reject the promise if the process exit code is not zero.
*/
export async function whichSpawn(
programName: string,
args: string[],
options: SpawnOptions = { stdio: 'inherit' },
): Promise<void> {
const program = await which(programName);
if (process.env.DEBUG) {
console.error(`[debug] [${program}, ${args.join(', ')}]`);
}
let error: Error | undefined;
let exitCode: number | undefined;
try {
exitCode = await new Promise<number>((resolve, reject) => {
spawn(program, args, options)
.on('error', reject)
.on('close', resolve);
});
} catch (err) {
error = err;
}
if (error || exitCode) {
const msg = [
`${programName} failed with exit code ${exitCode}:`,
`[${program}, ${args.join(', ')}]`,
...(error ? [`${error}`] : []),
];
throw new Error(msg.join('\n'));
}
}
export interface ProxyConfig {
host: string;
port: string;

5
npm-shrinkwrap.json generated
View File

@ -2431,11 +2431,6 @@
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz",
"integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw=="
},
"bash": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/bash/-/bash-0.0.1.tgz",
"integrity": "sha1-nuDnp1K8Xu8Wi8SHGVVqdFSKrgw="
},
"bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",

View File

@ -177,7 +177,6 @@
"balena-semver": "^2.2.0",
"balena-settings-client": "^4.0.4",
"balena-sync": "^10.2.0",
"bash": "0.0.1",
"bluebird": "^3.7.2",
"body-parser": "^1.19.0",
"capitano": "^1.9.2",

View File

@ -1 +0,0 @@
declare module 'bash';