Как выполнить произвольную простую команду над ssh, не зная оболочки входа удаленного пользователя?

26

ssh имеет раздражающую особенность, когда вы запускаете:

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

Вместо того чтобы запускать cmdего с включенными аргументами host, он объединяет cmdаргументы и аргументы с пробелами и запускает оболочку hostдля интерпретации полученной строки (я думаю, поэтому она вызывается sshи не вызывается sexec).

Хуже того, вы не знаете, какая оболочка будет использоваться для интерпретации этой строки, поскольку это оболочка входа в систему, userкоторая даже не гарантированно похожа на Борну, поскольку есть еще люди, которые используют ее в tcshкачестве оболочки входа в систему и fishнаходится на подъеме.

Есть ли способ обойти это?

Предположим , у меня есть команда , как список аргументов , хранящихся в bashмассиве, каждый из которых может содержать любую последовательность байтов непустых, есть ли способ , чтобы он выполнен на , hostкак userна постоянной основе , независимо от входа оболочки , что userна host(что мы предположим, это одно из основных семейств оболочки Unix: Bourne, csh, rc / es, fish)?

Другое разумное предположение, которое я должен сделать, - наличие shкоманды, hostдоступной в $PATHэтом, совместимой с Bourne.

Пример:

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

Я могу запустить его локально с ksh/ zsh/ bash/ yashкак:

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

или

env "${cmd[@]}"

или

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

Как бы мне запустить его hostкак userзакончено ssh?

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

очевидно не сработает.

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

будет работать только в том случае, если оболочка входа удаленного пользователя будет такой же, как локальная оболочка (или понимает кавычки так же, как printf %qв локальной оболочке, создающей ее) и работает в той же локали.

Стефан Шазелас
источник
3
Если бы cmdаргумент был таким, /bin/sh -cмы бы в конечном итоге создали оболочку posix в 99% случаев, не так ли? Конечно, экранировать специальные символы немного сложнее, но решит ли это начальную проблему?
Bananguin
@Bananguin, нет, если вы запускаете ssh host sh -c 'some cmd', так же, как это ssh host 'sh -c some cmd', если оболочка входа удаленного пользователя интерпретирует эту sh -c some cmdкомандную строку. Нам нужно написать команду в правильном синтаксисе для этой оболочки (и мы не знаем , какой он есть) , так что shможно назвать там с -cи some cmdаргументами.
Стефан Шазелас
1
@Otheus, да, sh -c 'some cmd'и some cmdкомандные строки , случается, интерпретируется одинаково во всех этих оболочках. А что если я захочу запустить echo \'командную строку Bourne на удаленном хосте? echo command-string | ssh ... /bin/shэто решение, которое я дал в своем ответе, но это означает, что вы не можете передавать данные на стандартный ввод этой удаленной команды.
Стефан Шазелас
1
Похоже, что более долгосрочное решение было бы плагином rexec для ssh, а именно плагином ftp.
Отей
1
@myrdd, нет, это не так, вам нужно использовать пробел или табуляцию для разделения аргументов в командной строке оболочки. Если cmdэто так cmd=(echo "foo bar"), передаваемая в командную строку оболочки sshдолжна быть что-то вроде `'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'` командной строки.
Стефан Шазелас

Ответы:

19

Я не думаю, что в какой-либо реализации sshесть собственный способ передачи команды от клиента к серверу без использования оболочки.

Теперь все может стать проще, если вы можете указать удаленной оболочке запускать только определенный интерпретатор (например sh, для которого мы знаем ожидаемый синтаксис) и дать код для выполнения другим способом.

Этим другим средством может быть, например, стандартный ввод или переменная окружения .

Когда ни один из них не может быть использован, я предлагаю хакерское третье решение ниже.

Использование stdin

Если вам не нужно передавать какие-либо данные в удаленную команду, это самое простое решение.

Если вы знаете, что на удаленном хосте есть xargsкоманда, которая поддерживает эту -0опцию, и эта команда не слишком велика, вы можете сделать:

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

Эта xargs -0 env --командная строка интерпретируется одинаково со всеми этими семействами оболочек. xargsчитает список аргументов с нулевым разделителем в stdin и передает их в качестве аргументов env. Это предполагает, что первый аргумент (имя команды) не содержит =символов.

