Jak wykonać dowolne proste polecenie za pomocą ssh, nie znając powłoki logowania zdalnego użytkownika?

26

ssh ma irytującą funkcję polegającą na tym, że po uruchomieniu:

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

Zamiast uruchamiać to cmdz włączonymi argumentami host, łączy to cmdi argumenty ze spacjami i uruchamia powłokę, hostaby zinterpretować otrzymany ciąg (chyba dlatego jest wywoływany, ssha nie sexec).

Co gorsza, nie wiesz, która powłoka będzie używana do interpretacji tego ciągu, ponieważ jest to powłoka logowania, userktórej nie można nawet zagwarantować, że jest Bourne, ponieważ wciąż są ludzie używający tcshich powłoki logowania i fishrośnie.

Czy jest na to jakiś sposób?

Załóżmy, że mam polecenie jako lista argumentów przechowywanych w bashtablicy, z których każdy może zawierać dowolną sekwencję bajtów non-null, czy jest jakiś sposób na to realizowane na hostjak userw spójny sposób, niezależnie od powłoki logowania, że userna host(zakładamy, że jest jedną z głównych rodzin powłok uniksowych: Bourne, csh, rc / es, fish)?

Innym rozsądne założenie, że powinienem być w stanie zrobić to, że nie będzie shkomenda na hostdostępne w $PATHto Bourne-kompatybilne.

Przykład:

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

Mogę uruchomić go lokalnie za pomocą ksh/ zsh/ bash/ yashas:

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

lub

env "${cmd[@]}"

lub

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

Jak bym go uruchomić na hostjak userpowyżej ssh?

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

oczywiście nie zadziała.

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

działałoby tylko wtedy, gdy powłoka logowania zdalnego użytkownika była taka sama jak powłoka lokalna (lub rozumie cytowanie w taki sam sposób, jak printf %qw powłoce lokalnej ją produkuje) i działa w tych samych ustawieniach narodowych.

Stéphane Chazelas
źródło
3
Jeśli cmdargument /bin/sh -cbyłby taki, że w 99% przypadków otrzymalibyśmy powłokę posiksową, prawda? Oczywiście ucieczka od znaków specjalnych jest w ten sposób nieco bardziej bolesna, ale czy rozwiązałoby to początkowy problem?
Bananguin
@Bananguin, nie, jeśli działasz ssh host sh -c 'some cmd'tak samo, jak ssh host 'sh -c some cmd'ma powłokę logowania użytkownika zdalnego, interpretuj ten sh -c some cmdwiersz poleceń. Musimy napisać polecenia w poprawnej składni tego płaszcza (i nie wiem, który to jest), tak aby shbyć nazywany tam z -ci some cmdargumenty.
Stéphane Chazelas
1
@Otheus, tak, wiersze poleceń sh -c 'some cmd'i some cmdsą interpretowane tak samo we wszystkich tych powłokach. Co teraz, jeśli chcę uruchomić echo \'wiersz poleceń Bourne na zdalnym hoście? echo command-string | ssh ... /bin/shto jedno rozwiązanie, które podałem w mojej odpowiedzi, ale oznacza to, że nie możesz podać danych na standardowe wyjście tej zdalnej komendy.
Stéphane Chazelas
1
Wygląda na to, że bardziej trwałe rozwiązanie byłoby wtyczką rexec dla ssh, zwaną także wtyczką ftp.
Otheus
1
@myrdd, nie, nie jest, potrzebujesz spacji lub tabulacji, aby oddzielić argumenty w linii poleceń powłoki. Jeśli cmdtak cmd=(echo "foo bar"), przekazany wiersz poleceń powłoki sshpowinien być czymś w rodzaju wiersza polecenia „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 ”„ pasek foo ” .
Stéphane Chazelas,

Odpowiedzi:

19

Nie sądzę, aby jakakolwiek implementacja sshmiała natywny sposób przekazywania poleceń z klienta na serwer bez angażowania powłoki.

Teraz może być łatwiej, jeśli powiesz zdalnej powłoce, aby uruchomiła tylko określony interpreter (na przykład sh, dla którego znamy oczekiwaną składnię) i poda kod do wykonania w inny sposób.

Tym innym środkiem może być na przykład standardowe wejście lub zmienna środowiskowa .

Gdy żadnego z nich nie można użyć, poniżej proponuję hacky trzecie rozwiązanie.

Używanie standardowego wejścia

Jeśli nie musisz podawać żadnych danych do polecenia zdalnego, jest to najłatwiejsze rozwiązanie.

Jeśli wiesz, że na zdalnym hoście znajduje się xargspolecenie obsługujące tę -0opcję, a polecenie to nie jest zbyt duże, możesz:

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

Ta xargs -0 env --linia poleceń jest interpretowana tak samo dla wszystkich rodzin powłok. xargsodczytuje listę argumentów ograniczoną przez zero na stdin i przekazuje je jako argumenty do env. Zakłada się, że pierwszy argument (nazwa polecenia) nie zawiera =znaków.

