Wie kann man einen beliebigen einfachen Befehl über ssh ausführen, ohne die Login-Shell des entfernten Benutzers zu kennen?


26

ssh hat eine nervige Funktion, wenn Sie ausführen:

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

Anstatt das cmdmit seinen Argumenten hostauszuführen, verkettet es das cmdund die Argumente mit Leerzeichen und führt eine Shell aus host, um den resultierenden String zu interpretieren (ich denke, deshalb heißt sie sshund nicht sexec).

Schlimmer noch, Sie wissen nicht, mit welcher Shell diese Zeichenfolge interpretiert werden soll, da es sich bei der Login-Shell usernicht einmal um eine Bourne- tcshShell fishhandelt, da immer noch Benutzer als Login-Shell fungieren und die Zahl der Nutzer steigt.

Gibt es einen Weg, das zu umgehen?

Angenommen , ich habe einen Befehl als eine Liste von Argumenten in einer gespeicherten bashMatrix, von denen jeder eine beliebige Folge von Nicht-Null - Bytes enthalten kann, ist es eine Möglichkeit , es zu haben Ausführung auf hostwie userin konsistenter Weise und unabhängig von der Login - Shell , dass userauf host(Was wir annehmen werden, ist eine der wichtigsten Unix-Shell-Familien: Bourne, csh, rc / es, fish)?

Eine andere vernünftige Annahme, die ich machen kann, ist, dass ein shBefehl hostverfügbar $PATHist, der Bourne-kompatibel ist.

Beispiel:

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

Ich kann es lokal ausführen mit ksh/ zsh/ bash/ yashals:

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

oder

env "${cmd[@]}"

oder

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

Wie würde ich es hostals userüber laufen ssh?

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

offensichtlich wird nicht funktionieren.

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

Dies würde nur funktionieren, wenn die Anmeldeshell des Remotebenutzers mit der lokalen Shell identisch wäre (oder das Zitieren so versteht, wie es printf %qin der lokalen Shell erzeugt wird) und im selben Gebietsschema ausgeführt wird.


3
Wenn das cmdArgument /bin/sh -cwäre, wir würden in 99% aller Fälle mit einer Posix-Shell enden, nicht wahr? Natürlich ist es auf diese Weise etwas schmerzhafter, Sonderzeichen zu entkommen, aber würde dies das ursprüngliche Problem lösen?
Bananguin

@Bananguin, nein, wenn Sie ssh host sh -c 'some cmd'wie ausgeführt ssh host 'sh -c some cmd'werden, wird diese sh -c some cmdBefehlszeile von der Anmeldeshell des Remotebenutzers interpretiert . Wir müssen den Befehl in der korrekten Syntax für diese Shell schreiben (und wir wissen nicht, um welche es sich handelt), shdamit er dort mit -cund some cmdArgumenten aufgerufen wird .
Stéphane Chazelas

1
@Otheus, ja, die sh -c 'some cmd'und some cmdKommandozeilen werden in all diesen Shells zufällig gleich interpretiert. Was ist nun, wenn ich die echo \'Bourne-Befehlszeile auf dem Remote-Host ausführen möchte ? echo command-string | ssh ... /bin/shist eine Lösung, die ich in meiner Antwort angegeben habe, aber das bedeutet, dass Sie keine Daten an die Standardeingabe dieses Remote-Befehls senden können.
Stéphane Chazelas

1
Klingt nach einer dauerhaften Lösung, wäre ein rexec-Plugin für ssh, unter anderem das FTP-Plugin.
Otheus

1
@myrdd, nein, das ist es nicht. Sie benötigen entweder Leerzeichen oder Tabulatoren, um Argumente in einer Shell-Befehlszeile zu trennen. Wenn dies der Fall cmdist cmd=(echo "foo bar"), sollte die Shell-Befehlszeile sshso etwas wie "Echo" -Foo-Leiste " . The *first* space (the one before Echo ) is superflous, but doen't harm. The other one (the ones before " -Foo-Leiste ) is needed. With "% q" Echo , we'd pass a "-Foo-Leiste" -Befehlszeile sein.
Stéphane Chazelas