Или вы можете использовать shна удаленном хосте после цитирования каждого элемента с использованием 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

Использование переменных среды

Теперь, если вам нужно передать некоторые данные от клиента на стандартный ввод удаленной команды, вышеуказанное решение не будет работать.

Однако некоторые sshразвертывания сервера позволяют передавать произвольные переменные среды от клиента к серверу. Например, многие развертывания openssh в системах на основе Debian позволяют передавать переменные, имя которых начинается с LC_.

В этих случаях вы можете иметь LC_CODEпеременную, например, содержащую код в кавычках, sh как указано выше, и запускать sh -c 'eval "$LC_CODE"'на удаленном хосте после того, как попросите своего клиента передать эту переменную (опять же, это командная строка, которая интерпретируется одинаково в каждой оболочке):

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

Создание командной строки, совместимой со всеми семействами оболочек

Если ни один из приведенных выше вариантов не является приемлемым (поскольку вам нужен stdin, а sshd не принимает никаких переменных, или потому, что вам нужно универсальное решение), вам придется подготовить командную строку для удаленного хоста, совместимого со всеми поддерживаемые снаряды.

Это особенно сложно, потому что все эти оболочки (Bourne, csh, rc, es, fish) имеют свой собственный синтаксис и, в частности, разные механизмы цитирования, а некоторые из них имеют ограничения, которые трудно обойти.

Вот решение, которое я придумала, я опишу его ниже:

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

Это perlсценарий обертки вокруг ssh. Я называю это sexec. Вы называете это как:

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

Итак, в вашем примере:

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

И оболочка превращается cmd and its argsв командную строку, которую все оболочки интерпретируют как вызовы cmdс ее аргументами (независимо от их содержания).

Ограничения:

  • Преамбула и способ указания команды означают, что удаленная командная строка оказывается значительно больше, что означает, что ограничение на максимальный размер командной строки будет достигнуто раньше.
  • Я протестировал его только с: оболочкой Bourne (из набора инструментов семейной реликвии), dash, bash, zsh, mksh, lksh, yash, ksh93, rc, es, akanga, csh, tcsh, fish, как было установлено в недавней системе Debian, и / bin / sh, / usr / bin / ksh, / bin / csh и / usr / xpg4 / bin / sh на Solaris 10.
  • Если yashэто оболочка удаленного входа, вы не можете передать команду, аргументы которой содержат недопустимые символы, но это ограничение в yashтом, что вы все равно не можете обойтись.
  • Некоторые оболочки, такие как csh или bash, читают некоторые файлы запуска при вызове через ssh. Мы предполагаем, что они не изменяют поведение резко, так что преамбула все еще работает.
  • кроме того sh, он также предполагает, что удаленная система имеет printfкоманду.

Чтобы понять, как это работает, вам нужно знать, как работает цитирование в различных оболочках:

  • Борн: '...'это сильные цитаты без специальных символов. "..."являются слабыми кавычками, где "можно избежать обратной косой черты.
  • csh, То же самое, что и Борн, за исключением того, что "нельзя сбежать внутрь "...". Также символ новой строки должен вводиться с префиксом с обратной косой чертой. И !вызывает проблемы даже внутри одинарных кавычек.
  • rc, Единственные цитаты '...'(сильные). Одиночная кавычка в одинарных кавычках вводится как ''(как '...''...'). Двойные кавычки или обратная косая черта не являются особенными.
  • es, То же, что и для rc, за исключением того, что вне кавычек обратная косая черта может выходить за пределы одной кавычки.
  • fish: так же, как Борн, за исключением того, что обратный слеш убегает 'внутрь '...'.

Со всеми этими ограничениями легко заметить, что нельзя надежно процитировать аргументы командной строки, чтобы она работала со всеми оболочками.

Использование одинарных кавычек, как в:

'foo' 'bar'

работает во всех, кроме:

'echo' 'It'\''s'

не будет работать в rc.

'echo' 'foo
bar'

не будет работать в csh.

'echo' 'foo\'

не будет работать в fish.

