Как скачать файл, используя только bash и ничего больше (без curl, wget, perl и т. д.)

Вопрос или проблема

У меня есть минимальный 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 и сохранит его на вашем локальном диске.

Замечания

  1. Данная функция поддерживает только HTTP и работает с текстовыми файлами. Вы можете загрузить бинарные файлы, но обработка данных может потребовать доработки.
  2. Обратите внимание, что функция реализует простую обработку HTTP-заголовков и проверяет код ответа для подтверждения успешной загрузки.
  3. Для использования этой функции вам необходимо иметь Bash версии, которая поддерживает /dev/tcp.

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

Оцените материал
Добавить комментарий

Капча загружается...