Antworten:


19

Ich glaube nicht, dass eine Implementierung von ssheine native Möglichkeit hat, einen Befehl vom Client zum Server zu übertragen, ohne eine Shell zu verwenden.

Jetzt kann es einfacher werden, wenn Sie der Remote-Shell anweisen, nur einen bestimmten Interpreter ( shfür den wir die erwartete Syntax kennen) auszuführen und den Code auf andere Weise auszuführen.

Das andere Mittel kann beispielsweise eine Standardeingabe oder eine Umgebungsvariable sein .

Wenn beides nicht verwendet werden kann, schlage ich eine hackige dritte Lösung vor.

Stdin verwenden

Wenn Sie dem Remote-Befehl keine Daten zuführen müssen, ist dies die einfachste Lösung.

Wenn Sie wissen, dass der Remote-Host einen xargsBefehl hat, der die -0Option unterstützt, und der Befehl nicht zu groß ist, können Sie Folgendes tun:

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

Diese xargs -0 env --Befehlszeile wird für alle diese Shell-Familien gleich interpretiert. xargsLiest die durch Nullen getrennte Liste der Argumente für stdin und übergibt diese als Argumente an env. Dies setzt voraus, dass das erste Argument (der Befehlsname) keine =Zeichen enthält .

Oder Sie können es shauf dem Remote-Host verwenden, nachdem Sie jedes Element in Anführungszeichen gesetzt haben sh.

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

Umgebungsvariablen verwenden

Wenn Sie nun einige Daten vom Client an die stdin des Remote-Befehls senden müssen, funktioniert die oben beschriebene Lösung nicht.

Bei einigen sshServerbereitstellungen können jedoch beliebige Umgebungsvariablen vom Client an den Server übergeben werden. Beispielsweise erlauben viele OpenSh-Implementierungen auf Debian-basierten Systemen die Übergabe von Variablen, deren Name mit beginnt LC_.

In diesen Fällen könnten Sie eine LC_CODEVariable haben, die beispielsweise den oben angegebenen sh Code enthält und sh -c 'eval "$LC_CODE"'auf dem Remote-Host ausgeführt wird, nachdem Sie Ihrem Client mitgeteilt haben, diese Variable zu übergeben (auch dies ist eine Befehlszeile, die in jeder Shell gleich interpretiert wird):

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

Erstellen einer Befehlszeile, die mit allen Shell-Familien kompatibel ist

Wenn keine der oben genannten Optionen akzeptabel ist (weil Sie stdin benötigen und sshd keine Variablen akzeptiert oder weil Sie eine generische Lösung benötigen), müssen Sie eine Befehlszeile für den Remote-Host vorbereiten, die mit allen kompatibel ist unterstützte Muscheln.

Dies ist besonders schwierig, da alle diese Shells (Bourne, csh, rc, es, fish) eine eigene unterschiedliche Syntax und insbesondere unterschiedliche Zitiermechanismen haben und einige von ihnen Einschränkungen aufweisen, die nur schwer zu umgehen sind.

Hier ist eine Lösung, die ich mir ausgedacht habe und die ich weiter unten beschreibe:

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

Das ist ein perlWrapper-Skript ssh. Ich nenne es sexec. Du nennst es so:

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

Also in deinem Beispiel:

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

Und der Wrapper wird cmd and its argszu einer Befehlszeile, die alle Shells als Aufruf cmdmit ihren Argumenten interpretieren (unabhängig von ihrem Inhalt).

