Làm thế nào để thực thi một lệnh đơn giản tùy ý trên ssh mà không biết shell đăng nhập của người dùng từ xa?


26

ssh có một tính năng khó chịu khi bạn chạy:

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

Thay vì chạy nó cmdvới các đối số của nó trên host, nó kết hợp điều đó cmdvà đối số với khoảng trắng và chạy shell hostđể giải thích chuỗi kết quả (tôi đoán đó là lý do tại sao nó được gọi sshvà không sexec).

Tồi tệ hơn, bạn không biết shell nào sẽ được sử dụng để diễn giải chuỗi đó vì đó là shell đăng nhập userthậm chí không được đảm bảo là Bourne vì vẫn có người sử dụng tcshlàm vỏ đăng nhập của họ và fishđang gia tăng.

Có cách nào xung quanh đó không?

Giả sử tôi có một lệnh như một danh sách các đối số được lưu trữ trong một bashmảng, mỗi trong số đó có thể chứa bất kỳ chuỗi không null byte, có cách nào để có nó thực thi trên hostnhư usermột cách phù hợp không phụ thuộc vào vỏ đăng nhập đó usertrên host(mà chúng ta sẽ cho là một trong những họ shell Unix chính: Bourne, csh, rc / es, fish)?

Một giả định hợp lý khác mà tôi có thể đưa ra là có một shlệnh hostcó sẵn trong $PATHđó là tương thích với Bourne.

Thí dụ:

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

Tôi có thể chạy nó cục bộ với ksh/ zsh/ bash/ yashnhư:

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

hoặc là

env "${cmd[@]}"

hoặc là

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

Làm thế nào tôi sẽ chạy nó trên hostnhư usertrên ssh?

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

rõ ràng sẽ không làm việc.

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

sẽ chỉ hoạt động nếu shell đăng nhập của người dùng từ xa giống như shell cục bộ (hoặc hiểu trích dẫn theo cách tương tự như printf %qtrong shell cục bộ tạo ra nó) và chạy trong cùng một miền.


3
Nếu cmdtranh luận là /bin/sh -cchúng ta sẽ kết thúc với một vỏ posix trong 99% của tất cả các trường hợp, phải không? Tất nhiên thoát khỏi các nhân vật đặc biệt là một chút đau đớn hơn theo cách này, nhưng nó sẽ giải quyết vấn đề ban đầu?
Bananguin

@Bananguin, không nếu bạn chạy ssh host sh -c 'some cmd', giống như ssh host 'sh -c some cmd', có vỏ đăng nhập của người dùng từ xa diễn giải sh -c some cmddòng lệnh đó . Chúng ta cần viết lệnh theo đúng cú pháp cho shell đó (và chúng ta không biết đó là gì) để shđược gọi ở đó với -cvà các some cmdđối số.
Stéphane Chazelas

1
@Otheus, vâng, các dòng lệnh sh -c 'some cmd'some cmdlệnh xảy ra giống nhau trong tất cả các shell đó. Bây giờ nếu tôi muốn chạy echo \'dòng lệnh Bourne trên máy chủ từ xa thì sao? echo command-string | ssh ... /bin/shlà một giải pháp tôi đã đưa ra trong câu trả lời của mình, nhưng điều đó có nghĩa là bạn không thể cung cấp dữ liệu cho stdin của lệnh từ xa đó.
Stéphane Chazelas

1
Âm thanh như một giải pháp lâu dài hơn sẽ là một plugin rexec cho ssh, ala plugin ftp.
Otheus

1
@myrdd, không, không cần, bạn cần khoảng trắng hoặc tab để phân tách các đối số trong một dòng lệnh shell. Nếu cmdcmd=(echo "foo bar"), dòng lệnh shell được truyền vào sshphải là một dòng như '' 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'` dòng lệnh.
Stéphane Chazelas

Câu trả lời:


19

Tôi không nghĩ rằng bất kỳ triển khai nào sshcũng có cách riêng để truyền lệnh từ máy khách đến máy chủ mà không liên quan đến trình bao.

