Come eseguire un semplice comando arbitrario su ssh senza conoscere la shell di accesso dell'utente remoto?


26

ssh ha una caratteristica fastidiosa quando si esegue:

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

Invece di eseguirlo cmdcon i suoi argomenti host, concatena quello cmde gli argomenti con gli spazi e esegue una shell hostper interpretare la stringa risultante (immagino che sia per questo che viene chiamato sshe non sexec).

Peggio ancora, non sai quale shell verrà utilizzata per interpretare quella stringa in quanto è la shell di login di usercui non è nemmeno garantita la presenza di Bourne come ci sono ancora persone che usano tcshcome shell di login ed fishè in aumento.

C'è un modo per aggirare questo?

Supponiamo che io abbia un comando come un elenco di argomenti archiviati in un basharray, ognuno dei quali può contenere qualsiasi sequenza di byte non nulli, esiste un modo per eseguirlo su hostcome userin modo coerente indipendentemente dalla shell di login di quello usersu host(che supponiamo sia una delle principali famiglie di shell Unix: Bourne, csh, rc / es, fish)?

Un altro presupposto ragionevole che dovrei essere in grado di formulare è che è disponibile un shcomando hostin $PATHquanto compatibile con Bourne.

Esempio:

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

Posso eseguirlo localmente con ksh/ zsh/ bash/ yashcome:

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

o

env "${cmd[@]}"

o

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

Come l'avrei eseguito hostcome usersopra ssh?

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

ovviamente non funzionerà.

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

funzionerebbe solo se la shell di login dell'utente remoto fosse la stessa della shell locale (o capisse che la quotazione nello stesso modo printf %qin cui la shell locale la produce) e funzionasse nella stessa locale.


3
Se l' cmdargomento fosse /bin/sh -cfiniremmo con una shell posix nel 99% di tutti i casi, no? Ovviamente sfuggire a personaggi speciali è un po 'più doloroso in questo modo, ma risolverebbe il problema iniziale?
Bananguin,

@Bananguin, no se si esegue ssh host sh -c 'some cmd', uguale a ssh host 'sh -c some cmd', che ha la shell di accesso dell'utente remoto interpretare quella sh -c some cmdriga di comando. Dobbiamo scrivere il comando nella sintassi corretta per quella shell (e non sappiamo quale sia) in modo che shpossa essere chiamato lì con -ce some cmdargomenti.
Stéphane Chazelas,

1
@Otheus, sì, le righe di comando sh -c 'some cmd'e some cmdvengono interpretate allo stesso modo in tutte quelle shell. E se volessi eseguire la echo \'riga di comando Bourne sull'host remoto? echo command-string | ssh ... /bin/shè una soluzione che ho dato nella mia risposta, ma ciò significa che non è possibile inviare dati allo stdin di quel comando remoto.
Stéphane Chazelas,

1
Sembra una soluzione più duratura sarebbe un plug-in rexec per ssh, come il plug-in ftp.
Otheus,

1
@myrdd, no non lo è, è necessario spazio o tab per separare gli argomenti in una riga di comando della shell. Se lo cmdè cmd=(echo "foo bar"), la riga di comando della shell passata sshdovrebbe essere qualcosa come `` 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,

Risposte:


19

Non credo che nessuna implementazione di sshabbia un modo nativo per passare un comando dal client al server senza coinvolgere una shell.

Ora, le cose possono diventare più facili se puoi dire alla shell remota di eseguire solo un interprete specifico (come sh, per il quale conosciamo la sintassi attesa) e dare al codice l'esecuzione con un altro mezzo.

L'altra media può essere, ad esempio , l'input standard o una variabile di ambiente .

Quando nessuno dei due può essere utilizzato, propongo di seguito una terza soluzione caotica.

Usando lo stdin

Se non è necessario fornire alcun dato al comando remoto, questa è la soluzione più semplice.

Se sai che l'host remoto ha un xargscomando che supporta l' -0opzione e il comando non è troppo grande, puoi fare:

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

Quella xargs -0 env --riga di comando viene interpretata allo stesso modo con tutte quelle famiglie di shell. xargslegge l'elenco di argomenti delimitato da null su stdin e li passa come argomenti aenv . Ciò presuppone che il primo argomento (il nome del comando) non contenga =caratteri.