Einschränkungen:

  • Die Präambel und die Art und Weise, wie der Befehl in Anführungszeichen gesetzt wird, bedeuten, dass die Remote-Befehlszeile erheblich größer wird, was bedeutet, dass die Grenze für die maximale Größe einer Befehlszeile eher erreicht wird.
  • Ich habe es nur getestet mit: Bourne-Shell (aus der Erbstück-Werkzeugkiste), Dash, Bash, Zsh, Mksh, Lksh, Yash, Ksh93, RC, ES, Akanga, CSH, TCSH, Fisch, wie auf einem aktuellen Debian-System und / bin / sh, / usr / bin / ksh, / bin / csh und / usr / xpg4 / bin / sh unter Solaris 10.
  • Wenn yashes sich um die Remote-Anmeldeshell handelt, können Sie keinen Befehl übergeben, dessen Argumente ungültige Zeichen enthalten. Dies ist jedoch eine Einschränkung yash, die Sie ohnehin nicht umgehen können.
  • Einige Shells wie csh oder bash lesen einige Startdateien, wenn sie über ssh aufgerufen werden. Wir gehen davon aus, dass dies das Verhalten nicht dramatisch verändert, so dass die Präambel weiterhin funktioniert.
  • shAußerdem wird davon ausgegangen , dass das ferne System über den printfBefehl verfügt.

Um zu verstehen, wie es funktioniert, müssen Sie wissen, wie das Zitieren in den verschiedenen Shells funktioniert:

  • Bourne: '...'sind starke Anführungszeichen ohne besonderen Charakter. "..."sind schwache Anführungszeichen, denen "mit Backslash begegnet werden kann.
  • csh. Wie Bourne, nur dass "es nicht drinnen entkommen kann "...". Außerdem muss ein Zeilenumbruch mit einem Backslash vorangestellt werden. Und !verursacht Probleme auch in einfachen Anführungszeichen.
  • rc. Die einzigen Anführungszeichen sind '...'(stark). Ein einfaches Anführungszeichen in einfachen Anführungszeichen wird als ''(like '...''...') eingegeben . Doppelte Anführungszeichen oder Backslashes sind keine Besonderheiten.
  • es. Entspricht rc, mit der Ausnahme, dass Backslash außerhalb von Anführungszeichen vor einem einzelnen Anführungszeichen stehen kann.
  • fish: wie Bourne mit dem Unterschied, dass ein Backslash im 'Inneren auftritt '...'.

Bei all diesen Einschränkungen ist leicht zu erkennen, dass man Befehlszeilenargumente nicht zuverlässig zitieren kann, so dass es mit allen Shells funktioniert.

Verwenden von einfachen Anführungszeichen wie in:

'foo' 'bar'

funktioniert in allen aber:

'echo' 'It'\''s'

würde nicht funktionieren rc.

'echo' 'foo
bar'

würde nicht funktionieren csh.

'echo' 'foo\'

würde nicht funktionieren fish.

Allerdings sollten wir in der Lage sein , um die meisten dieser Probleme zu arbeiten , wenn wir die problematischen Zeichen in Variablen, wie Backslash in speichern verwalten $b, Apostroph in $q, Newline in $n(und !in $xfür csh Geschichte Expansion) in einer Schale unabhängige Art und Weise.

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

würde in allen Schalen funktionieren. Das würde aber für newline immer noch nicht funktionieren csh. Wenn $nnewline enthält, cshmüssen Sie es so schreiben, $n:qdass es zu einer newline erweitert wird, und das funktioniert nicht für andere Shells. Also rufen wir stattdessen hier an shund haben diese sherweitert $n. Dies bedeutet auch, dass zwei Angebotsstufen erforderlich sind, eine für die Remote-Anmeldeshell und eine für sh.

Der $preamblein diesem Code ist der schwierigste Teil. Es nutzt die verschiedenen zitiert Regeln in allen Schalen einige Abschnitte des Codes durch nur eine der Schalen interpretiert zu haben (während es für die anderen Kommentar gesetzt ist) , von denen jeder nur diejenigen definieren $b, $q, $n, $xVariablen für ihre jeweiligen Shell.

Hier ist der Shell-Code, der von der Login-Shell des entfernten Benutzers hostfür Ihr Beispiel interpretiert wird :

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

