Como executar um comando simples e arbitrário sobre o ssh sem conhecer o shell de login do usuário remoto?


26

ssh tem um recurso irritante quando você executa:

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

Em vez de executar isso cmdcom seus argumentos host, concatena isso cmde argumentos com espaços e executa um shell hostpara interpretar a sequência resultante (acho que é por isso que é chamada sshe não sexec).

Pior, você não sabe qual shell será usado para interpretar essa string, pois esse é o shell de login userque nem é garantido como Bourne, pois ainda há pessoas usando tcsho shell de login e fishestá em ascensão.

Existe uma maneira de contornar isso?

Suponhamos que tem um comando como uma lista de argumentos armazenados em uma bashmatriz, cada um dos quais pode conter qualquer sequência de bytes não nulos, existe alguma maneira para que seja executado sobre hostcomo userde uma forma consistente independentemente da concha de login do que userna host(que assumiremos que é uma das principais famílias de shell do Unix: Bourne, csh, rc / es, fish)?

Outra suposição razoável que devo fazer é que exista um shcomando hostdisponível $PATHque seja compatível com Bourne.

Exemplo:

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

Posso executá-lo localmente com ksh/ zsh/ bash/ yashas:

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

ou

env "${cmd[@]}"

ou

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

Como eu iria executá-lo em hosttão userlongo ssh?

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

obviamente não vai funcionar.

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

funcionaria apenas se o shell de login do usuário remoto fosse o mesmo que o shell local (ou entendesse a citação da mesma maneira que printf %qno shell local a produz) e seja executado no mesmo local.


3
Se o cmdargumento fosse /bin/sh -cque acabaríamos com um shell posix em 99% de todos os casos, não é? É claro que escapar de caracteres especiais é um pouco mais doloroso, mas isso resolveria o problema inicial?
Bananguin 25/05

@Banguanguin, não, se você executar o ssh host sh -c 'some cmd'mesmo ssh host 'sh -c some cmd'que o shell de logon do usuário remoto interpreta essa sh -c some cmdlinha de comando. Precisamos escrever o comando na sintaxe correta para que a Shell (e não sabemos o que é) para que shser chamado por lá com -ce some cmdargumentos.
Stéphane Chazelas

1
@Otheus, sim, as linhas de comando sh -c 'some cmd'e some cmdpassam a ser interpretadas da mesma forma em todos esses shells. Agora, e se eu quiser executar a echo \'linha de comando Bourne no host remoto? echo command-string | ssh ... /bin/shé uma solução que eu dei na minha resposta, mas isso significa que você não pode alimentar dados para o stdin desse comando remoto.
Stéphane Chazelas

1
Parece que uma solução mais duradoura seria um plugin rexec para ssh, ala o plugin ftp.
Otheus

1
@myrdd, não, não é, você precisa de espaço ou tab para separar argumentos em uma linha de comando do shell. Se cmdfor cmd=(echo "foo bar"), a linha de comando do shell transmitida sshdeve ser algo como a linha 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

Respostas:


19

Não acho que nenhuma implementação sshtenha uma maneira nativa de passar um comando do cliente para o servidor sem envolver um shell.

Agora, as coisas podem ficar mais fáceis se você puder dizer ao shell remoto para executar apenas um intérprete específico (como sh, para o qual conhecemos a sintaxe esperada) e fornecer o código para executar por outro meio.

Essa outra média pode ser, por exemplo , entrada padrão ou uma variável de ambiente .

Quando nenhum deles pode ser usado, proponho uma terceira solução hacky abaixo.

Usando stdin

Se você não precisar alimentar nenhum dado com o comando remoto, essa é a solução mais fácil.

Se você sabe que o host remoto possui um xargscomando que suporta a -0opção e o comando não é muito grande, você pode:

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

Essa xargs -0 env --linha de comando é interpretada da mesma forma com todas essas famílias de shell. xargslê a lista de argumentos delimitados por nulo no stdin e os passa como argumentos para env. Isso pressupõe que o primeiro argumento (o nome do comando) não contenha =caracteres.

