Вопрос или проблема
У меня есть минимальный headless *nix, который не имеет никаких командных утилит для скачивания файлов (например, нет curl, wget и т.д.). У меня есть только bash.
Как я могу загрузить файл?
В идеале, я хотел бы решение, которое будет работать на широком диапазоне *nix.
Если у вас bash 2.04 или выше с включенным псевдо-устройством /dev/tcp
, вы можете скачать файл прямо из bash.
Вставьте следующий код непосредственно в оболочку bash (вам не нужно сохранять код в файл для выполнения):
function __wget() {
: ${DEBUG:=0}
local URL=$1
local tag="Connection: close"
local mark=0
if [ -z "${URL}" ]; then
printf "Использование: %s \"URL\" [например: %s http://www.google.com/]" \
"${FUNCNAME[0]}" "${FUNCNAME[0]}"
return 1;
fi
read proto server path <<<$(echo ${URL//// })
DOC=/${path// //}
HOST=${server//:*}
PORT=${server//*:}
[[ x"${HOST}" == x"${PORT}" ]] && PORT=80
[[ $DEBUG -eq 1 ]] && echo "HOST=$HOST"
[[ $DEBUG -eq 1 ]] && echo "PORT=$PORT"
[[ $DEBUG -eq 1 ]] && echo "DOC =$DOC"
exec 3<>/dev/tcp/${HOST}/$PORT
echo -en "GET ${DOC} HTTP/1.1\r\nHost: ${HOST}\r\n${tag}\r\n\r\n" >&3
while read line; do
[[ $mark -eq 1 ]] && echo $line
if [[ "${line}" =~ "${tag}" ]]; then
mark=1
fi
done <&3
exec 3>&-
}
Затем вы можете выполнить это из оболочки следующим образом:
__wget http://example.iana.org/
Источник: ответ Moreaki на вопрос об обновлении и установке пакетов через командную строку cygwin?
Обновление:
как упоминалось в комментарии, вышеописанный подход является упрощенным:
read
будет удалять обратные слэши и ведущие пробелы.- Bash не может хорошо обрабатывать нулевые байты, поэтому бинарные файлы исключаются.
- Неприведенный
$line
будет соответствовать glob.
Используйте lynx.
Это довольно распространено для большинства Unix/Linux.
lynx -dump http://www.google.com
-dump: выводит первый файл в stdout и завершает работу
man lynx
Или netcat:
/usr/bin/printf 'GET / \n' | nc www.google.com 80
Или telnet:
(echo 'GET /'; echo ""; sleep 1; ) | telnet www.google.com 80
Адаптировано из ответа Криса Сноу.
Это также может обрабатывать бинарные файлы.
function __curl() {
read -r proto server path <<<"$(printf '%s' "${1//// }")"
if [ "$proto" != "http:" ]; then
printf >&2 "Извините, %s поддерживает только http\n" "${FUNCNAME[0]}"
return 1
fi
DOC=/${path// //}
HOST=${server//:*}
PORT=${server//*:}
[ "${HOST}" = "${PORT}" ] && PORT=80
exec 3<>"/dev/tcp/${HOST}/$PORT"
printf 'GET %s HTTP/1.0\r\nHost: %s\r\n\r\n' "${DOC}" "${HOST}" >&3
(while read -r line; do
[ "$line" = $'\r' ] && break
done && cat) <&3
exec 3>&-
}
- Я
break && cat
для выхода изread
. - Я использую HTTP 1.0, поэтому нет необходимости ждать/отправлять ‘connection:close’.
Вы можете тестировать бинарные файлы следующим образом:
$ __curl http://www.google.com/favicon.ico > mine.ico
$ curl http://www.google.com/favicon.ico > theirs.ico
$ md5sum mine.ico theirs.ico
f3418a443e7d841097c714d69ec4bcb8 mine.ico
f3418a443e7d841097c714d69ec4bcb8 theirs.ico
Учитывая строгое “только Bash и ничего больше“, вот одна адаптация предыдущих ответов (@Криса, @131), которая не вызывает никаких внешних утилит (даже стандартных), но также работает с бинарными файлами:
#!/bin/bash
download() {
read proto server path <<< "${1//"https://unix.stackexchange.com/"/ }"
DOC=/${path// //}
HOST=${server//:*}
PORT=${server//*:}
[[ x"${HOST}" == x"${PORT}" ]] && PORT=80
exec 3<>/dev/tcp/${HOST}/$PORT
# отправляем запрос
echo -en "GET ${DOC} HTTP/1.0\r\nHost: ${HOST}\r\n\r\n" >&3
# читаем заголовок, он заканчивается пустой строкой (просто CRLF)
while IFS= read -r line ; do
[[ "$line" == $'\r' ]] && break
done <&3
# считываем данные
nul="\0"
while IFS= read -d '' -r x || { nul=""; [ -n "$x" ]; }; do
printf "%s$nul" "$x"
done <&3
exec 3>&-
}
Используйте с download http://path/to/file > file
.
Мы обрабатываем нулевые байты с помощью read -d ''
. Он считывает до нулевого байта и возвращает true, если его нашел, false, если нет. Bash не может обрабатывать нулевые байты в строках, поэтому, когда read
возвращает true, мы вручную добавляем нулевой байт при печати, а когда он возвращает false, мы знаем, что нулевых байтов больше нет, и это должна быть последняя часть данных.
Проверено с Bash 4.4 на файлах с нулями посередине и заканчивающимися на ноль, один или два нуля, а также с бинарниками wget
и curl
из Debian. Бинарный wget
объемом 373 кБ загрузился за примерно 5.7 секунд. Скорость около 65 кБ/с или немного больше 512 кб/с.
В сравнении, решение с cat от @131 заканчивает меньше чем за 0.1 с, или почти в сто раз быстрее. На самом деле, это не удивительно.
Это явно глупо, поскольку без использования внешних утилит мы не можем много сделать с загруженным файлом, даже сделать его исполняемым.
Вместо этого используйте загрузку через SSH с вашей локальной машины
“Минимальный headless *nix” подразумевает, что вы, вероятно, подключаетесь к нему по SSH. Так что вы также можете использовать SSH для загрузки на него. Это функционально эквивалентно загрузке (программных пакетов и т.д.) за исключением, когда вы хотите команду загрузки для включения в сценарий на вашем headless сервере, конечно.
Как показано в этом ответе, вы бы выполнили следующее на вашей локальной машине, чтобы разместить файл на вашем удаленном headless сервере:
wget -O - http://example.com/file.zip | ssh user@host 'cat >/path/to/file.zip'
Более быстрая загрузка через SSH с третьей машины
Недостаток вышеуказанного решения по сравнению со скачиванием – это более низкая скорость передачи, поскольку соединение с вашей локальной машиной обычно имеет гораздо меньшую пропускную способность, чем соединение между вашим headless сервером и другими серверами.
Чтобы решить эту проблему, вы, конечно, можете выполнить вышеуказанную команду на другом сервере с хорошей пропускной способностью. Чтобы сделать это более комфортным (избегая ручного входа на третьей машине), вот команда для выполнения на вашей локальной машине.
Чтобы быть в безопасности, скопируйте и вставьте эту команду включая начальный пробел ' '
. Смотрите объяснения ниже для причины.
ssh user@intermediate-host "sshpass -f <(printf '%s\n' yourpassword) \
ssh -T -e none \
-o StrictHostKeyChecking=no \
< <(wget -O - http://example.com/input-file.zip) \
user@target-host \
'cat >/path/to/output-file.zip' \
"
Объяснения:
-
Команда подключится по SSH к вашей третьей машине
intermediate-host
, начнет загружать файл туда черезwget
и начнет загружать его наtarget-host
через SSH. Загрузка и выгрузка используют пропускную способность вашегоintermediate-host
и происходят одновременно (из-за эквивалентов pipe в Bash), так что прогресс будет быстрым. -
При использовании этого вам нужно заменить две логины серверов (
user@*-host
), пароль целевого хоста (yourpassword
), URL загрузки (http://example.com/…
) и путь вывода на вашем целевом хосте (/path/to/output-file.zip
) на соответствующие собственные значения. -
Для опций SSH
-T -e none
, когда вы используете их для передачи файлов, смотрите эти подробные объяснения. -
Эта команда предназначена для случаев, когда вы не можете использовать механизм аутентификации по публичному ключу SSH – это по-прежнему происходит у некоторых провайдеров общего хостинга, в частности, Host Europe. Чтобы все же автоматизировать процесс, мы полагаемся на
sshpass
, чтобы иметь возможность указать пароль в команде. Это требует установкиsshpass
на вашей промежуточной машине (sudo apt-get install sshpass
в Ubuntu). -
Мы пытаемся использовать
sshpass
безопасным образом, но это все равно не будет так безопасно, как механизм публичного ключа SSH (говоритman sshpass
). В частности, мы указываем пароль SSH не как аргумент командной строки, а через файл, который замещается подстановкой процесса bash, чтобы удостовериться, что он никогда не существует на диске.printf
является встроенной командой bash, что позволяет обеспечить, чтобы эта часть кода не появлялась как отдельная команда в выводеps
, так как это могло бы раскрыть пароль [источник]. Я думаю, что это использованиеsshpass
так же безопасно, как и вариантsshpass -d<file-descriptor>
, рекомендуемый вman sshpass
, потому что bash в любом случае сопоставляет его внутренне к такому файловому дескриптору/dev/fd/*
. И все это без использования временного файла [источник]. Но никаких гарантий, возможно, я что-то упустил. -
Снова, чтобы сделать использование
sshpass
безопасным, нам нужно предотвратить запись команды в историю bash на вашей локальной машине. Для этого вся команда предваряется одним пробелом, что дает этот эффект. -
Часть
-o StrictHostKeyChecking=no
предотвращает сбой команды в случае, если она никогда не подключалась к целевому хосту. (Обычно SSH тогда будет ждать ввода от пользователя для подтверждения попытки подключения. Мы позволяем ей продолжать, чтобы не иметь бесконечно висящей команды на промежуточном хосте.) -
sshpass
ожидает командуssh
илиscp
как последний аргумент. Поэтому нам нужно переписать типичную командуwget -O - … | ssh …
в форму без pipe bash, как объяснено здесь.
Исходя из скрипта Криса Сноу. Я внес некоторые улучшения:
- проверка схемы http (поддерживает только http)
- валидация ответа http (проверка строки статуса ответа,
и разделение заголовка и тела по ‘\r\n’, а не ‘Connection: close’, что иногда неверно) - ошибка для кода, отличного от 200 (это важно для загрузки файлов из интернета)
Вот код:
function __wget() {
: "${DEBUG:=0}"
local URL=$1
local tag="Connection: close"
if [ -z "${URL}" ]; then
printf "Использование: %s \"URL\" [например, %s http://www.google.com/]" \
"${FUNCNAME[0]}" "${FUNCNAME[0]}"
return 1;
fi
read -r proto server path <<< "$(printf '%s' "${URL//// }")"
local SCHEME=${proto//:*}
local PATH=/${path// //}
local HOST=${server//:*}
local PORT=${server//*:}
if [[ "$SCHEME" != "http" ]]; then
printf "Извините, %s поддерживает только http\n" "${FUNCNAME[0]}"
return 1
fi
[[ "${HOST}" == "${PORT}" ]] && PORT=80
[[ "$DEBUG" -eq 1 ]] && echo "SCHEME=$SCHEME" >&2
[[ "$DEBUG" -eq 1 ]] && echo "HOST=$HOST" >&2
[[ "$DEBUG" -eq 1 ]] && echo "PORT=$PORT" >&2
[[ "$DEBUG" -eq 1 ]] && echo "PATH=$PATH" >&2
if ! exec 3<>"/dev/tcp/${HOST}/$PORT"; then
return "$?"
fi
if ! echo -en "GET ${PATH} HTTP/1.1\r\nHost: ${HOST}\r\n${tag}\r\n\r\n" >&3 ; then
return "$?"
fi
# 0: в начале, перед чтением http-ответа
# 1: чтение заголовка
# 2: чтение тела
local state=0
local num=0
local code=0
while read -r line; do
num=$((num + 1))
# проверка http кода
if [ "$state" -eq 0 ]; then
if [ "$num" -eq 1 ]; then
if [[ $line =~ ^HTTP/1\.[01][[:space:]]([0-9]{3}).*$ ]]; then
code="${BASH_REMATCH[1]}"
if [[ "$code" != "200" ]]; then
printf "не удалось выполнить wget '%s', код не 200 (%s)\n" \
"$URL" "$code"
exec 3>&-
return 1
fi
state=1
else
printf "некорректный http ответ от '%s'" "$URL"
exec 3>&-
return 1
fi
fi
elif [ "$state" -eq 1 ]; then
if [[ "$line" == $'\r' ]]; then
# найдено "\r\n"
state=2
fi
elif [ "$state" -eq 2 ]; then
# перенаправляем тело на stdout
# TODO: есть ли способ направить данные прямо на stdout?
echo "$line"
fi
done <&3
exec 3>&-
}
Если у вас есть этот пакет libwww-perl
Вы можете просто использовать:
/usr/bin/GET
Если у вас есть python2:
/usr/bin/python2.7 -c "import sys; import urllib2; exec('try: response = urllib2.urlopen(\'http://localhost:8080/ping\');\nexcept Exception as e: sys.exit(1)')"
Если у вас есть Python 3 на вашем компьютере:
python3 -c 'from urllib.request import urlopen
with urlopen("http://example.com/") as response:
print(response.read())'
Или в одну строку:
python3 -c $'from urllib.request import urlopen\nwith urlopen("http://example.com/") as response:\n print(response.read())'
(По вдохновению от ответа на Python 2.)
Ответ или решение
Если у вас есть минимальная оболочка *nix, которая не имеет командных утилит для загрузки файлов, таких как curl
или wget
, то можно воспользоваться встроенными возможностями Bash для загрузки файлов с использованием специального устройства /dev/tcp
.
Вот шаги, которые вам понадобятся, чтобы написать код для загрузки файла с использованием только Bash:
Шаг 1: Создание функции для загрузки
Вставьте следующий код непосредственно в вашу оболочку Bash. Данная функция осуществляет HTTP-запрос и может использоваться для загрузки файла:
function __wget() {
: "${DEBUG:=0}"
local URL=$1
local tag="Connection: close"
if [ -z "${URL}" ]; then
printf "Использование: %s \"URL\" [пример: %s http://www.google.com/]" \
"${FUNCNAME[0]}" "${FUNCNAME[0]}"
return 1;
fi
read -r proto server path <<< "$(printf '%s' "${URL//// }")"
local SCHEME=${proto//:*}
local PATH=/${path// //}
local HOST=${server//:*}
local PORT=${server//*:}
if [[ "$SCHEME" != "http" ]]; then
printf "Извините, %s поддерживает только http\n" "${FUNCNAME[0]}"
return 1
fi
[[ "${HOST}" == "${PORT}" ]] && PORT=80
if ! exec 3<>"/dev/tcp/${HOST}/$PORT"; then
return "$?"
fi
if ! echo -en "GET ${PATH} HTTP/1.0\r\nHost: ${HOST}\r\n${tag}\r\n\r\n" >&3 ; then
return "$?"
fi
local state=0
local num=0
local code=0
while read -r line; do
num=$((num + 1))
if [ "$state" -eq 0 ]; then
if [ "$num" -eq 1 ]; then
if [[ $line =~ ^HTTP/1\.[01][[:space:]]([0-9]{3}).*$ ]]; then
code="${BASH_REMATCH[1]}"
if [[ "$code" != "200" ]]; then
printf "Ошибка при загрузке '%s', код не 200 (%s)\n" \
"$URL" "$code"
exec 3>&-
return 1
fi
state=1
else
printf "Недопустимый HTTP-ответ от '%s'" "$URL"
exec 3>&-
return 1
fi
fi
elif [ "$state" -eq 1 ]; then
if [[ "$line" == $'\r' ]]; then
state=2
fi
elif [ "$state" -eq 2 ]; then
echo "$line"
fi
done &<3
exec 3>&-
}
Шаг 2: Использование функции для загрузки файла
После определения функции можно использовать её для загрузки файла. Например:
__wget http://example.com/file.txt > file.txt
Этот код загрузит файл file.txt
с указанного URL и сохранит его на вашем локальном диске.
Замечания
- Данная функция поддерживает только HTTP и работает с текстовыми файлами. Вы можете загрузить бинарные файлы, но обработка данных может потребовать доработки.
- Обратите внимание, что функция реализует простую обработку HTTP-заголовков и проверяет код ответа для подтверждения успешной загрузки.
- Для использования этой функции вам необходимо иметь Bash версии, которая поддерживает
/dev/tcp
.
Эта возможность позволяет вам обойтись без дополнительных утилит для загрузки файлов, просто используя возможности Bash.