¿Cómo ejecutar un comando simple arbitrario sobre ssh sin conocer el shell de inicio de sesión del usuario remoto?

26

ssh tiene una característica molesta cuando ejecutas:

ssh user@host cmd and "here's" "one arg"

En lugar de ejecutar eso cmdcon sus argumentos host, concatena eso cmdy argumentos con espacios y ejecuta un shell hostpara interpretar la cadena resultante (supongo que por eso se llama sshy no sexec).

Peor aún, no sabes qué shell se va a usar para interpretar esa cadena, ya que ese shell de inicio de sesión userni siquiera se garantiza que sea Bourne, ya que todavía hay personas que lo usan tcshcomo su shell de inicio de sesión y fishestá en aumento.

¿Hay alguna manera de evitar eso?

Supongamos que tengo un comando como una lista de argumentos almacenados en una bashmatriz, cada uno de los cuales puede contener cualquier secuencia de bytes no nulos, hay alguna manera de tener que ejecutar en hostcomo userde una manera consistente independientemente de la shell de entrada de que useren host(que asumiremos que es una de las principales familias de shell de Unix: Bourne, csh, rc / es, fish)?

Otra suposición razonable que debería poder hacer es que haya un shcomando hostdisponible $PATHque sea compatible con Bourne.

Ejemplo:

cmd=(
  'printf'
  '<%s>\n'
  'arg with $and spaces'
  '' # empty
  $'even\n* * *\nnewlines'
  "and 'single quotes'"
  '!!'
)

Puedo ejecutarlo localmente con ksh/ zsh/ bash/ yashas:

$ "${cmd[@]}"
<arg with $and spaces>
<>
<even
* * *
newlines>
<and 'single quotes'>
<!!>

o

env "${cmd[@]}"

o

xterm -hold -e "${cmd[@]}"
...

¿Cómo iba a ejecutarlo en hostlo usermás ssh?

ssh user@host "${cmd[@]}"

Obviamente no funcionará.

ssh user@host "$(printf ' %q' exec "${cmd[@]}")"

solo funcionaría si el shell de inicio de sesión del usuario remoto fuera el mismo que el shell local (o entiende las citas de la misma manera que printf %qen el shell local lo produce) y se ejecuta en la misma configuración regional.

Stéphane Chazelas
fuente
3
Si el cmdargumento fuera /bin/sh -cque terminaríamos con un shell posix en el 99% de todos los casos, ¿no? Por supuesto, escapar de caracteres especiales es un poco más doloroso de esta manera, pero ¿resolvería el problema inicial?
Bananguin
@Bananguin, no, si ejecuta ssh host sh -c 'some cmd', lo mismo ssh host 'sh -c some cmd'que el shell de inicio de sesión del usuario remoto interpreta esa sh -c some cmdlínea de comando. Necesitamos escribir el comando en la sintaxis correcta para ese shell (y no sabemos cuál es) para que shse llame allí con -cy some cmdargumentos.
Stéphane Chazelas
1
@Otheus, sí, las líneas de comando sh -c 'some cmd'y some cmdse interpretan de la misma manera en todos esos shells. ¿Qué sucede si quiero ejecutar la echo \'línea de comando Bourne en el host remoto? echo command-string | ssh ... /bin/shes una solución que di en mi respuesta, pero eso significa que no puede alimentar datos al stdin de ese comando remoto.
Stéphane Chazelas
1
Parece que una solución más duradera sería un complemento rexec para ssh, al igual que el complemento ftp.
Oteo
1
@myrdd, no, no lo es, necesita espacio o tabulación para separar los argumentos en una línea de comando de shell. Si cmdes así cmd=(echo "foo bar"), la línea de comando de shell pasada sshdebería ser algo así como la línea de comando '' echo '' foo bar ' . The *first* space (the one before echo ) is superflous, but doen't harm. The other one (the ones before ' foo bar ' ) is needed. With '% q ' , we'd pass a ' echo '' foo bar ' ' .
Stéphane Chazelas

Respuestas:

19

No creo que ninguna implementación sshtenga una forma nativa de pasar un comando del cliente al servidor sin involucrar un shell.

Ahora, las cosas pueden ser más fáciles si puede decirle al shell remoto que solo ejecute un intérprete específico (por ejemplo sh, para el que conocemos la sintaxis esperada) y le dé al código que se ejecute por otro medio.

Esa otra media puede ser, por ejemplo, una entrada estándar o una variable de entorno .