Однако мы сможем обойти большинство из этих проблем, если нам удастся сохранить эти проблемные символы в переменных, таких как обратная косая черта $b, одинарная кавычка $q, новая строка в $n!в $xдля расширения истории csh) независимым способом оболочки.

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

будет работать во всех снарядах. Это все равно не будет работать для новой строки, cshхотя. Если в нем $nсодержится символ новой строки, cshвы должны написать его так, $n:qчтобы он расширился до новой строки, и это не будет работать для других оболочек. Итак, то, что мы в итоге делаем вместо этого, - это звонить shи shрасширять их $n. Это также означает необходимость выполнения двух уровней цитирования: один для оболочки удаленного входа и один для sh.

В $preambleэтом коде самая сложная часть. Это делает использование различных различного котирования правил во всех оболочках , чтобы иметь некоторые участки кода истолкован лишь одной из оболочек ( в то время как закомментированы для других) , каждого из которых только определяющих те $b, $q, $n, $xпеременных для их соответствующей оболочки.

Вот код оболочки, который будет интерпретирован оболочкой входа удаленного пользователя hostдля вашего примера:

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

Этот код в конечном итоге выполняет ту же команду при интерпретации любым из поддерживаемых оболочек.

Стефан Шазелас
источник
1
Протокол SSH ( RFC 4254 §6.5 ) определяет удаленную команду как строку. Сервер должен решить, как интерпретировать эту строку. В системах Unix обычная интерпретация - передать строку в оболочку входа пользователя. Для ограниченной учетной записи это может быть что-то вроде rssh или rush, которое не принимает произвольные команды. Для учетной записи или для ключа может быть даже принудительная команда, которая приводит к игнорированию командной строки, отправленной клиентом.
Жиль "ТАК - перестань быть злым"
1
@ Жиль, спасибо за ссылку на RFC. Да, предполагается, что для этой Q & A можно использовать оболочку входа удаленного пользователя (как я могу запустить эту удаленную команду, которую я хочу запустить) и одно из основных семейств оболочек в системах POSIX. Меня не интересуют ограниченные оболочки или не-оболочки, или принудительные команды или что-нибудь, что не позволит мне запустить эту удаленную команду в любом случае.
Стефан Шазелас
1
Полезную ссылку об основных различиях в синтаксисе между некоторыми общими оболочками можно найти в Гиперполиглоте .
lcd047
0

ТЛ; др

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

Для более сложного решения, прочитайте комментарии и изучите другой ответ .

описание

Ну, мое решение не будет работать с не- bashоболочками. Но при условии, что это bashна другом конце, все становится проще. Моя идея состоит в том, чтобы повторно использовать printf "%q"для побега. Кроме того, обычно на другом конце более удобно читать сценарий, который принимает аргументы. Но если команда короткая, возможно, это нормально. Вот несколько примеров функций для использования в скриптах:

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

Выход:

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

Кроме того, вы можете сделать printfработу самостоятельно, если знаете, что делаете:

ssh USER@HOST ./1.sh '"1  '\''  \"  2"' '"3  '\''  \"  4"'
х-юри
источник
1
Это предполагает, что оболочкой входа удаленного пользователя является bash (как в bash в printf% q в стиле bash), и bashона доступна на удаленном компьютере. Есть также несколько проблем с пропущенными кавычками, которые могут вызвать проблемы с пробелами и подстановочными знаками.
Стефан
@ StéphaneChazelas Действительно, мое решение, вероятно, нацелено только на bashоболочки. Но, надеюсь, люди найдут его полезным. Я пытался решить другие вопросы, хотя. Не стесняйтесь сказать мне, если я что-то упускаю, кроме этой bashвещи.
x-
1
Обратите внимание, что он все еще не работает с примером команды в question ( ssh_run user@host "${cmd[@]}"). У вас все еще есть некоторые пропущенные цитаты.
Стефан
1
Так-то лучше. Обратите внимание, что выходные данные bash printf %qнебезопасны для использования в другой локали (и они также довольно ошибочны; например, в локалях, использующих кодировку BIG5, они (4.3.48) заключаются в кавычки εкак α`!). Для этого лучше всего цитировать все и в одинарных кавычках, как shquote()в моем ответе.
Стефан