Oppure è possibile utilizzare shsull'host remoto dopo aver quotato ciascun elemento utilizzando la shsintassi di quotazione.

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

Utilizzando le variabili di ambiente

Ora, se è necessario alimentare alcuni dati dal client allo stdin del comando remoto, la soluzione sopra non funzionerà.

Alcune sshdistribuzioni di server consentono tuttavia il passaggio di variabili di ambiente arbitrarie dal client al server. Ad esempio, molte distribuzioni openssh su sistemi basati su Debian consentono il passaggio di variabili il cui nome inizia conLC_ .

In quei casi potresti avere una LC_CODEvariabile per esempio contenente il codice shquot sh come sopra ed eseguire sh -c 'eval "$LC_CODE"'sull'host remoto dopo aver detto al tuo client di passare quella variabile (di nuovo, quella è una riga di comando che viene interpretata allo stesso modo in ogni shell):

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

Creazione di una riga di comando compatibile con tutte le famiglie di shell

Se nessuna delle opzioni sopra è accettabile (perché hai bisogno di stdin e sshd non accetta alcuna variabile o perché hai bisogno di una soluzione generica), dovrai preparare una riga di comando per l'host remoto compatibile con tutti shell supportate.

Ciò è particolarmente complicato perché tutte quelle conchiglie (Bourne, csh, rc, es, fish) hanno una propria sintassi diversa, e in particolare diversi meccanismi di quotazione e alcuni di essi hanno limitazioni che sono difficili da aggirare.

Ecco una soluzione che mi è venuta in mente, la descrivo più in basso:

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

Questo è uno perlscript wrapper in giro ssh. Io lo chiamo sexec. Lo chiami come:

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

quindi nel tuo esempio:

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

E il wrapper si trasforma cmd and its argsin una riga di comando che tutte le shell finiscono per interpretare come una chiamata cmdcon i suoi arg (indipendentemente dal loro contenuto).

limitazioni:

  • Il preambolo e il modo in cui viene citato il comando indicano che la riga di comando remota finisce per essere significativamente più grande, il che significa che il limite sulla dimensione massima di una riga di comando verrà raggiunto prima.
  • L'ho provato solo con: Bourne shell (dal cimelio di cimelio), trattino, bash, zsh, mksh, lksh, yash, ksh93, rc, es, akanga, csh, tcsh, fish come si trova su un recente sistema Debian e / bin / sh, / usr / bin / ksh, / bin / csh e / usr / xpg4 / bin / sh su Solaris 10.
  • Se yashè la shell di accesso remoto, non è possibile passare un comando i cui argomenti contengono caratteri non validi, ma questa è una limitazione in yashquanto non è possibile aggirare comunque.
  • Alcune shell come csh o bash leggono alcuni file di avvio quando invocate su ssh. Partiamo dal presupposto che quelli non cambiano drasticamente il comportamento in modo che il preambolo funzioni ancora.
  • accanto sh, essa assume anche il sistema remoto ha il printfcomando.

Per capire come funziona, devi sapere come funziona la quotazione nelle diverse shell:

  • Bourne: '...'sono virgolette forti senza caratteri speciali. "..."sono virgolette deboli dove "può essere evitato con la barra rovesciata.
  • csh. Lo stesso di Bourne, tranne per il fatto che "non può essere evitato all'interno "...". Inoltre, è necessario inserire un carattere di nuova riga con il prefisso con una barra rovesciata. E !causa problemi anche all'interno di virgolette singole.
  • rc. Le uniche virgolette sono '...'(forti). Una virgoletta singola tra virgolette singole viene inserita come ''(come '...''...'). Le virgolette doppie o le barre rovesciate non sono speciali.
  • es. Come in rc, ad eccezione delle virgolette esterne, la barra rovesciata può sfuggire a una singola virgoletta.
  • fish: uguale a Bourne, tranne per il fatto che la barra rovesciata fuoriesce 'all'interno '...'.

Con tutte queste contraddizioni, è facile capire che non è possibile citare in modo affidabile argomenti della riga di comando in modo che funzioni con tutte le shell.

Utilizzando virgolette singole come in:

'foo' 'bar'

funziona in tutto ma:

'echo' 'It'\''s'

non funzionerebbe rc.

'echo' 'foo
bar'

non funzionerebbe csh.

'echo' 'foo\'

non funzionerebbe fish.