Cuando ninguno de los dos se puede utilizar, propongo una tercera solución hacky a continuación.

Usando stdin

Si no necesita alimentar ningún dato al comando remoto, esa es la solución más fácil.

Si sabe que el host remoto tiene un xargscomando que admite la -0opción y el comando no es demasiado grande, puede hacer lo siguiente:

printf '%s\0' "${cmd[@]}" | ssh user@host 'xargs -0 env --'

Esa xargs -0 env --línea de comando se interpreta de la misma manera con todas esas familias de shell. xargslee la lista de argumentos delimitados por nulos en stdin y los pasa como argumentos a env. Eso supone que el primer argumento (el nombre del comando) no contiene =caracteres.

O puede usar shen el host remoto después de haber citado cada elemento utilizando la shsintaxis de cita.

shquote() {
  LC_ALL=C awk -v q=\' '
    BEGIN{
      for (i=1; i<ARGC; i++) {
        gsub(q, q "\\" q q, ARGV[i])
        printf "%s ", q ARGV[i] q
      }
      print ""
    }' "$@"
}
shquote "${cmd[@]}" | ssh user@host sh

Usar variables de entorno

Ahora, si necesita alimentar algunos datos del cliente al stdin del comando remoto, la solución anterior no funcionará.

sshSin embargo, algunas implementaciones de servidor permiten pasar variables de entorno arbitrarias del cliente al servidor. Por ejemplo, muchas implementaciones de openssh en sistemas basados ​​en Debian permiten pasar variables cuyo nombre comienza con LC_.

En esos casos, podría tener una LC_CODEvariable que, por ejemplo, contenga el código entrecomillado sh como se indicó anteriormente y ejecutarla sh -c 'eval "$LC_CODE"'en el host remoto después de haberle dicho a su cliente que pase esa variable (de nuevo, esa es una línea de comandos que se interpreta igual en cada shell):

LC_CODE=$(shquote "${cmd[@]}") ssh -o SendEnv=LC_CODE user@host '
  sh -c '\''eval "$LC_CODE"'\'

Construyendo una línea de comando compatible con todas las familias de shell

Si ninguna de las opciones anteriores es aceptable (porque necesita stdin y sshd no acepta ninguna variable, o porque necesita una solución genérica), deberá preparar una línea de comando para el host remoto que sea compatible con todos Conchas soportadas.

Eso es particularmente complicado porque todos esos shells (Bourne, csh, rc, es, fish) tienen su propia sintaxis diferente y, en particular, diferentes mecanismos de cotización y algunos de ellos tienen limitaciones que son difíciles de solucionar.

Aquí hay una solución que se me ocurrió, la describo más abajo:

#! /usr/bin/perl
my $arg, @ssh, $preamble =
q{printf '%.0s' "'\";set x=\! b=\\\\;setenv n "\
";set q=\';printf %.0s "\""'"';q='''';n=``()echo;x=!;b='\'
printf '%.0s' '\'';set b \\\\;set x !;set -x n \n;set q \'
printf '%.0s' '\'' #'"\"'";export n;x=!;b=\\\\;IFS=.;set `echo;echo \.`;n=$1 IFS= q=\'
};

@ssh = ('ssh');
while ($arg = shift @ARGV and $arg ne '--') {
  push @ssh, $arg;
}

if (@ARGV) {
  for (@ARGV) {
    s/'/'\$q\$b\$q\$q'/g;
    s/\n/'\$q'\$n'\$q'/g;
    s/!/'\$x'/g;
    s/\\/'\$b'/g;
    $_ = "\$q'$_'\$q";
  }
  push @ssh, "${preamble}exec sh -c 'IFS=;exec '" . join "' '", @ARGV;
}

exec @ssh;

Ese es un perlguión envolvente ssh. Yo lo llamo sexec. Lo llamas así:

sexec [ssh-options] user@host -- cmd and its args

entonces en tu ejemplo:

sexec user@host -- "${cmd[@]}"

Y el contenedor se convierte cmd and its argsen una línea de comando que todos los shells terminan interpretando como llamadas cmdcon sus argumentos (independientemente de su contenido).

Limitaciones:

  • El preámbulo y la forma en que se cita el comando significa que la línea de comando remota termina siendo significativamente más grande, lo que significa que el límite en el tamaño máximo de una línea de comando se alcanzará antes.
  • Solo lo he probado con: Bourne shell (de heirloom toolchest), dash, bash, zsh, mksh, lksh, yash, ksh93, rc, es, akanga, csh, tcsh, fish como se encuentra en un sistema Debian reciente y / bin / sh, / usr / bin / ksh, / bin / csh y / usr / xpg4 / bin / sh en Solaris 10.
  • Si yashes el shell de inicio de sesión remoto, no puede pasar un comando cuyos argumentos contengan caracteres no válidos, pero esa es una limitación en el sentido de yashque no puede evitarlo de todos modos.
  • Algunos shells como csh o bash leen algunos archivos de inicio cuando se invocan a través de ssh. Asumimos que esos no cambian el comportamiento dramáticamente para que el preámbulo aún funcione.
  • además sh, también supone que el sistema remoto tiene el printfcomando.

Para comprender cómo funciona, necesita saber cómo funciona la cita en los diferentes shells:

  • Bourne: '...'son citas fuertes sin carácter especial. "..."son comillas débiles donde "se puede escapar con una barra invertida.
  • csh. Igual que Bourne, excepto que "no se puede escapar por dentro "...". También se debe ingresar un carácter de nueva línea con una barra diagonal inversa. Y !causa problemas incluso dentro de comillas simples.
  • rc. Las únicas citas son '...'(fuertes). Una comilla simple dentro de comillas simples se ingresa como ''(like '...''...'). Las comillas dobles o las barras invertidas no son especiales.
  • es. Igual que rc, excepto que las comillas externas, la barra diagonal inversa puede escapar de una comilla simple.
  • fish: igual que Bourne, excepto que la barra invertida se escapa 'dentro '...'.

Con todas esas restricciones, es fácil ver que uno no puede citar de manera confiable los argumentos de la línea de comandos para que funcione con todos los shells.

Usando comillas simples como en:

'foo' 'bar'

funciona en todos pero:

'echo' 'It'\''s'

no funcionaría en rc.

'echo' 'foo
bar'

no funcionaría en csh.

'echo' 'foo\'

no funcionaría en fish.

Sin embargo, deberíamos poder solucionar la mayoría de esos problemas si logramos almacenar esos caracteres problemáticos en variables, como la barra diagonal inversa $b, la comilla simple $q, la nueva línea $n(y !en la $xexpansión del historial de csh) de una manera independiente del shell.

'echo' 'It'$q's'
'echo' 'foo'$b

funcionaría en todos los depósitos. Sin embargo, eso todavía no funcionaría para Newline csh. Si $ncontiene nueva línea, en csh, debe escribirlo $n:qpara que se expanda a una nueva línea y eso no funcionará para otros shells. Entonces, lo que terminamos haciendo aquí es llamar shy shexpandirlos $n. Eso también significa tener que hacer dos niveles de cotización, uno para el shell de inicio de sesión remoto y otro para sh.

El $preambleen ese código es la parte más complicada. Se hace uso de las distintas reglas diferentes de cotización en todas las cáscaras tenían algunas secciones del código interpretado por una sola de las conchas (mientras se está comentada para los demás) cada uno de los cuales acaba de definir los $b, $q, $n, $xvariables para su respectiva concha.

Aquí está el código de shell que sería interpretado por el shell de inicio de sesión del usuario remoto hostpara su ejemplo:

printf '%.0s' "'\";set x=\! b=\\;setenv n "\
";set q=\';printf %.0s "\""'"';q='''';n=``()echo;x=!;b='\'
printf '%.0s' '\'';set b \\;set x !;set -x n \n;set q \'
printf '%.0s' '\'' #'"\"'";export n;x=!;b=\\;IFS=.;set `echo;echo \.`;n=$1 IFS= q=\'
exec sh -c 'IFS=;exec '$q'printf'$q' '$q'<%s>'$b'n'$q' '$q'arg with $and spaces'$q' '$q''$q' '$q'even'$q'$n'$q'* * *'$q'$n'$q'newlines'$q' '$q'and '$q$b$q$q'single quotes'$q$b$q$q''$q' '$q''$x''$x''$q

Ese código termina ejecutando el mismo comando cuando lo interpretan cualquiera de los shells compatibles.

Stéphane Chazelas
fuente
1
El protocolo SSH ( RFC 4254 §6.5 ) define un comando remoto como una cadena. Depende del servidor decidir cómo interpretar esa cadena. En los sistemas Unix, la interpretación normal es pasar la cadena al shell de inicio de sesión del usuario. Para una cuenta restringida, eso podría ser algo como rssh o rush que no acepta comandos arbitrarios. Incluso podría haber un comando forzado en la cuenta o en la clave que hace que se ignore la cadena de comandos enviada por el cliente.
Gilles 'SO- deja de ser malvado'
1
@Gilles, gracias por la referencia RFC. Sí, la suposición para estas preguntas y respuestas es que el shell de inicio de sesión del usuario remoto es utilizable (ya que puedo ejecutar el comando remoto que quiero ejecutar) y una de las principales familias de shell en los sistemas POSIX. No estoy interesado en shells restringidos o no shells o comandos forzados o cualquier cosa que no me permita ejecutar ese comando remoto de todos modos.
Stéphane Chazelas
1
Se puede encontrar una referencia útil sobre las principales diferencias en la sintaxis entre algunos shells comunes en Hyperpolyglot .
lcd047
0

tl; dr

ssh USER@HOST -p PORT $(printf "%q" "cmd") $(printf "%q" "arg1") \
    $(printf "%q" "arg2")

Para una solución más elaborada, lea los comentarios e inspeccione la otra respuesta .

descripción

Bueno, mi solución no funcionará con non- bashshells. Pero suponiendo que esté bashen el otro extremo, las cosas se vuelven más simples. Mi idea es reutilizar printf "%q"para escapar. También en general, es más legible tener un script en el otro extremo, que acepte argumentos. Pero si el comando es corto, probablemente esté bien alinearlo. Aquí hay algunas funciones de ejemplo para usar en scripts:

local.sh:

#!/usr/bin/env bash
set -eu

ssh_run() {
    local user_host_port=($(echo "$1" | tr '@:' ' '))
    local user=${user_host_port[0]}
    local host=${user_host_port[1]}
    local port=${user_host_port[2]-22}
    shift 1
    local cmd=("$@")
    local a qcmd=()
    for a in ${cmd[@]+"${cmd[@]}"}; do
        qcmd+=("$(printf "%q" "$a")")
    done
    ssh "$user"@"$host" -p "$port" ${qcmd[@]+"${qcmd[@]}"}
}

ssh_cmd() {
    local user_host_port=$1
    local cmd=$2
    shift 2
    local args=("$@")
    ssh_run "$user_host_port" bash -lc "$cmd" - ${args[@]+"${args[@]}"}
}

ssh_run USER@HOST ./remote.sh "1  '  \"  2" '3  '\''  "  4'
ssh_cmd USER@HOST:22 "for a; do echo \"'\$a'\"; done" "1  '  \"  2" '3  '\''  "  4'
ssh_cmd USER@HOST:22 'for a; do echo "$a"; done' '1  "2' "3'  4"

remote.sh:

#!/usr/bin/env bash
set -eu
for a; do
    echo "'$a'"
done

La salida:

'1  '  "  2'
'3  '  "  4'
'1  '  "  2'
'3  '  "  4'
1  "2
3'  4

Alternativamente, puede hacer printfel trabajo usted mismo, si sabe lo que está haciendo:

ssh USER@HOST ./1.sh '"1  '\''  \"  2"' '"3  '\''  \"  4"'
x-yuri
fuente
1
Eso supone que el shell de inicio de sesión del usuario remoto es bash (como cita printf% q de bash de manera bash) y que bashestará disponible en la máquina remota. También hay algunos problemas con las comillas faltantes que podrían causar problemas con espacios en blanco y comodines.
Stéphane Chazelas
@ StéphaneChazelas De hecho, mi solución probablemente solo esté dirigida a bashproyectiles. Pero con suerte la gente lo encontrará útil. Sin embargo, traté de abordar los otros problemas. Siéntase libre de decirme si hay algo que me falta además de la bashcosa.
x-yuri
1
Tenga en cuenta que todavía no funciona con el comando de muestra en la pregunta ( ssh_run user@host "${cmd[@]}"). Aún te faltan algunas citas.
Stéphane Chazelas
1
Eso es mejor. Tenga en cuenta que la salida de bash printf %qno es segura de usar en una configuración regional diferente (y también es bastante defectuosa; por ejemplo, en configuraciones regionales que usan el juego de caracteres BIG5, cita (4.3.48) εcomo α`!). Para eso, lo mejor es citar todo y con comillas simples solo como con el shquote()en mi respuesta.
Stéphane Chazelas