zx icon indicating copy to clipboard operation
zx copied to clipboard

Feature request: support secure shell

Open r0binary opened this issue 2 years ago • 26 comments

I love to use this to unify scripts that otherwise would end up being complex bash scripts.

It would be great if the lib was not limited to child_processes but could run commands via SSH on remote machines. This way I could get rid of automation tools like Ansible in some scenarios. Let me know if this sounds reasonable and valuable.

r0binary avatar Nov 03 '21 16:11 r0binary

This is what I was thinking of too. Maybe as a separate project.

antonmedv avatar Nov 03 '21 18:11 antonmedv

A hack to get this is to create an executable bash script with this content:

#!/bin/sh
ssh -t -q $SSH_USER@$SSH_HOST "$2"

This requires a non interactive authentication method (I tried with public key auth). In the JavaScript one can set the script as shell and provide parameters to it:

process.env.SSH_USER = 'USER'
process.env.SSH_HOST = 'HOSTNAME'
$.shell = '/path/to/script.sh'

One limitation is the $.cwd won't work, because it sets the cwd for the locally spawned child process and has no chance to manipulate the remotely invoked shell. A workaround to this is to extend $.prefix

$.prefix += 'cd /path/to/work/in;'

r0binary avatar Nov 04 '21 08:11 r0binary

I love to use this to unify scripts that otherwise would end up being complex bash scripts.

It would be great if the lib was not limited to child_processes but could run commands via SSH on remote machines. This way I could get rid of automation tools like Ansible in some scenarios. Let me know if this sounds reasonable and valuable.

I had a somewhat similar idea, being able to run zx in a Docker container and execute the commands on the host machine (or some remote instance). Goal would be to not even have to have Node.js / zx installed [though Docker would need to be], and still run the scripts outside of the container.

That way, if anything funky needed to be set up to make it work, or give it some extra capability, that could be bottled up within the container itself, and the potentially interactive output be handled on the host / some other machine.

jzombie avatar Feb 21 '22 00:02 jzombie

I was thinking about sending commands by ssh.

antonmedv avatar Feb 21 '22 07:02 antonmedv

I imagine an API like this:

const exampleConfig = {
  user: 'fred',
  identityFile: '/path/to/id_rsa',
  hostName: 'example.com',
  passphrase: 'secret'
};

await ssh(exampleConfig, $ => {
  await $`ls -la`;
});

This would preserve the feel of zx but using ssh rather than child_process as the execution mechanism

I hacked something together which basically works for my case: https://gist.github.com/joewalker/9cb2529325009b89d4935c42f49d3f74

joewalker avatar Feb 23 '22 14:02 joewalker

I also was thinking about something like this. One thing I’m wondering it what about cd() function. Should it be working too?

antonmedv avatar Feb 23 '22 14:02 antonmedv

@joewalker did you saw https://www.npmjs.com/package/ssh2-promise, it's looks like you looking for.

alexey-zaharchenko avatar Feb 23 '22 14:02 alexey-zaharchenko

I also was thinking about something like this. One thing I’m wondering it what about cd() function. Should it be working too?

The gist I posted doesn't support cd(), but ideally it should. Zx assumes that it's the only command processor, so I guess to support the case where ssh adds others, you'd need to do something like:

await ssh(exampleConfig, { $, cd } => {
  cd('/')
  await $`ls -la`
});

@joewalker did you saw https://www.npmjs.com/package/ssh2-promise, it's looks like you looking for.

I hadn't seen that thanks. The idea was to have something like zx + ssh which isn't exactly ssh2-promise, but ssh2-promise is the closest published thing that works now.

joewalker avatar Feb 23 '22 15:02 joewalker

Here's my quick hack:

import { NodeSSH } from 'node-ssh';

const ssh = async (config, job) => {
  const conn = new NodeSSH();
  await conn.connect(config);
  
  let cwd = undefined;
  const cd = (newCwd) => cwd = newCwd;
  
  const $ = async (pieces, ...args) => {
    const { verbose = true } = $;

    let cmd = '';
    pieces.forEach((p, i) => {
        cmd += `${p}${i === pieces.length - 1 ? '' : args[i]}`;
    });
    if (verbose) {
      console.log('$', cmd);
    }

    const output = await conn.execCommand(cmd, {
      cwd,
      onStdout: (b) => process.stdout.write(b),
      onStderr: (b) => process.stderr.write(b),
    });
    if (output.code) {
      throw new Error(`exit code: ${output.code}`);
    }
    return output;
  }

  await job({ $, cd });
  await conn.dispose();
}

await ssh({
  host: '...',
  username: '...',
  privateKey: `${os.homedir()}/.ssh/id_rsa`
}, async ({ $, cd }) => {
  await $`pwd`;
  cd('/etc');
  await $`pwd`;
  await $`echo stderr > /dev/stderr`;
});