Bây giờ, mọi thứ có thể trở nên dễ dàng hơn nếu bạn có thể yêu cầu trình điều khiển từ xa chỉ chạy một trình thông dịch cụ thể (như sh, chúng tôi biết cú pháp dự kiến) và cung cấp mã để thực thi theo nghĩa khác.

Điều đó có nghĩa là có thể là đầu vào tiêu chuẩn hoặc một biến môi trường .

Khi không thể được sử dụng, tôi đề xuất một giải pháp thứ ba hacky dưới đây.

Sử dụng stdin

Nếu bạn không cần cung cấp bất kỳ dữ liệu nào cho lệnh từ xa, đó là giải pháp đơn giản nhất.

Nếu bạn biết máy chủ từ xa có một xargslệnh hỗ trợ -0tùy chọn và lệnh không quá lớn, bạn có thể thực hiện:

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

xargs -0 env --Dòng lệnh đó được giải thích giống nhau với tất cả các họ shell. xargsđọc danh sách các đối số được phân tách bằng null trên stdin và chuyển chúng làm đối số cho env. Giả sử đối số đầu tiên (tên lệnh) không chứa =ký tự.

Hoặc bạn có thể sử dụng shtrên máy chủ từ xa sau khi đã trích dẫn từng phần tử bằng cách sử dụng shcú pháp trích dẫn.

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

Sử dụng biến môi trường

Bây giờ, nếu bạn cần cung cấp một số dữ liệu từ máy khách đến stdin của lệnh từ xa, giải pháp trên sẽ không hoạt động.

sshTuy nhiên, một số triển khai máy chủ cho phép chuyển các biến môi trường tùy ý từ máy khách sang máy chủ. Chẳng hạn, nhiều triển khai openssh trên các hệ thống dựa trên Debian cho phép truyền các biến có tên bắt đầu bằng LC_.

Trong những trường hợp, bạn có thể có một LC_CODEbiến ví dụ chứa shquoted sh mã như ở trên và chạy sh -c 'eval "$LC_CODE"'trên máy chủ từ xa sau khi đã nói với khách hàng của bạn để vượt qua biến mà (một lần nữa, đó là một dòng lệnh của hiểu như nhau trong mỗi vỏ):

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

Xây dựng một dòng lệnh tương thích với tất cả các họ shell

Nếu không có tùy chọn nào ở trên chấp nhận được (vì bạn cần stdin và sshd không chấp nhận bất kỳ biến nào hoặc vì bạn cần một giải pháp chung), thì bạn sẽ phải chuẩn bị một dòng lệnh cho máy chủ từ xa tương thích với tất cả hỗ trợ đạn pháo.

Điều đó đặc biệt khó khăn bởi vì tất cả các shell đó (Bourne, csh, rc, es, fish) có cú pháp khác nhau, và đặc biệt là các cơ chế trích dẫn khác nhau và một số trong số chúng có những hạn chế khó khắc phục.

Đây là một giải pháp tôi đã đưa ra, tôi mô tả nó sâu hơn:

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

Đó là một perlkịch bản bao bọc xung quanh ssh. Tôi gọi nó sexec. Bạn gọi nó như:

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

vì vậy trong ví dụ của bạn:

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

Và trình bao bọc biến cmd and its argsthành một dòng lệnh mà tất cả các shell kết thúc bằng cách gọi cmdvới các đối số của nó (không có nội dung của chúng).