Ou você pode usar shno host remoto depois de citar cada elemento usando a shsintaxe de citação.

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

Usando variáveis ​​de ambiente

Agora, se você precisar alimentar alguns dados do cliente para o stdin do comando remoto, a solução acima não funcionará.

No sshentanto, algumas implantações de servidor permitem a passagem de variáveis ​​de ambiente arbitrárias do cliente para o servidor. Por exemplo, muitas implantações openssh em sistemas baseados no Debian permitem passar variáveis ​​cujo nome começa com LC_.

Nesses casos, você pode ter uma LC_CODEvariável, por exemplo, contendo o código shquoted sh , como descrito acima, e executar sh -c 'eval "$LC_CODE"'no host remoto depois de solicitar ao seu cliente que passe essa variável (novamente, essa é uma linha de comando que é interpretada da mesma forma em todos os shell):

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

Construindo uma linha de comando compatível com todas as famílias de shell

Se nenhuma das opções acima for aceitável (porque você precisa de stdin e sshd não aceita nenhuma variável ou porque precisa de uma solução genérica), precisará preparar uma linha de comando para o host remoto compatível com todos os conchas suportadas.

Isso é particularmente complicado porque todas essas conchas (Bourne, csh, rc, es, fish) têm sua própria sintaxe diferente e, em particular, diferentes mecanismos de citação e algumas delas têm limitações difíceis de contornar.

Aqui está uma solução que eu criei, eu a descrevo mais abaixo:

#! /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;

Esse é um perlscript de wrapper ssh. Eu chamo isso sexec. Você chama assim:

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

então no seu exemplo:

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

E o wrapper se transforma cmd and its argsem uma linha de comando que todos os shells acabam interpretando como chamando cmdcom seus argumentos (independentemente do conteúdo).

Limitações:

  • O preâmbulo e a maneira como o comando é citado significa que a linha de comando remota acaba sendo significativamente maior, o que significa que o limite do tamanho máximo de uma linha de comando será atingido mais rapidamente.
  • Eu só testei com: shell Bourne (do heirloom toolchest), dash, bash, zsh, mksh, lksh, yash, ksh93, rc, es, akanga, csh, tcsh, fish, encontrado em um sistema Debian recente e / bin / sh, / usr / bin / ksh, / bin / csh e / usr / xpg4 / bin / sh no Solaris 10.
  • Se yashfor o shell de logon remoto, você não pode passar um comando cujos argumentos contenham caracteres inválidos, mas isso é uma limitação, yashpois você não pode contornar isso de qualquer maneira.
  • Alguns shells como csh ou bash leem alguns arquivos de inicialização quando invocados pelo ssh. Assumimos que eles não mudam drasticamente o comportamento para que o preâmbulo ainda funcione.
  • ao lado sh, também assume que o sistema remoto possui o printfcomando.

Para entender como ele funciona, você precisa saber como a citação funciona nos diferentes shells:

  • Bourne: '...'são citações fortes, sem caráter especial. "..."são aspas fracas onde "podem ser escapadas com barra invertida.
  • csh. O mesmo que Bourne, exceto que "não pode ser escapado por dentro "...". Além disso, um caractere de nova linha deve ser inserido como prefixo com uma barra invertida. E !causa problemas mesmo dentro de aspas simples.
  • rc. As únicas citações são '...'(forte). Uma citação única entre aspas simples é inserida como ''(como '...''...'). Aspas duplas ou barras invertidas não são especiais.
  • es. O mesmo que rc, exceto que aspas externas, a barra invertida pode escapar de uma única aspas.
  • fish: o mesmo que Bourne, exceto que a barra invertida escapa por 'dentro '...'.

Com todas essas restrições, é fácil ver que não é possível citar argumentos de linha de comando de maneira confiável, para que funcione com todos os shells.

Usando aspas simples como em:

'foo' 'bar'

funciona em todos, exceto:

'echo' 'It'\''s'

não funcionaria rc.

'echo' 'foo
bar'

não funcionaria csh.

'echo' 'foo\'

não funcionaria fish.