Tuttavia dovremmo essere in grado di aggirare la maggior parte di questi problemi se riusciamo a memorizzare quei caratteri problematici in variabili, come backslash in $b, single quote in $q, newline in $n(e !in $xper l'espansione della storia di csh) in modo indipendente dalla shell.

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

funzionerebbe in tutte le conchiglie. cshTuttavia, ciò non funzionerebbe ancora per Newline . Se $ncontiene newline, in csh, devi scriverlo in modo che $n:qsi espanda a una nuova riga e che non funzionerà per altre shell. Quindi, ciò che finiamo per fare invece qui è chiamare she shampliarli $n. Ciò significa anche dover fare due livelli di quotazione, uno per la shell di accesso remoto e uno persh .

Il $preamblecodice in quel codice è la parte più difficile. Si fa uso di varie regole di quoting differenti in tutte le shell di avere alcune sezioni del codice interpretato da uno solo dei gusci (mentre è commentata per gli altri) ciascuno dei quali definire soltanto quelli $b, $q, $n, $xvariabili per le rispettive coperture.

Ecco il codice della shell che verrebbe interpretato dalla shell di accesso dell'utente remoto su hostper il tuo esempio:

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

Quel codice finisce per eseguire lo stesso comando quando interpretato da una qualsiasi delle shell supportate.


1
Il protocollo SSH ( RFC 4254 §6.5 ) definisce un comando remoto come una stringa. Spetta al server decidere come interpretare quella stringa. Sui sistemi Unix, l'interpretazione normale è di passare la stringa alla shell di login dell'utente. Per un account limitato, potrebbe essere qualcosa come rssh o rush che non accetta comandi arbitrari. Potrebbe esserci anche un comando forzato sull'account o sulla chiave che fa ignorare la stringa di comando inviata dal client.
Gilles 'SO- smetti di essere malvagio'

1
@Gilles, grazie per il riferimento RFC. Sì, il presupposto per queste domande e risposte è che la shell di accesso dell'utente remoto sia utilizzabile (come in posso eseguire quel comando remoto che voglio eseguire) e una delle principali famiglie di shell sui sistemi POSIX. Non mi interessano shell limitate o non shell o comandi di forzamento o qualsiasi cosa che non mi permetta comunque di eseguire quel comando remoto.
Stéphane Chazelas,

1
Un riferimento utile sulle principali differenze nella sintassi tra alcune shell comuni è disponibile su Hyperpolyglot .
lcd047,

0

tl; dr

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

Per una soluzione più elaborata, leggi i commenti e controlla l'altra risposta .

descrizione

Bene, la mia soluzione non funzionerà con non bashshell. Ma supponendo che sia bashdall'altra parte, le cose diventano più semplici. La mia idea è di riutilizzare printf "%q"per scappare. Inoltre, in genere, è più leggibile avere uno script sull'altra estremità, che accetta argomenti. Ma se il comando è breve, probabilmente va bene incorporarlo. Ecco alcune funzioni di esempio da utilizzare negli script:

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

L'output:

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

In alternativa, puoi fare printfil lavoro da solo, se sai cosa stai facendo:

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

1
Ciò presuppone che la shell di accesso dell'utente remoto sia bash (come citazioni printf% q di bash in modo bash) e che bashsia disponibile sul computer remoto. Ci sono anche alcuni problemi con le virgolette mancanti che potrebbero causare problemi con spazi bianchi e caratteri jolly.
Stéphane Chazelas,

@ StéphaneChazelas In effetti, la mia soluzione è probabilmente indirizzata solo alle bashshell. Ma si spera che la gente lo troverà utile. Ho provato a risolvere gli altri problemi però. Sentiti libero di dirmi se c'è qualcosa che mi manca oltre alla bashcosa.
x-yuri,

1
Si noti che continua a non funzionare con il comando di esempio nella domanda ( ssh_run user@host "${cmd[@]}"). Hai ancora delle citazioni mancanti.
Stéphane Chazelas,

1
Va meglio. Si noti che l'output di bash printf %qnon è sicuro da usare in una locale diversa (ed è anche piuttosto errato; ad esempio in locali che usano il set di caratteri BIG5, esso (4.3.48) cita εcome α`!). Per questo, la cosa migliore è citare tutto e solo con virgolette come shquote()nella mia risposta.
Stéphane Chazelas,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.