Comment exécuter une commande simple arbitraire sur ssh sans connaître le shell de connexion de l'utilisateur distant?

26

ssh a une fonctionnalité ennuyeuse en ce que lorsque vous exécutez:

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

Au lieu d'exécuter cela cmdavec ses arguments host, il concatène cela cmdet les arguments avec des espaces et exécute un shell hostpour interpréter la chaîne résultante (je suppose que c'est pourquoi elle est appelée sshet non sexec).

Pire, vous ne savez pas quel shell va être utilisé pour interpréter cette chaîne car c'est le shell de connexion userqui n'est même pas garanti d'être Bourne comme il y a toujours des gens qui utilisent tcshcomme shell de connexion et qui fishsont en augmentation.

Y a-t-il un moyen de contourner cela?

Supposons que j'ai une commande sous la forme d'une liste d'arguments stockés dans un bashtableau, chacun pouvant contenir n'importe quelle séquence d'octets non nuls, existe-t-il un moyen de l'exécuter de hostmanière usercohérente, quel que soit le shell de connexion de celui-ci usersur host(que nous supposerons être l'une des principales familles de shell Unix: Bourne, csh, rc / es, fish)?

Une autre hypothèse raisonnable que je devrais être en mesure de faire est qu'il y ait une shcommande hostdisponible $PATHqui est compatible avec Bourne.

Exemple:

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

Je peux l'exécuter localement avec ksh/ zsh/ bash/ yashcomme:

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

ou

env "${cmd[@]}"

ou

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

Comment pourrais-je l'exécuter hostcomme userplus ssh?

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

ne fonctionnera évidemment pas.

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

ne fonctionnerait que si le shell de connexion de l'utilisateur distant était le même que le shell local (ou comprend la citation de la même manière que printf %qdans le shell local le produit) et s'exécute dans les mêmes paramètres régionaux.

Stéphane Chazelas
la source
3
Si l' cmdargument était que /bin/sh -cnous nous retrouverions avec un shell posix dans 99% des cas, n'est-ce pas? Bien sûr, échapper des caractères spéciaux est un peu plus douloureux de cette façon, mais cela résoudrait-il le problème initial?
Bananguin
@Bananguin, non si vous exécutez ssh host sh -c 'some cmd', de la même manière ssh host 'sh -c some cmd'que le shell de connexion de l'utilisateur distant interprète cette sh -c some cmdligne de commande. Nous devons écrire la commande dans la syntaxe correcte pour ce shell (et nous ne savons pas de quoi il s'agit) afin qu'elle shsoit appelée là-bas avec -cet some cmdarguments.
Stéphane Chazelas
1
@Otheus, oui, les lignes de commande sh -c 'some cmd'et some cmdsont interprétées de la même manière dans tous ces shells. Maintenant, que faire si je veux exécuter la echo \'ligne de commande Bourne sur l'hôte distant? echo command-string | ssh ... /bin/shest une solution que j'ai donnée dans ma réponse, mais cela signifie que vous ne pouvez pas fournir de données au stdin de cette commande à distance.
Stéphane Chazelas
1
Cela ressemble à une solution plus durable serait un plugin rexec pour ssh, ala le plugin ftp.
Otheus
1
@myrdd, non ce n'est pas le cas, vous avez besoin d'espace ou de tabulation pour séparer les arguments dans une ligne de commande du shell. Si cmdc'est le cas cmd=(echo "foo bar"), la ligne de commande du shell passée à sshdevrait être quelque chose comme `` 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'` ligne de commande.
Stéphane Chazelas

Réponses:

19

Je ne pense pas qu'une implémentation de sshait une manière native de passer une commande du client au serveur sans impliquer un shell.

Maintenant, les choses peuvent devenir plus faciles si vous pouvez dire au shell distant d'exécuter uniquement un interpréteur spécifique (comme sh, pour lequel nous connaissons la syntaxe attendue) et de donner le code à exécuter par un autre moyen.

Cet autre moyen peut être par exemple une entrée standard ou une variable d'environnement .

Lorsque ni l'un ni l'autre ne peut être utilisé, je propose une troisième solution hacky ci-dessous.

Utilisation de stdin

Si vous n'avez pas besoin de fournir de données à la commande à distance, c'est la solution la plus simple.

Si vous savez que l'hôte distant possède une xargscommande qui prend en charge l' -0option et que la commande n'est pas trop grande, vous pouvez faire:

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

Cette xargs -0 env --ligne de commande est interprétée de la même manière avec toutes ces familles de shell. xargslit la liste d'arguments séparés par des valeurs nulles sur stdin et les transmet comme arguments à env. Cela suppose que le premier argument (le nom de la commande) ne contient pas de =caractères.

Ou vous pouvez utiliser shsur l'hôte distant après avoir cité chaque élément en utilisant la shsyntaxe de citation.

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

Utilisation de variables d'environnement

Maintenant, si vous avez besoin de fournir des données du client au stdin de la commande à distance, la solution ci-dessus ne fonctionnera pas.

Certains sshdéploiements de serveur permettent cependant de transmettre des variables d'environnement arbitraires du client au serveur. Par exemple, de nombreux déploiements openssh sur des systèmes basés sur Debian permettent de passer des variables dont le nom commence par LC_.

Dans ces cas, vous pourriez avoir une LC_CODEvariable contenant par exemple le code shquoted sh comme ci-dessus et s'exécuter sh -c 'eval "$LC_CODE"'sur l'hôte distant après avoir dit à votre client de passer cette variable (encore une fois, c'est une ligne de commande qui est interprétée de la même manière dans chaque shell):

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

Construire une ligne de commande compatible avec toutes les familles de shell

Si aucune des options ci-dessus n'est acceptable (parce que vous avez besoin de stdin et que sshd n'accepte aucune variable, ou parce que vous avez besoin d'une solution générique), alors vous devrez préparer une ligne de commande pour l'hôte distant qui soit compatible avec tous coquilles supportées.

C'est particulièrement délicat car tous ces shells (Bourne, csh, rc, es, fish) ont leur propre syntaxe différente, et en particulier différents mécanismes de citation et certains d'entre eux ont des limitations difficiles à contourner.

Voici une solution que j'ai trouvée, je la décris plus bas:

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

C'est un perlscript wrapper autour ssh. Je l'appelle sexec. Vous l'appelez comme:

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

donc dans votre exemple:

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

Et l'encapsuleur se transforme cmd and its argsen ligne de commande que tous les shells finissent par interpréter comme appelant cmdavec ses arguments (indépendamment de leur contenu).

Limites:

  • Le préambule et la façon dont la commande est citée signifie que la ligne de commande distante finit par être beaucoup plus grande, ce qui signifie que la limite de la taille maximale d'une ligne de commande sera atteinte plus tôt.
  • Je ne l'ai testé qu'avec: Bourne shell (de heirloom toolchest), dash, bash, zsh, mksh, lksh, yash, ksh93, rc, es, akanga, csh, tcsh, fish comme sur un système Debian récent et / bin / sh, / usr / bin / ksh, / bin / csh et / usr / xpg4 / bin / sh sur Solaris 10.
  • Si yashest le shell de connexion à distance, vous ne pouvez pas passer une commande dont les arguments contiennent des caractères non valides, mais c'est une limitation dans la mesure yashoù vous ne pouvez pas contourner de toute façon.
  • Certains shells comme csh ou bash lisent certains fichiers de démarrage lorsqu'ils sont invoqués via ssh. Nous supposons que ceux-ci ne modifient pas le comportement de façon spectaculaire afin que le préambule fonctionne toujours.
  • à côté sh, il suppose également que le système distant a la printfcommande.

Pour comprendre comment cela fonctionne, vous devez savoir comment fonctionne la citation dans les différents shells:

  • Bourne: '...'sont des citations fortes sans caractère spécial. "..."sont des guillemets faibles où il "est possible d'échapper avec une barre oblique inverse.
  • csh. Identique à Bourne, sauf qu'il "ne peut pas s'échapper à l'intérieur "...". De plus, un caractère de nouvelle ligne doit être saisi, préfixé par une barre oblique inverse. Et !cause des problèmes même à l'intérieur des guillemets simples.
  • rc. Les seules citations sont '...'(fortes). Un guillemet simple entre guillemets simples est entré comme ''(comme '...''...'). Les guillemets doubles ou les contre-obliques ne sont pas spéciaux.
  • es. Identique à rc, sauf que les guillemets extérieurs, la barre oblique inverse peut échapper à un guillemet simple.
  • fish: identique à Bourne sauf que la barre oblique inverse s'échappe à l' 'intérieur '...'.

Avec toutes ces contraintes, il est facile de voir que l'on ne peut pas citer de manière fiable des arguments de ligne de commande pour qu'il fonctionne avec tous les shells.

Utiliser des guillemets simples comme dans:

'foo' 'bar'

fonctionne dans tous, mais:

'echo' 'It'\''s'

ne fonctionnerait pas rc.

'echo' 'foo
bar'

ne fonctionnerait pas csh.

'echo' 'foo\'

ne fonctionnerait pas fish.

Cependant, nous devrions être en mesure de contourner la plupart de ces problèmes si nous parvenons à stocker ces caractères problématiques dans des variables, comme la barre oblique inverse $b, les guillemets simples $q, la nouvelle ligne dans $n(et !dans $xpour l'expansion de l'historique csh) de manière indépendante du shell.

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

fonctionnerait dans tous les obus. Cela ne fonctionnerait toujours pas pour newline csh. Si $ncontient un retour à la ligne, dans csh, vous devez l'écrire $n:qpour qu'il s'étende à un retour à la ligne et cela ne fonctionnera pas pour les autres shells. Donc, ce que nous finissons par faire ici, c'est d'appeler shet de les shétendre $n. Cela signifie également avoir à faire deux niveaux de devis, un pour le shell de connexion à distance et un pour sh.

Le $preambledans ce code est la partie la plus délicate. Il utilise les différentes différentes règles de cotation dans toutes les coquilles d'avoir certaines sections du code interprété par une seule des obus (alors qu'il est commentée pour les autres) , dont chacun vient définir les $b, $q, $n, $xvariables pour leur coquille respective.

Voici le code shell qui serait interprété par le shell de connexion de l'utilisateur distant sur hostpour votre exemple:

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

Ce code finit par exécuter la même commande lorsqu'il est interprété par l'un des shells pris en charge.

Stéphane Chazelas
la source
1
Le protocole SSH ( RFC 4254 §6.5 ) définit une commande à distance comme une chaîne. C'est au serveur de décider comment interpréter cette chaîne. Sur les systèmes Unix, l'interprétation normale consiste à transmettre la chaîne au shell de connexion de l'utilisateur. Pour un compte restreint, cela pourrait être quelque chose comme rssh ou rush qui n'accepte pas les commandes arbitraires. Il peut même y avoir une commande forcée sur le compte ou sur la clé qui entraîne l'ignorance de la chaîne de commande envoyée par le client.
Gilles 'SO- arrête d'être méchant'
1
@Gilles, merci pour la référence RFC. Oui, l'hypothèse de cette Q&R est que le shell de connexion de l'utilisateur distant est utilisable (comme dans Je peux exécuter cette commande à distance que je veux exécuter) et l'une des principales familles de shell sur les systèmes POSIX. Je ne suis pas intéressé par les shells restreints ou non-shells ou les commandes de force ou tout ce qui ne me permettra pas d'exécuter cette commande à distance de toute façon.
Stéphane Chazelas
1
Une référence utile sur les principales différences de syntaxe entre certains shells communs peut être trouvée sur Hyperpolyglot .
lcd047
0

tl; dr

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

Pour une solution plus élaborée, lisez les commentaires et inspectez l'autre réponse .

la description

Eh bien, ma solution ne fonctionnera pas avec des non- bashshells. Mais en supposant que c'est bashà l'autre bout, les choses deviennent plus simples. Mon idée est de réutiliser printf "%q"pour échapper. De manière générale, il est plus lisible d'avoir un script à l'autre extrémité, qui accepte des arguments. Mais si la commande est courte, il est probablement correct de l'intégrer. Voici quelques exemples de fonctions à utiliser dans les 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

Le résultat:

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

Alternativement, vous pouvez faire printfle travail vous-même, si vous savez ce que vous faites:

ssh USER@HOST ./1.sh '"1  '\''  \"  2"' '"3  '\''  \"  4"'
x-yuri
la source
1
Cela suppose que le shell de connexion de l'utilisateur distant est bash (comme les citations printf% q de bash de façon bash) et qu'il bashsoit disponible sur la machine distante. Il y a aussi quelques problèmes avec les guillemets manquants qui pourraient causer des problèmes avec les espaces et les caractères génériques.
Stéphane Chazelas
@ StéphaneChazelas En effet, ma solution ne vise probablement que les bashobus. Mais j'espère que les gens le trouveront utile. J'ai cependant essayé de résoudre les autres problèmes. N'hésitez pas à me dire s'il me manque autre bashchose que la chose.
x-yuri
1
Notez qu'il ne fonctionne toujours pas avec l'exemple de commande dans la question ( ssh_run user@host "${cmd[@]}"). Vous avez encore des citations manquantes.
Stéphane Chazelas
1
C'est mieux. Notez que la sortie de bash printf %qn'est pas sûre à utiliser dans un environnement local différent (et est également assez boguée; par exemple dans les environnements locaux utilisant le jeu de caractères BIG5, il (4.3.48) cite εcomme α`!). Pour cela, le mieux est de tout citer et avec des guillemets simples comme avec le shquote()dans ma réponse.
Stéphane Chazelas