No entanto, deve ser capaz de resolver a maioria desses problemas se conseguirmos armazenar esses personagens problemáticos em variáveis, como barra invertida no $b, aspas simples em $q, nova linha na $n(e !no $xde expansão história CSH) de forma independente shell.

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

funcionaria em todas as conchas. No entanto, isso ainda não funcionaria para a nova linha csh. Se $ncontiver nova linha, em csh, você deverá escrevê- $n:qla para expandir para uma nova linha e isso não funcionará para outros shells. Então, o que acabamos fazendo aqui é chamar she shexpandi-los $n. Isso também significa ter que fazer dois níveis de citação, um para o shell de login remoto e outro para sh.

O $preamblecódigo é a parte mais complicada. Ele faz uso das várias regras citando diferentes em todas as conchas de ter algumas seções do código interpretado por apenas uma das conchas (enquanto ele está comentado para os outros) cada um dos quais apenas definindo os $b, $q, $n, $xvariáveis para seus respectivos shell.

Aqui está o código do shell que seria interpretado pelo shell de login do usuário remoto no hostseu exemplo:

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

Esse código acaba executando o mesmo comando quando interpretado por qualquer um dos shells suportados.


1
O protocolo SSH ( RFC 4254 §6.5 ) define um comando remoto como uma string. Cabe ao servidor decidir como interpretar essa sequência. Nos sistemas Unix, a interpretação normal é passar a string para o shell de login do usuário. Para uma conta restrita, isso pode ser algo como rssh ou rush que não aceita comandos arbitrários. Pode até haver um comando forçado na conta ou na chave que faz com que a cadeia de comandos enviada pelo cliente seja ignorada.
Gilles 'SO- stop be evil'

1
@Gilles, obrigado pela referência RFC. Sim, a suposição para estas perguntas e respostas é que o shell de login do usuário remoto é utilizável (como em eu posso executar o comando remoto que desejo executar) e uma das principais famílias de shell nos sistemas POSIX. Não estou interessado em shells restritos ou não-shells ou comandos de força ou qualquer coisa que não me permita executar esse comando remoto de qualquer maneira.
Stéphane Chazelas

1
Uma referência útil sobre as principais diferenças de sintaxe entre alguns shells comuns pode ser encontrada em Hyperpolyglot .
Lcd047 26/05

0

tl; dr

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

Para uma solução mais elaborada, leia os comentários e inspecione a outra resposta .

descrição

Bem, minha solução não funcionará com não- bashconchas. Mas, assumindo que está bashdo outro lado, as coisas ficam mais simples. Minha idéia é reutilizar printf "%q"para escapar. Também geralmente é mais legível ter um script do outro lado, que aceite argumentos. Mas se o comando for curto, provavelmente não há problema em incorporá-lo. Aqui estão alguns exemplos de funções para usar em 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

A saída:

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

Como alternativa, você mesmo pode fazer printfo trabalho, se souber o que está fazendo:

ssh USER@HOST ./1.sh '"1  '\''  \"  2"' '"3  '\''  \"  4"'

1
Isso pressupõe que o shell de login do usuário remoto seja bash (como printf% q do bash cita de forma bash) e bashesteja disponível na máquina remota. Também existem alguns problemas com aspas ausentes, o que causaria problemas com espaços em branco e curingas.
Stéphane Chazelas

@ StéphaneChazelas De fato, minha solução provavelmente está direcionada apenas a bashconchas. Mas espero que as pessoas achem isso útil. Eu tentei resolver os outros problemas. Sinta-se à vontade para me dizer se falta algo além da bashcoisa.
x-yuri

1
Observe que ele ainda não funciona com o comando de amostra na pergunta ( ssh_run user@host "${cmd[@]}"). Você ainda tem algumas aspas ausentes.
Stéphane Chazelas

1
Isso é melhor. Observe que a saída do bash's printf %qnão é segura para uso em um código de idioma diferente (e também é bastante problemático; por exemplo, em códigos de idioma usando o conjunto de caracteres BIG5, ele (4.3.48) cita εcomo α`!). Para isso, o melhor é citar tudo e com aspas simples, como shquote()na minha resposta.
Stéphane Chazelas
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.