Hạn chế:

  • Lời mở đầu và cách trích dẫn lệnh có nghĩa là dòng lệnh từ xa kết thúc lớn hơn đáng kể, điều đó có nghĩa là giới hạn về kích thước tối đa của một dòng lệnh sẽ đạt được sớm hơn.
  • Tôi chỉ thử nghiệm nó với: Bourne shell (từ công cụ gia truyền), dash, bash, zsh, mksh, lksh, yash, ksh93, rc, es, akanga, csh, tcsh, fish như được tìm thấy trên hệ thống Debian gần đây và / bin / sh, / usr / bin / ksh, / bin / csh và / usr / xpg4 / bin / sh trên Solaris 10.
  • Nếu yashlà vỏ đăng nhập từ xa, bạn không thể truyền lệnh có đối số chứa các ký tự không hợp lệ, nhưng đó là hạn chế ở yashchỗ bạn không thể làm việc xung quanh.
  • Một số shell như csh hoặc bash đọc một số tệp khởi động khi được gọi qua ssh. Chúng tôi cho rằng những người không thay đổi hành vi một cách đáng kể để lời mở đầu vẫn hoạt động.
  • bên cạnh sh, nó cũng giả sử hệ thống từ xa có printflệnh.

Để hiểu cách thức hoạt động của nó, bạn cần biết cách trích dẫn hoạt động trong các shell khác nhau:

  • Bourne: '...'là những trích dẫn mạnh mẽ không có ký tự đặc biệt trong đó. "..."là những trích dẫn yếu nơi "có thể thoát được bằng dấu gạch chéo ngược.
  • csh. Giống như Bourne ngoại trừ việc "không thể trốn thoát bên trong "...". Ngoài ra, một ký tự dòng mới phải được nhập bằng tiền tố với dấu gạch chéo ngược. Và !gây ra vấn đề ngay cả trong dấu ngoặc đơn.
  • rc. Các trích dẫn duy nhất là '...'(mạnh). Một trích dẫn trong dấu ngoặc đơn được nhập dưới dạng ''(như '...''...'). Dấu ngoặc kép hoặc dấu gạch chéo ngược không đặc biệt.
  • es. Giống như RC ngoại trừ các trích dẫn bên ngoài, dấu gạch chéo ngược có thể thoát khỏi một trích dẫn.
  • fish: giống như Bourne ngoại trừ dấu gạch chéo ngược thoát ra 'bên trong '...'.

Với tất cả các mâu thuẫn đó, thật dễ dàng để thấy rằng người ta không thể trích dẫn một cách đáng tin cậy các đối số dòng lệnh để nó hoạt động với tất cả các shell.

Sử dụng dấu ngoặc đơn như trong:

'foo' 'bar'

hoạt động trong tất cả nhưng:

'echo' 'It'\''s'

sẽ không làm việc trong rc.

'echo' 'foo
bar'

sẽ không làm việc trong csh.

'echo' 'foo\'

sẽ không làm việc trong fish.

Tuy nhiên chúng tôi sẽ có thể làm việc xung quanh hầu hết những vấn đề nếu chúng ta quản lý để lưu trữ những ký tự có vấn đề trong các biến, giống như dấu chéo ngược trong $b, dấu ngoặc đơn trong $q, xuống dòng trong $n(và !trong $xcho csh mở rộng lịch sử) một cách độc lập vỏ.

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

sẽ làm việc trong tất cả các vỏ. Điều đó vẫn sẽ không làm việc cho dòng mới cshmặc dù. Nếu $ncó dòng mới, trong csh, bạn phải viết $n:qnó để mở rộng sang dòng mới và nó sẽ không hoạt động cho các shell khác. Vì vậy, những gì chúng ta cuối cùng làm thay vì ở đây là gọi shvà đã shmở rộng chúng $n. Điều đó cũng có nghĩa là phải thực hiện hai cấp độ trích dẫn, một cho vỏ đăng nhập từ xa và một cho sh.

Trong $preamblemã đó là phần khó nhất. Nó làm cho việc sử dụng quy tắc trích dẫn khác nhau khác nhau trong tất cả các vỏ có một số phần của mã giải thích bởi chỉ một trong những vỏ (trong khi nó đang nhận xét ra cho những người khác) mỗi trong số đó chỉ cần xác định những $b, $q, $n, $xbiến cho vỏ tương ứng của họ.

Đây là mã shell sẽ được giải thích bởi shell đăng nhập của người dùng từ xa trên hoství dụ của bạn:

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

