Bagaimana cara menjalankan perintah sederhana sewenang-wenang atas ssh tanpa mengetahui shell login dari pengguna jarak jauh?


26

ssh memiliki fitur yang mengganggu ketika Anda menjalankan:

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

Alih-alih menjalankannya cmddengan argumen-argumennya aktif host, ia menggabungkannya cmddan berargumen dengan spasi dan menjalankan shell hostuntuk menafsirkan string yang dihasilkan (saya kira itu sebabnya dipanggil sshdan tidak sexec).

Lebih buruk lagi, Anda tidak tahu shell apa yang akan digunakan untuk menafsirkan string itu karena itu shell login useryang bahkan tidak dijamin menjadi Bourne seperti masih ada orang yang menggunakan tcshshell login mereka dan fishsedang meningkat.

Apakah ada jalan keluarnya?

Misalkan saya memiliki perintah sebagai daftar argumen yang disimpan dalam basharray, yang masing-masing dapat berisi urutan byte non-null, apakah ada cara untuk menjalankannya hostseperti userdalam cara yang konsisten terlepas dari shell login yang ada userdi host(yang akan kita asumsikan adalah salah satu keluarga shell Unix utama: Bourne, csh, rc / es, fish)?

Asumsi lain yang masuk akal bahwa saya harus dapat membuat adalah bahwa ada shperintah yang hosttersedia di $PATHyang kompatibel dengan Bourne.

Contoh:

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

Saya dapat menjalankannya secara lokal dengan ksh/ zsh/ bash/ yashsebagai:

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

atau

env "${cmd[@]}"

atau

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

Bagaimana saya menjalankannya hostsetelah userselesai ssh?

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

jelas tidak akan berhasil.

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

hanya akan berfungsi jika shell login dari pengguna jarak jauh sama dengan shell lokal (atau memahami mengutip dengan cara yang sama seperti printf %qpada shell lokal yang memproduksinya) dan berjalan di lokal yang sama.


3
Jika cmdargumennya adalah /bin/sh -ckita akan berakhir dengan shell posix di 99% dari semua kasus, bukan? Tentu saja melarikan diri dari karakter khusus sedikit lebih menyakitkan dengan cara ini, tetapi apakah itu akan menyelesaikan masalah awal?
Bananguin

@ Bananguin, tidak jika Anda menjalankan ssh host sh -c 'some cmd', sama seperti ssh host 'sh -c some cmd', yang memiliki shell login dari pengguna jarak jauh menafsirkan sh -c some cmdbaris perintah itu. Kita perlu menulis perintah dalam sintaks yang benar untuk shell itu (dan kita tidak tahu yang mana) sehingga shdipanggil ke sana dengan -cdan some cmdargumen.
Stéphane Chazelas

1
@ Ous, ya, sh -c 'some cmd'dan some cmdbaris perintah kebetulan diartikan sama di semua shell itu. Sekarang bagaimana jika saya ingin menjalankan echo \'baris perintah Bourne pada remote host? echo command-string | ssh ... /bin/shadalah salah satu solusi yang saya berikan dalam jawaban saya, tetapi itu berarti Anda tidak dapat memberi makan data ke stdin dari perintah jarak jauh itu.
Stéphane Chazelas

1
Kedengarannya seperti solusi yang lebih tahan lama akan menjadi plugin rexec untuk ssh, ala plugin ftp.
Otheus

1
@ myrdd, tidak, Anda perlu spasi atau tab untuk memisahkan argumen dalam baris perintah shell. Jika cmdadalah cmd=(echo "foo bar"), baris perintah shell diteruskan ke sshharus sesuatu seperti ` 'echo' 'foo bar' . The *first* space (the one before gema ) 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'` baris perintah.
Stéphane Chazelas

Jawaban:


19

Saya tidak berpikir implementasi apapun sshmemiliki cara asli untuk meneruskan perintah dari klien ke server tanpa melibatkan shell.

Sekarang, segala sesuatunya dapat menjadi lebih mudah jika Anda dapat memberi tahu remote shell untuk hanya menjalankan juru bahasa tertentu (seperti sh, yang kami tahu sintaks yang diharapkan) dan memberikan kode untuk dieksekusi dengan cara lain.

Itu berarti lain dapat misalnya input standar atau variabel lingkungan .

Ketika tidak ada yang dapat digunakan, saya mengusulkan solusi ketiga yang hacky di bawah ini.

Menggunakan stdin

Jika Anda tidak perlu memasukkan data apa pun ke perintah jarak jauh, itu solusi termudah.

Jika Anda tahu host jarak jauh memiliki xargsperintah yang mendukung -0opsi dan perintahnya tidak terlalu besar, Anda dapat melakukannya:

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

Itu xargs -0 env --baris perintah ditafsirkan sama dengan semua keluarga shell mereka. xargsmembaca daftar argumen tanpa batas pada stdin dan meneruskannya sebagai argumen env. Itu mengasumsikan argumen pertama (nama perintah) tidak mengandung =karakter.

