วิธีการรันคำสั่งแบบง่ายโดยพลการบน ssh โดยไม่ทราบว่าเชลล์ล็อกอินของผู้ใช้รีโมต


26

ssh มีคุณสมบัติที่น่ารำคาญเมื่อคุณเรียกใช้:

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

แทนที่จะเรียกใช้งานโดยที่cmdมีการขัดแย้งบนhostมันจะเชื่อมต่อcmdและขัดแย้งกับช่องว่างและรันเชลล์hostเพื่อตีความสตริงผลลัพธ์ (ฉันเดาว่านั่นเป็นสาเหตุที่เรียกว่าsshไม่ใช่sexec)

ยิ่งกว่านั้นคุณไม่รู้ว่าเชลล์จะใช้อะไรในการตีความสตริงนั้นเพราะนั่นคือเชลล์ล็อกอินuserที่ไม่ได้รับประกันว่าจะเป็นบอร์นเหมือนที่มีคนใช้tcshเป็นเชลล์ล็อกอินfishอยู่และกำลังเพิ่มขึ้นเรื่อย ๆ

มีวิธีแก้ไขไหม?

สมมติว่าฉันมีคำสั่งเป็นรายการของอาร์กิวเมนต์เก็บไว้ในที่bashอาร์เรย์แต่ละที่อาจจะมีลำดับของไบต์ไม่ใช่ null ใด ๆ จะมีวิธีใดที่จะมีมันดำเนินการในhostขณะที่userในลักษณะที่สอดคล้องกันโดยไม่คำนึงถึงเปลือกเข้าสู่ระบบที่userเกี่ยวกับhost(ซึ่งเราจะถือว่าเป็นหนึ่งในตระกูลเชลล์ที่สำคัญของ Unix: Bourne, csh, rc / es, fish)?

อีกข้อสันนิษฐานที่สมเหตุสมผลที่ฉันควรจะทำก็คือมีshคำสั่งhostให้ใช้งานได้ใน$PATHที่นั้นเข้ากันได้กับบอร์น

ตัวอย่าง:

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

@Banguanin ไม่ถ้าคุณเรียกใช้ssh host sh -c 'some cmd'เช่นเดียวกับssh host 'sh -c some cmd'ที่มีเปลือกเข้าสู่ระบบของผู้ใช้ระยะไกลตีความsh -c some cmdบรรทัดคำสั่งนั้น เราจำเป็นต้องเขียนคำสั่งในรูปแบบที่ถูกต้องสำหรับเชลล์นั้น (และเราไม่รู้ว่ามันคืออะไร) เพื่อที่shจะถูกเรียกใช้พร้อมกับ-cและsome cmdอาร์กิวเมนต์
Stéphane Chazelas

1
@Othus ใช่แล้วsh -c 'some cmd'และsome cmdบรรทัดคำสั่งจะถูกตีความเหมือนกันในเชลล์เหล่านั้นทั้งหมด ตอนนี้ถ้าฉันต้องการรันecho \'บรรทัดคำสั่ง Bourne บนรีโมตโฮสต์? echo command-string | ssh ... /bin/shเป็นทางออกหนึ่งที่ฉันให้ในคำตอบของฉัน แต่นั่นหมายความว่าคุณไม่สามารถป้อนข้อมูลไปยัง stdin ของคำสั่งระยะไกล
Stéphane Chazelas

1
ฟังดูเหมือนโซลูชันที่ยาวนานกว่านี้จะเป็นปลั๊กอิน rexec สำหรับ ssh หรือใช้ปลั๊กอิน ftp
Otheus

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'` บรรทัดคำสั่ง
Stéphane Chazelas

คำตอบ:


19

ฉันไม่คิดว่าการติดตั้งใด ๆsshมีวิธีเนทีฟในการส่งคำสั่งจากไคลเอ็นต์ไปยังเซิร์ฟเวอร์โดยไม่ต้องใช้เชลล์