Mã đó kết thúc bằng cách chạy cùng một lệnh khi được giải thích bởi bất kỳ shell nào được hỗ trợ.


1
Giao thức SSH ( RFC 4254 §6.5 ) định nghĩa một lệnh từ xa là một chuỗi. Tùy thuộc vào máy chủ quyết định cách giải thích chuỗi đó. Trên các hệ thống Unix, cách hiểu thông thường là chuyển chuỗi sang shell đăng nhập của người dùng. Đối với một tài khoản bị hạn chế, đó có thể là một cái gì đó như rssh hoặc rush không chấp nhận các lệnh tùy ý. Thậm chí có thể có một lệnh bắt buộc trên tài khoản hoặc trên khóa khiến chuỗi lệnh được gửi bởi máy khách bị bỏ qua.
Gilles 'SO- ngừng trở nên xấu xa'

1
@Gilles, cảm ơn bạn đã tham khảo RFC. Có, giả định cho câu hỏi và trả lời này là shell đăng nhập của người dùng từ xa có thể sử dụng được (vì tôi có thể chạy lệnh từ xa mà tôi muốn chạy) và một trong những họ shell chính trên hệ thống POSIX. Tôi không quan tâm đến các shell bị giới hạn hoặc các shell không hoặc các lệnh bắt buộc hoặc bất cứ thứ gì không cho phép tôi chạy lệnh từ xa đó.
Stéphane Chazelas

1
Một tài liệu tham khảo hữu ích về sự khác biệt chính trong cú pháp giữa một số shell phổ biến có thể được tìm thấy tại Hyperpolyglot .
lcd047

0

tl; dr

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

Đối với một giải pháp phức tạp hơn, đọc các ý kiến ​​và kiểm tra câu trả lời khác .

sự miêu tả

Chà, giải pháp của tôi sẽ không hoạt động với phi bashvỏ. Nhưng giả sử nó bashở đầu bên kia, mọi thứ trở nên đơn giản hơn. Ý tưởng của tôi là tái sử dụng printf "%q"để trốn thoát. Nói chung, dễ đọc hơn khi có một tập lệnh ở đầu bên kia, chấp nhận các đối số. Nhưng nếu lệnh ngắn, có thể ổn định nội tuyến. Dưới đây là một số chức năng ví dụ để sử dụng trong các tập lệnh:

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

Đầu ra:

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

Ngoài ra, bạn có thể tự làm printfcông việc của mình, nếu bạn biết bạn đang làm gì:

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

1
Giả sử vỏ đăng nhập của người dùng từ xa là bash (như bash's printf% q trích dẫn theo kiểu bash) và bashcó sẵn trên máy từ xa. Cũng có một vài vấn đề với các trích dẫn bị thiếu sẽ gây ra vấn đề với khoảng trắng và ký tự đại diện.
Stéphane Chazelas

@ StéphaneChazelas Thật vậy, giải pháp của tôi có lẽ chỉ nhắm vào bashđạn pháo. Nhưng hy vọng mọi người sẽ tìm thấy nó sử dụng. Tôi đã cố gắng để giải quyết các vấn đề khác mặc dù. Vui lòng cho tôi biết nếu có bất cứ điều gì tôi thiếu ngoài bashđiều đó.
x-yuri

1
Lưu ý rằng nó vẫn không hoạt động với lệnh mẫu trong câu hỏi ( ssh_run user@host "${cmd[@]}"). Bạn vẫn còn thiếu một số trích dẫn.
Stéphane Chazelas

1
Cái đó tốt hơn. Lưu ý rằng đầu ra của bash's printf %qkhông an toàn để sử dụng ở một địa điểm khác (và cũng khá lỗi; ví dụ, tại các địa phương sử dụng bộ ký tự BIG5, nó (4.3.48) trích dẫn εnhư α`!). Đối với điều đó, tốt nhất là trích dẫn tất cả mọi thứ và với trích dẫn duy nhất giống như với shquote()câu trả lời của tôi.
Stéphane Chazelas
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.