Atau Anda dapat menggunakan shpada host jarak jauh setelah mengutip setiap elemen menggunakan shsintaks mengutip.

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

Menggunakan variabel lingkungan

Sekarang, jika Anda perlu memberi makan beberapa data dari klien ke stdin perintah jarak jauh, solusi di atas tidak akan berfungsi.

Namun beberapa sshpenyebaran server memungkinkan berlalunya variabel lingkungan sewenang-wenang dari klien ke server. Sebagai contoh, banyak penyebaran openssh pada sistem berbasis Debian memungkinkan lewat variabel yang namanya dimulai LC_.

Dalam kasus tersebut, Anda bisa memiliki LC_CODEvariabel misalnya berisi kode shquoted sh seperti di atas dan dijalankan sh -c 'eval "$LC_CODE"'pada host jarak jauh setelah menyuruh klien Anda untuk mengirimkan variabel itu (sekali lagi, itu adalah baris perintah yang diartikan sama di setiap shell):

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

Membangun baris perintah yang kompatibel untuk semua keluarga shell

Jika tidak ada opsi di atas yang dapat diterima (karena Anda perlu stdin dan sshd tidak menerima variabel apa pun, atau karena Anda memerlukan solusi generik), maka Anda harus menyiapkan baris perintah untuk host jarak jauh yang kompatibel dengan semua kerang yang didukung.

Itu sangat sulit karena semua cangkang (Bourne, csh, rc, es, fish) memiliki sintaks yang berbeda, dan khususnya mekanisme kutipan yang berbeda dan beberapa dari mereka memiliki keterbatasan yang sulit untuk dikerjakan.

Berikut adalah solusi yang saya buat, saya jelaskan lebih jauh ke bawah:

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

Itu perlskrip pembungkus di sekitar ssh. Saya menyebutnya sexec. Anda menyebutnya seperti:

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

jadi dalam contoh Anda:

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

Dan pembungkusnya berubah cmd and its argsmenjadi baris perintah yang akhirnya ditafsirkan oleh semua shell sebagai panggilan cmddengan argumennya (tanpa konten).

Keterbatasan:

  • Pembukaan dan cara perintah dikutip berarti garis perintah jarak jauh berakhir secara signifikan lebih besar yang berarti batas ukuran maksimum dari baris perintah akan tercapai lebih cepat.
  • Saya hanya mengujinya dengan: Bourne shell (dari heirloom toolchest), tanda hubung, bash, zsh, mksh, lksh, yash, ksh93, rc, es, akanga, csh, tcsh, ikan seperti yang ditemukan pada sistem Debian baru-baru ini dan / bin / sh, / usr / bin / ksh, / bin / csh dan / usr / xpg4 / bin / sh pada Solaris 10.
  • Jika yashshell login jarak jauh, Anda tidak bisa meneruskan perintah yang argumennya berisi karakter yang tidak valid, tetapi itu adalah batasan di yashmana Anda tidak bisa menyelesaikannya.
  • Beberapa shell seperti csh atau bash membaca beberapa file startup ketika dipanggil melalui ssh. Kami menganggap itu tidak mengubah perilaku secara dramatis sehingga pembukaan masih berfungsi.
  • selain shitu, ia juga menganggap sistem remote memiliki printfperintah.

Untuk memahami cara kerjanya, Anda perlu tahu cara mengutip bekerja di shell yang berbeda:

  • Bourne: '...'adalah kutipan kuat tanpa karakter khusus di dalamnya. "..."adalah tanda kutip yang lemah di mana "bisa lolos dengan backslash.
  • csh. Sama seperti Bourne kecuali itu "tidak bisa diloloskan ke dalam "...". Juga karakter baris baru harus dimasukkan diawali dengan garis miring terbalik. Dan !menyebabkan masalah bahkan di dalam tanda kutip tunggal.
  • rc. Satu-satunya kutipan adalah '...'(kuat). Kutipan tunggal dalam kutipan tunggal dimasukkan sebagai ''(seperti '...''...'). Kutipan ganda atau garis miring tidak spesial.
  • es. Sama seperti rc kecuali kutipan luar, backslash dapat lolos dari penawaran tunggal.
  • fish: sama seperti Bourne kecuali bahwa backslash lolos 'di dalam '...'.

Dengan semua hambatan itu, mudah untuk melihat bahwa seseorang tidak dapat dengan baik mengutip argumen baris perintah sehingga ia bekerja dengan semua shell.

Menggunakan tanda kutip tunggal seperti pada:

'foo' 'bar'

bekerja di semua tetapi:

'echo' 'It'\''s'

tidak akan bekerja di rc.

'echo' 'foo
bar'

tidak akan bekerja di csh.

'echo' 'foo\'

tidak akan bekerja di fish.