mszczepanczyk avatar Feb 23 '22 19:02 mszczepanczyk

sounds like zx is reinventing the python tool fabric

airtonix avatar Jun 25 '22 06:06 airtonix

Alternate version from the one by @r0binary which more or less does the same thing but more explicitly, and supports any case where zx splits arguments instead of passing them all as one big arg to bash -c (I'm not sure if there are any cases like this):

#!/usr/bin/env bash
shift # Get rid of the -c that zx prefixes every command with
exec ssh -T ${SSH_ARGS} <<< "$@" # Note: intentional word splitting on SSH_ARGS

use it like:

import { $ } from "zx";

$.shell = "/path/to/ssh_shell.sh";
process.env.SSH_ARGS = "-v [email protected]";
await $`cd /some/directory/on/the/remote && ls -la | grep js`;

milesrichardson avatar Jan 22 '23 23:01 milesrichardson

completed in 7.2.0

mustafa0x avatar Feb 28 '23 05:02 mustafa0x

Great news, thanks. So can we close the ticket then?

r0binary avatar Feb 28 '23 07:02 r0binary

For those curious for more info, here's the diff of 7.1.1..7.2.0 with the changes, in particular, a new dependency on webpod as shown in the newly updated readme:

Executing commands on remote hosts

The zx uses webpod to execute commands on remote hosts.

import { ssh } from 'zx'
await ssh('user@host')`echo Hello, world!`

It looks like webpod is a new library from @antonmedv who is in this very thread, so thanks for that! 😸

milesrichardson avatar Feb 28 '23 22:02 milesrichardson

For now it is not possible to enable inherit mode for ssh stdio, so migrating to this new feature would reduce scripts' transparency.

StreetStrider avatar Mar 02 '23 15:03 StreetStrider

Why do you need inherited stdio for remote shell?

antonmedv avatar Mar 02 '23 16:03 antonmedv

@antonmedv I have a multi-stage build script which is part-local and part-remote. Remote parts are essentially ssh -T $remote <<HEREDOC. So they are like local subcommands in inherit mode; and I can see the output (progress and errors) immediately.

StreetStrider avatar Mar 02 '23 17:03 StreetStrider

Ok, got it: for real-time output. Will add this.

No interactive inputs?

antonmedv avatar Mar 02 '23 18:03 antonmedv

@antonmedv not sure, I'm not using this, but maybe someone is. I think people would try to blindly port trailing .stdio() option from their local zx statements. (that was my first try). If so, it may be worth to port this syntax 1-to-1 for ssh, so interactive inputs will be supported as well.

StreetStrider avatar Mar 02 '23 18:03 StreetStrider

Is it possible to use cd, fs and all other goodies in ssh when using webpod?

haikyuu avatar May 03 '23 08:05 haikyuu

For cd() not sure, as it will be unclear what will be changed.

Maybe via this API:

const $ = ssh('host')
$.cd('./path')

Same for fs. $.fs.readFile()?

antonmedv avatar May 03 '23 09:05 antonmedv

@antonmedv I understand that the host machine would need to also have (node and) zx installed, right?

I first thought of an API like:

ssh("user@host").within((target)=>{
  target.fs.readFile()
  target.cd('./path')
})

But that there is no need to use within, as there is no interference here.

So yes, I much prefer your API.

haikyuu avatar May 03 '23 09:05 haikyuu

No, no need to install zx and node on the host.

antonmedv avatar May 03 '23 10:05 antonmedv

@antonmedv cool. There are tools like https://github.com/adaltas/node-ssh2-fs that enable a similar experience. Curious about your plan to implement it 👀 ?

P.S: I'm very much interested in contributing this feature, as it's very painful to actually work around it: have to use shell syntax [ -d "folder" ] ||, and stdout isn't handled+ it's a pain to navigate folders without whithin ...

haikyuu avatar May 03 '23 10:05 haikyuu

Hello. Is it possible to set stdio = inherit for ssh yet?

StreetStrider avatar Jul 26 '23 14:07 StreetStrider

Hello!

An ability to create interactive console on request could be really useful. I wanted to create the script that would send signal to the nodejs process on the remote host, open ssh tunnel and connect to the interactive nodejs debugger.

dec0dOS avatar Sep 01 '23 02:09 dec0dOS

It would be nice to have stdio for ssh. I think I will revert my script to ssh -T heredoc for the moment.

StreetStrider avatar Sep 23 '23 14:09 StreetStrider

The ssh() func is dropped from zx v8 for now.

Please, use webpod directly.

antonmedv avatar Mar 28 '24 21:03 antonmedv