Lub możesz użyć shna zdalnym hoście po zacytowaniu każdego elementu przy użyciu shskładni cytowania.

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

Korzystanie ze zmiennych środowiskowych

Teraz, jeśli musisz podać pewne dane od klienta do standardowego polecenia zdalnego, powyższe rozwiązanie nie będzie działać.

Niektóre sshwdrożenia serwera umożliwiają jednak przekazywanie dowolnych zmiennych środowiskowych z klienta na serwer. Na przykład wiele wdrożeń openssh w systemach opartych na Debianie umożliwia przekazywanie zmiennych, których nazwa zaczyna się od LC_.

W takich przypadkach możesz mieć LC_CODEna przykład zmienną zawierającą kod dzielony sh jak wyżej i uruchomić sh -c 'eval "$LC_CODE"'na zdalnym hoście po tym, jak kazałeś klientowi przekazać tę zmienną (ponownie, to wiersz poleceń, który jest interpretowany tak samo w każdej powłoce):

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

Budowanie wiersza poleceń zgodnego ze wszystkimi rodzinami powłok

Jeśli żadna z powyższych opcji nie jest akceptowalna (ponieważ potrzebujesz stdin, a sshd nie akceptuje żadnej zmiennej lub potrzebujesz ogólnego rozwiązania), musisz przygotować wiersz poleceń dla zdalnego hosta, który jest kompatybilny ze wszystkimi obsługiwane powłoki.

Jest to szczególnie trudne, ponieważ wszystkie te powłoki (Bourne, csh, rc, es, fish) mają swoją inną składnię, a w szczególności różne mechanizmy cytowania, a niektóre z nich mają ograniczenia, które trudno jest obejść.

Oto rozwiązanie, które wymyśliłem, opisuję je poniżej:

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

To jest perlskrypt opakowania ssh. Nazywam to sexec. Nazywasz to tak:

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

więc w twoim przykładzie:

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

Opakowanie zamienia cmd and its argssię w wiersz poleceń, który wszystkie powłoki interpretują jako wywołanie cmdz argumentami (niezależnie od ich zawartości).

Ograniczenia:

  • Preambuła i sposób cytowania polecenia oznaczają, że zdalna linia poleceń jest znacznie większa, co oznacza, że ​​limit maksymalnego rozmiaru wiersza poleceń zostanie osiągnięty wcześniej.
  • Testowałem to tylko z: powłoką Bourne'a (z heirloom Toolchest), dash, bash, zsh, mksh, lksh, yash, ksh93, rc, es, akanga, csh, tcsh, fish, jak znaleziono w ostatnim systemie Debian i / bin / sh, / usr / bin / ksh, / bin / csh i / usr / xpg4 / bin / sh w systemie Solaris 10.
  • Jeśli yashjest to powłoka zdalnego logowania, nie można przekazać polecenia, którego argumenty zawierają nieprawidłowe znaki, ale jest to ograniczenie yashpolegające na tym, że i tak nie można się obejść.
  • Niektóre powłoki, takie jak csh lub bash, odczytują niektóre pliki startowe, gdy są wywoływane przez ssh. Zakładamy, że nie zmieniają one radykalnie zachowania, więc preambuła nadal działa.
  • poza shtym zakłada również, że system zdalny ma printfpolecenie.

Aby zrozumieć, jak to działa, musisz wiedzieć, jak działa cytowanie w różnych powłokach:

  • Bourne: '...'są mocnymi cytatami bez charakteru specjalnego. "..."są słabymi cytatami, w których "można uniknąć znaku ukośnika odwrotnego.
  • csh. Tak samo jak Bourne, tyle że "nie można uciec do środka "...". Również znak nowej linii musi być poprzedzony odwrotnym ukośnikiem. I !powoduje problemy nawet w pojedynczych cudzysłowach.
  • rc. Jedyne cytaty to '...'(mocne). Pojedynczy cytat w ramach pojedynczych cytatów jest wprowadzany jako ''(jak '...''...'). Podwójne cudzysłowy lub odwrotne ukośniki nie są wyjątkowe.
  • es. Podobnie jak rc, z wyjątkiem cudzysłowów, odwrotny ukośnik może uciec od pojedynczego cudzysłowu.
  • fish: tak samo jak Bourne, tyle że ukośnik ucieka do 'środka '...'.

Przy wszystkich tych przeciwnościach łatwo zauważyć, że nie można w sposób wiarygodny zacytować argumentów wiersza poleceń, aby działał ze wszystkimi powłokami.

Używanie pojedynczych cudzysłowów jak w:

'foo' 'bar'

działa we wszystkich oprócz:

'echo' 'It'\''s'

nie działa rc.

'echo' 'foo
bar'

nie działa csh.

'echo' 'foo\'

nie działa fish.