ตอนนี้สิ่งต่าง ๆ สามารถทำได้ง่ายขึ้นถ้าคุณสามารถบอกให้รีโมตเชลล์ทำงานเฉพาะล่ามที่เฉพาะเจาะจง (เช่นshที่เรารู้จักไวยากรณ์ที่คาดหมาย) และให้โค้ดเพื่อรันโดยค่าเฉลี่ยอื่น

ที่มีความหมายอื่น ๆ สามารถเช่นเข้ามาตรฐานหรือตัวแปรสภาพแวดล้อม

เมื่อไม่สามารถใช้งานได้ฉันขอเสนอวิธีแก้ปัญหาที่สามด้านล่าง

ใช้ stdin

หากคุณไม่จำเป็นต้องป้อนข้อมูลใด ๆ ไปยังคำสั่งระยะไกลนั่นเป็นทางออกที่ง่ายที่สุด

หากคุณรู้ว่ารีโมตโฮสต์มีxargsคำสั่งที่สนับสนุน-0ตัวเลือกและคำสั่งนั้นไม่ใหญ่เกินไปคุณสามารถทำได้:

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

ที่xargs -0 env --บรรทัดคำสั่งถูกตีความเดียวกันกับทุกคนในครอบครัวเปลือก xargsอ่านรายการ null จุลภาคของข้อโต้แย้งใน 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

การใช้ตัวแปรสภาพแวดล้อม

ตอนนี้ถ้าคุณจำเป็นต้องป้อนข้อมูลบางอย่างจากไคลเอนต์ไปยัง stdin ของคำสั่งระยะไกลการแก้ปัญหาข้างต้นจะไม่ทำงาน

sshการปรับใช้เซิร์ฟเวอร์บางอย่างอย่างไรก็ตามอนุญาตให้ส่งผ่านตัวแปรสภาพแวดล้อมโดยพลการจากไคลเอนต์ไปยังเซิร์ฟเวอร์ ยกตัวอย่างเช่นระบบการใช้งานหลาย openssh ใน Debian LC_ตามอนุญาตให้ผ่านตัวแปรที่มีชื่อเริ่มต้นด้วย

ในกรณีดังกล่าวคุณอาจมีLC_CODEตัวแปรเช่นที่มีโค้ดshquoted 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[@]}"

และ wrapper เปลี่ยนcmd and its argsเป็นบรรทัดคำสั่งที่เชลล์ทั้งหมดจบการตีความเป็นการเรียกcmdด้วย args (ไม่นับเนื้อหาของมัน)

ข้อ จำกัด :

  • คำนำและวิธีการอ้างอิงคำสั่งหมายถึงบรรทัดคำสั่งรีโมตจะใหญ่ขึ้นอย่างมีนัยสำคัญซึ่งหมายความว่าจะมีการ จำกัด ขนาดสูงสุดของบรรทัดคำสั่งในไม่ช้า
  • ฉันได้ทดสอบด้วย: Bourne shell (จากมรดกสืบทอด), ประ, ทุบตี, zsh, mksh, lksh, yash, ksh93, rc, es, akanga, csh, tcsh, ปลาที่พบในระบบ Debian ล่าสุดและ / bin / sh, / usr / bin / ksh, / bin / csh และ / usr / xpg4 / bin / sh บน Solaris 10
  • หากyashเป็นเชลล์การเข้าสู่ระบบระยะไกลคุณจะไม่สามารถส่งคำสั่งที่อาร์กิวเมนต์มีอักขระที่ไม่ถูกต้อง แต่นั่นเป็นข้อ จำกัดyashที่คุณไม่สามารถแก้ไขได้
  • เชลล์บางตัวเช่น csh หรือ bash อ่านไฟล์เริ่มต้นบางไฟล์เมื่อเรียกใช้ผ่าน ssh เราคิดว่าสิ่งเหล่านั้นจะไม่เปลี่ยนพฤติกรรมอย่างมากเพื่อให้การเริ่มนำยังคงใช้ได้
  • ข้างshก็ยังถือว่าระบบจากระยะไกลมีprintfคำสั่ง