Namun kita harus dapat mengatasi sebagian besar masalah tersebut jika kita berhasil menyimpan karakter-karakter bermasalah tersebut dalam variabel, seperti backslash in $b, single quote in $q, newline in $n(dan !in $xuntuk ekspansi sejarah csh) dengan cara independen shell.

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

akan bekerja di semua kulit. Itu masih tidak akan bekerja untuk baris baru csh. Jika $nberisi baris baru, dalam csh, Anda harus menulisnya $n:qagar bisa meluas ke baris baru dan itu tidak akan berfungsi untuk shell lain. Jadi, apa yang akhirnya kami lakukan di sini adalah menelepon shdan shmemperluasnya $n. Itu juga berarti harus melakukan dua tingkat penawaran, satu untuk shell login jarak jauh, dan satu untuk sh.

Di $preambledalam kode itu adalah bagian tersulit. Itu membuat penggunaan berbagai aturan mengutip berbeda dalam semua kerang untuk memiliki beberapa bagian dari kode ditafsirkan oleh hanya satu dari kerang (sementara itu komentar untuk orang lain) yang masing-masing hanya mendefinisikan mereka $b, $q, $n, $xvariabel untuk shell masing-masing.

Inilah kode shell yang akan ditafsirkan oleh shell login pengguna jarak jauh hostuntuk contoh Anda:

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

Kode itu akhirnya menjalankan perintah yang sama ketika ditafsirkan oleh salah satu shell yang didukung.


1
Protokol SSH ( RFC 4254 §6.5 ) mendefinisikan perintah jarak jauh sebagai string. Terserah server untuk memutuskan bagaimana menafsirkan string itu. Pada sistem Unix, interpretasi normal adalah meneruskan string ke shell login pengguna. Untuk akun terbatas, itu bisa berupa rssh atau rush yang tidak menerima perintah sewenang-wenang. Bahkan mungkin ada perintah yang dipaksakan pada akun atau pada kunci yang menyebabkan string perintah yang dikirim oleh klien diabaikan.
Gilles 'SANGAT berhenti menjadi jahat'

1
@Gilles, terima kasih untuk referensi RFC. Ya, asumsi untuk T&J ini adalah bahwa shell login dari pengguna jarak jauh dapat digunakan (karena saya dapat menjalankan perintah jarak jauh yang ingin saya jalankan) dan salah satu keluarga shell utama pada sistem POSIX. Saya tidak tertarik pada shell terbatas atau non-shells atau memaksa perintah atau apapun yang tidak memungkinkan saya menjalankan perintah jarak jauh itu.
Stéphane Chazelas

1
Referensi yang berguna tentang perbedaan utama dalam sintaksis antara beberapa shell umum dapat ditemukan di Hyperpolyglot .
lcd047

0

tl; dr

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

Untuk solusi yang lebih rumit, baca komentar dan periksa jawaban lainnya .

deskripsi

Nah, solusi saya tidak akan bekerja dengan non- bashshell. Tetapi dengan asumsi itu bashdi ujung yang lain, segalanya menjadi lebih sederhana. Ide saya adalah menggunakan kembali printf "%q"untuk melarikan diri. Juga umumnya, lebih mudah dibaca untuk memiliki skrip di ujung yang lain, yang menerima argumen. Tetapi jika perintahnya singkat, mungkin tidak apa-apa untuk memasukkannya. Berikut adalah beberapa contoh fungsi yang digunakan dalam skrip:

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

Hasil:

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

Atau, Anda dapat melakukan printfpekerjaan sendiri, jika Anda tahu apa yang Anda lakukan:

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

1
Itu mengasumsikan shell login dari pengguna jarak jauh adalah bash (seperti printf% q bash mengutip dalam mode bash) dan yang bashtersedia pada mesin remote. Ada juga beberapa masalah dengan kutipan yang hilang yang akan menyebabkan masalah dengan spasi putih dan wildcard.
Stéphane Chazelas

@ StéphaneChazelas Memang, solusi saya mungkin hanya menargetkan bashshell. Tapi semoga orang akan menemukannya berguna. Saya mencoba untuk mengatasi masalah lain. Jangan ragu untuk memberi tahu saya jika ada sesuatu yang saya lewatkan selain bashhal itu.
x-yuri

1
Perhatikan bahwa itu masih tidak berfungsi dengan perintah sampel dalam pertanyaan ( ssh_run user@host "${cmd[@]}"). Anda masih memiliki beberapa kutipan yang hilang.
Stéphane Chazelas

1
Itu lebih baik. Perhatikan bahwa output bash printf %qtidak aman untuk digunakan di lokal yang berbeda (dan juga cukup buggy; misalnya di lokal menggunakan charset BIG5, ia (4.3.48) mengutip εsebagai α`!). Untuk itu, yang terbaik adalah mengutip segala sesuatu dan hanya dengan satu tanda kutip seperti dengan shquote()jawaban saya.
Stéphane Chazelas
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.