Jednak powinniśmy być w stanie obejść większość z tych problemów, jeśli uda nam się zachować tych problematycznych znaków w zmiennych, jak ukośnik w $b, w pojedynczy cudzysłów $q, znak nowej linii w $n(i !w $xdla csh historii ekspansji) w powłoce niezależny sposób.

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

działałby we wszystkich powłokach. Mimo to nadal nie działałoby to w przypadku nowej linii csh. Jeśli $nzawiera znak nowej linii csh, musisz napisać, $n:qaby rozwinął się do nowej linii i nie będzie to działać w przypadku innych powłok. Więc to, co w końcu robimy, to dzwonienie shi shrozwijanie ich $n. Oznacza to również konieczność wykonania dwóch poziomów cytowania, jednego dla zdalnej powłoki logowania i jednego dla sh.

W $preambletym kodzie jest najtrudniejsza część. To sprawia, że korzystanie z różnymi cytując zasad we wszystkich muszli mieć pewne fragmenty kodu interpretowane tylko przez jedno z muszli (podczas gdy jest to w komentarzu dla innych), z których każdy tylko definiującej te $b, $q, $n, $xzmienne dla ich odpowiedniego zbiornika.

Oto kod powłoki, który zostałby zinterpretowany przez powłokę logowania użytkownika zdalnego na hosttwoim przykładzie:

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

Ten kod uruchamia to samo polecenie, gdy jest interpretowane przez jedną z obsługiwanych powłok.

Stéphane Chazelas
źródło
1
Protokół SSH ( RFC 4254 §6.5 ) definiuje polecenie zdalne jako ciąg znaków. To serwer decyduje, jak interpretować ten ciąg. W systemach uniksowych normalną interpretacją jest przekazywanie ciągu do powłoki logowania użytkownika. W przypadku konta z ograniczeniami może to być coś w rodzaju rssh lub rush, który nie akceptuje dowolnych poleceń. Może istnieć nawet wymuszone polecenie na koncie lub kluczu, które powoduje ignorowanie ciągu poleceń wysłanego przez klienta.
Gilles „SO- przestań być zły”
1
@Gilles, dzięki za odniesienie do RFC. Tak, założeniem dla tego pytania i odpowiedzi jest to, że powłoka logowania zdalnego użytkownika jest użyteczna (jak w tym, że mogę uruchomić to zdalne polecenie, które chcę uruchomić) i jedna z głównych rodzin powłok w systemach POSIX. Nie jestem zainteresowany ograniczonymi powłokami i powłokami, ani wymuszania poleceń, ani niczego, co nie pozwoli mi na uruchomienie tego zdalnego polecenia.
Stéphane Chazelas,
1
Przydatne odniesienie do głównych różnic w składni między niektórymi popularnymi powłokami można znaleźć w Hyperpolyglot .
lcd047
0

tl; dr

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

Aby uzyskać bardziej skomplikowane rozwiązanie, przeczytaj komentarze i sprawdź drugą odpowiedź .

opis

Moje rozwiązanie nie będzie działać z bashpowłokami innymi niż powłoki. Ale zakładając, że jest bashz drugiej strony, sprawy stają się prostsze. Moim pomysłem jest ponowne wykorzystanie printf "%q"do ucieczki. Ogólnie rzecz biorąc, bardziej czytelny jest skrypt na drugim końcu, który akceptuje argumenty. Ale jeśli polecenie jest krótkie, prawdopodobnie dobrze jest je wstawić. Oto kilka przykładowych funkcji używanych w skryptach:

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

Wyjście:

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

Alternatywnie możesz sam wykonać printfpracę, jeśli wiesz, co robisz:

ssh USER@HOST ./1.sh '"1  '\''  \"  2"' '"3  '\''  \"  4"'
x-yuri
źródło
1
Zakłada się, że powłoka logowania zdalnego użytkownika to bash (jak drukuje bash printf% q w sposób bash) i że bashbędzie dostępna na zdalnym komputerze. Istnieje również kilka problemów z brakującymi cytatami, które mogłyby powodować problemy z białymi znakami i znakami wieloznacznymi.
Stéphane Chazelas,
@ StéphaneChazelas Rzeczywiście, moje rozwiązanie prawdopodobnie dotyczy tylko bashpowłok. Ale mam nadzieję, że ludzie znajdą to w użyciu. Próbowałem jednak rozwiązać inne problemy. Nie krępuj się powiedzieć mi, czy coś mi brakuje oprócz tego bash.
x-yuri,
1
Zauważ, że nadal nie działa z przykładowym poleceniem w question ( ssh_run user@host "${cmd[@]}"). Wciąż brakuje niektórych cytatów.
Stéphane Chazelas,
1
Tak lepiej Zauważ, że wyjście bash printf %qnie jest bezpieczne do użycia w różnych lokalizacjach (i jest również dość błędne; na przykład w lokalizacjach używających zestawu znaków BIG5, to (4.3.48) cytuje εjako α`!). W tym celu najlepiej zacytować wszystko, używając pojedynczych cytatów, tak jak shquote()w mojej odpowiedzi.
Stéphane Chazelas,