เพื่อให้เข้าใจถึงวิธีการทำงานคุณต้องรู้ว่าการอ้างอิงทำงานอย่างไรในเชลล์ที่แตกต่างกัน:

  • Bourne: '...'เป็นคำพูดที่แข็งแกร่งโดยไม่มีตัวอักษรพิเศษอยู่ในนั้น "..."เป็นเครื่องหมายคำพูดอ่อนแอที่"สามารถใช้เครื่องหมายแบคสแลชได้
  • csh. เหมือนกับ Bourne ยกเว้นว่า"ไม่สามารถหลบหนี"..."ได้ นอกจากนี้ยังต้องป้อนอักขระขึ้นบรรทัดใหม่ด้วยเครื่องหมายแบ็กสแลช และ!ทำให้เกิดปัญหาแม้ในเครื่องหมายคำพูดเดี่ยว
  • 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 หรือเร่งด่วนที่ไม่ยอมรับคำสั่งโดยพลการ อาจมีคำสั่งบังคับใช้กับบัญชีหรือบนคีย์ที่ทำให้สตริงคำสั่งที่ส่งโดยไคลเอนต์ถูกละเว้น
Gilles 'หยุดความชั่วร้าย' Gilles

1
@Gilles ขอบคุณสำหรับการอ้างอิง RFC ใช่ข้อสมมติฐานสำหรับคำถาม & คำตอบนี้คือล็อกอินเชลล์ของผู้ใช้รีโมตสามารถใช้งานได้ (ในขณะที่ฉันสามารถรันคำสั่งรีโมตที่ฉันต้องการรัน) และหนึ่งในตระกูลเชลล์หลักบนระบบ POSIX ฉันไม่สนใจเชลล์ที่ จำกัด หรือไม่ใช่เชลล์หรือคำสั่งบังคับหรืออะไรก็ตามที่ไม่อนุญาตให้ฉันเรียกใช้คำสั่งระยะไกล
Stéphane Chazelas

1
การอ้างอิงที่เป็นประโยชน์เกี่ยวกับความแตกต่างหลักไวยากรณ์ระหว่างเปลือกหอยบางอย่างร่วมกันสามารถพบได้ที่Hyperpolyglot
lcd047

0

TL; DR

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 (เนื่องจากคำสั่ง printf% q ของ bash ใน bash fashion) และที่bashมีอยู่ในเครื่องรีโมต นอกจากนี้ยังมีปัญหาเล็กน้อยเกี่ยวกับคำพูดที่หายไปซึ่งจะทำให้เกิดปัญหากับช่องว่างและสัญลักษณ์แทน
Stéphane Chazelas

@ StéphaneChazelasแน่นอนว่าโซลูชันของฉันอาจกำหนดเป้าหมายไปที่bashหอยเท่านั้น แต่หวังว่าผู้คนจะพบว่ามันใช้งานได้ ฉันพยายามที่จะแก้ไขปัญหาอื่น ๆ แม้ว่า อย่าลังเลที่จะบอกฉันว่ามีอะไรที่ฉันขาดไปนอกเหนือจากbashสิ่งนั้น
x-yuri

1
โปรดทราบว่ามันยังไม่สามารถใช้ได้กับคำสั่งตัวอย่างในคำถาม ( ssh_run user@host "${cmd[@]}") คุณยังมีคำพูดที่หายไป
Stéphane Chazelas

1
นั่นดีกว่า. โปรดทราบว่าผลลัพธ์ของ bash printf %qนั้นไม่ปลอดภัยในการใช้งานในสถานที่ที่แตกต่างกัน (และค่อนข้างเป็นบั๊กกี้เช่นใน locales ที่ใช้ชุดอักขระ BIG5 มันจะแสดงเครื่องหมาย (4.3.48) εเป็นα`!) เพื่อที่ดีที่สุดคือการพูดทุกอย่างและด้วยคำพูดเดียวเช่นเดียวกับshquote()ในคำตอบของฉัน
Stéphane Chazelas
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.