Dieser Code führt am Ende denselben Befehl aus, wenn er von einer der unterstützten Shells interpretiert wird.


1
Das SSH-Protokoll ( RFC 4254, §6.5 ) definiert einen Remote-Befehl als Zeichenfolge. Es ist Sache des Servers, zu entscheiden, wie dieser String interpretiert wird. Auf Unix-Systemen besteht die normale Interpretation darin, die Zeichenfolge an die Anmeldeshell des Benutzers zu übergeben. Bei einem eingeschränkten Konto kann dies so etwas wie "rssh" oder "rush" sein, das keine willkürlichen Befehle akzeptiert. Möglicherweise gibt es sogar einen erzwungenen Befehl für das Konto oder den Schlüssel, der dazu führt, dass die vom Client gesendete Befehlszeichenfolge ignoriert wird.
Gilles 'SO- hör auf böse zu sein'

1
@ Gilles, danke für die RFC-Referenz. Ja, die Voraussetzung für diese Fragen und Antworten ist, dass die Anmeldeshell des Remotebenutzers (wie in "Ich kann den gewünschten Remotebefehl ausführen") und eine der Hauptshellfamilien auf POSIX-Systemen verwendet werden kann. Ich bin nicht an eingeschränkten Shells oder Nicht-Shells oder Force-Befehlen interessiert oder an irgendetwas, das es mir sowieso nicht erlaubt, diesen Remote-Befehl auszuführen.
Stéphane Chazelas

1
Eine nützliche Referenz zu den wichtigsten Syntaxunterschieden zwischen einigen gängigen Shells finden Sie unter Hyperpolyglot .
lcd047

0

tl; dr

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

Lesen Sie die Kommentare, und lesen Sie die andere Antwort , um eine ausführlichere Lösung zu erhalten .

Beschreibung

Nun, meine Lösung funktioniert nicht mit Nicht- bashShells. Aber wenn es basham anderen Ende ist, wird es einfacher. Meine Idee ist es, printf "%q"für die Flucht wiederzuverwenden . Außerdem ist es im Allgemeinen besser lesbar, ein Skript am anderen Ende zu haben, das Argumente akzeptiert. Aber wenn der Befehl kurz ist, ist es wahrscheinlich in Ordnung, ihn einzufügen. Hier sind einige Beispielfunktionen, die in Skripten verwendet werden können:

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

Die Ausgabe:

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

Alternativ können Sie printfIhre Arbeit auch selbst erledigen , wenn Sie wissen, was Sie tun:

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

1
Dies setzt voraus, dass die Anmeldeshell des Remotebenutzers bash ist (als bashs printf% q in bash-Anführungszeichen) und bashauf dem Remotecomputer verfügbar ist. Es gibt auch einige Probleme mit fehlenden Anführungszeichen, die Probleme mit Leerzeichen und Platzhaltern verursachen würden.
Stéphane Chazelas

@ StéphaneChazelas In der Tat zielt meine Lösung wahrscheinlich nur auf bashMuscheln. Aber hoffentlich werden die Leute es von Nutzen finden. Ich habe versucht, die anderen Probleme anzusprechen. Sie können mir gerne mitteilen, ob etwas anderes fehlt als das, bashwas ich vermisse .
X-Yuri

1
Beachten Sie, dass es mit dem Beispielbefehl in der question ( ssh_run user@host "${cmd[@]}") immer noch nicht funktioniert . Es fehlen noch einige Anführungszeichen.
Stéphane Chazelas

1
Das ist besser. Beachten Sie, dass die Ausgabe von bash printf %qin einem anderen Gebietsschema nicht sicher ist (und auch ziemlich fehlerhaft ist; in Gebietsschemas, die den BIG5-Zeichensatz verwenden, wird sie (4.3.48) in Anführungszeichen gesetzt εals α`!). Dazu zitiere shquote()ich am besten alles und mit einfachen Anführungszeichen nur so wie mit der in meiner Antwort.
Stéphane Chazelas
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.