jak pobrać plik za pomocą bash i nic więcej (bez curl, wget, perl itp.)

40

Mam minimalny * nix bezgłowy, który nie ma żadnych narzędzi wiersza poleceń do pobierania plików (np. Bez curl, wget itp.). Mam tylko uderzenie.

Jak mogę pobrać plik?

Idealnie chciałbym mieć rozwiązanie, które działałoby w szerokim zakresie * nix.

Chris Snow
źródło
co powieszgawk
Neil McGuigan
Nie pamiętam teraz, czy gawk był dostępny, ale chciałbym zobaczyć rozwiązanie oparte na gawk, jeśli je masz :)
Chris Snow

Odpowiedzi:

64

Jeśli masz wersję bash 2.04 lub nowszą z /dev/tcpwłączonym pseudo-urządzeniem, możesz pobrać plik z samej wersji bash.

Wklej następujący kod bezpośrednio do powłoki bash (nie musisz zapisywać kodu w pliku do wykonania):

function __wget() {
    : ${DEBUG:=0}
    local URL=$1
    local tag="Connection: close"
    local mark=0

    if [ -z "${URL}" ]; then
        printf "Usage: %s \"URL\" [e.g.: %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>&-
}

Następnie możesz wykonać go z poziomu powłoki w następujący sposób:

__wget http://example.iana.org/

Źródło: odpowiedź Moreaki dotycząca aktualizacji i instalacji pakietów za pomocą wiersza poleceń cygwin?

Aktualizacja: jak wspomniano w komentarzu, podejście opisane powyżej jest uproszczone:

  • readwola trashes backslashy i prowadząc spacje.
  • Bash nie radzi sobie bardzo dobrze z bajtami NUL, więc pliki binarne są niedostępne.
  • nie cytowany $linebędzie glob.
Chris Snow
źródło
8
Więc odpowiedziałeś na swoje pytanie w tym samym czasie, w którym je zadałeś. To interesująca wehikuł czasu;)
Meer Borg
11
@MeerBorg - kiedy zadajesz pytanie, odszukaj pole wyboru „odpowiedz na własne pytanie” - blog.stackoverflow.com/2011/07/07
Chris Snow
@eestartup - Nie sądzę, abyś mógł głosować na swoją własną odpowiedź. Czy mogę wyjaśnić kod? Jeszcze nie! Ale działa na Cygwinie.
Chris Snow
3
Tylko uwaga: to nie zadziała w niektórych konfiguracjach Bash. Wierzę, że Debian konfiguruje tę funkcję ze swojej dystrybucji Bash.
1
Urgh, chociaż jest to fajna sztuczka, może zbyt łatwo powodować uszkodzone pobieranie. while readtak, że kasuje ukośniki odwrotne i wiodące białe znaki, a Bash nie radzi sobie bardzo dobrze z bajtami NUL, więc pliki binarne są usuwane. I nie cytowany $linebędzie glob ... Nic z tego nie widzę wspomniane w odpowiedzi.
ilkkachu
19

Użyj rysia.

Jest to dość powszechne w większości systemów Unix / Linux.

lynx -dump http://www.google.com

-dump: zrzuć pierwszy plik na standardowe wyjście i wyjdź

man lynx

Lub netcat:

/usr/bin/printf 'GET / \n' | nc www.google.com 80

Lub telnet:

(echo 'GET /'; echo ""; sleep 1; ) | telnet www.google.com 80
woodstack
źródło
5
OP ma „* nix, który nie ma żadnych narzędzi wiersza poleceń do pobierania plików”, więc na pewno nie rysia.
Celada,
2
Uwaga lynx -sourcejest bliższa wget
Steven Penny,
Hej, więc jest to bardzo późny komentarz, ale jak zapisać dane wyjściowe polecenia telnet w pliku? Przekierowanie za pomocą „>” wyświetla zarówno zawartość pliku, jak i wyjście telnet, takie jak „Próbowanie 93.184.216.34 ... Połączono ze stroną www.example.com”. Jestem w sytuacji, w której mogę korzystać tylko z telnetu, próbuję zrobić więzienie chroot z możliwie najmniejszą liczbą ram.
pixelomer
10

Na podstawie odpowiedzi Chrisa Snowa Może to również obsługiwać pliki binarne

function __curl() {
  read proto server path <<<$(echo ${1//// })
  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
  (while read line; do
   [[ "$line" == $'\r' ]] && break
  done && cat) <&3
  exec 3>&-
}
  • łamię && cat, żeby wyjść z lektury
  • używam http 1.0, więc nie trzeba czekać na / wysłać połączenie: zamknij

Możesz testować takie pliki binarne

ivs@acsfrlt-j8shv32:/mnt/r $ __curl http://www.google.com/favicon.ico > mine.ico
ivs@acsfrlt-j8shv32:/mnt/r $ curl http://www.google.com/favicon.ico > theirs.ico
ivs@acsfrlt-j8shv32:/mnt/r $ md5sum mine.ico theirs.ico
f3418a443e7d841097c714d69ec4bcb8  mine.ico
f3418a443e7d841097c714d69ec4bcb8  theirs.ico
131
źródło
Nie obsłuży to plików binarnych - nie powiedzie się w przypadku bajtów zerowych.
Wildcard
@Wildcard, nie rozumiem, edytowałem przykład binarnego transferu plików (zawierający bajty null), czy możesz wskazać mi to, czego mi brakuje?
131
2
@Wildcard, heheh, tak, wygląda na to, że powinno działać, ponieważ odczytuje rzeczywiste dane pliku cat. Nie jestem pewien, czy to oszustwo (ponieważ nie jest to wyłącznie powłoka), czy fajne rozwiązanie (ponieważ catw końcu jest to standardowe narzędzie). Ale @ 131, możesz dodać notatkę o tym, dlaczego działa lepiej niż inne rozwiązania tutaj.
ilkkachu
@Wildcard, dodałem również rozwiązanie czysto bashowe jako odpowiedź poniżej. I tak, oszukuje czy nie, jest to prawidłowe rozwiązanie i warte pochwały :)
ilkkachu
7

Przyjmując ściśle „ tylko Bash i nic więcej ”, oto jedna adaptacja wcześniejszych odpowiedzi ( @ Chris's , @ 131's ), która nie wywołuje żadnych zewnętrznych narzędzi (nawet standardowych), ale działa również z plikami binarnymi:

#!/bin/bash
download() {
  read proto server path <<< "${1//"/"/ }"
  DOC=/${path// //}
  HOST=${server//:*}
  PORT=${server//*:}
  [[ x"${HOST}" == x"${PORT}" ]] && PORT=80

  exec 3<>/dev/tcp/${HOST}/$PORT

  # send request
  echo -en "GET ${DOC} HTTP/1.0\r\nHost: ${HOST}\r\n\r\n" >&3

  # read the header, it ends in a empty line (just CRLF)
  while IFS= read -r line ; do 
      [[ "$line" == $'\r' ]] && break
  done <&3

  # read the data
  nul='\0'
  while IFS= read -d '' -r x || { nul=""; [ -n "$x" ]; }; do 
      printf "%s$nul" "$x"
  done <&3
  exec 3>&-
}

Użyj z download http://path/to/file > file.

Mamy do czynienia z bajtami NUL read -d ''. Czyta do bajtu NUL i zwraca true, jeśli go znalazł, a false, jeśli nie. Bash nie może obsłużyć bajtów NUL w ciągach, więc gdy readzwraca true, dodajemy bajt NUL ręcznie podczas drukowania, a gdy zwraca false, wiemy, że nie ma już bajtów NUL i powinien to być ostatni kawałek danych .

Testowane z Bash 4.4 na plikach z NUL w środku, a kończące się na zero, jeden lub dwa NUL, a także z wgeti curlbinaria z Debiana. wgetPobranie pliku binarnego o wielkości 373 kB zajęło około 5,7 sekundy. Prędkość około 65 kB / s lub nieco więcej niż 512 kb / s.

Dla porównania, rozwiązanie dla kota @ 131 kończy się w mniej niż 0,1 s, czyli prawie sto razy szybciej. Naprawdę niezbyt zaskakujące.

Jest to oczywiście głupie, ponieważ bez korzystania z zewnętrznych narzędzi nie możemy wiele zrobić z pobranym plikiem, nawet nie można go uruchomić.

ilkkachu
źródło
Czy echo nie jest samodzielną wersją binarną? (: p)
131
1
@ 131, nie! Bash ma echoi printfjako wbudowane (potrzebuje wbudowanego printfdo wdrożenia printf -v)
ilkkachu
4

Jeśli masz ten pakiet libwww-perl

Możesz po prostu użyć:

/usr/bin/GET
Stackexchanger
źródło
Biorąc pod uwagę, że inne odpowiedzi nie spełniają wymogu pytania (tylko bash), myślę, że jest to w rzeczywistości lepsze niż lynxrozwiązanie, ponieważ Perl jest z pewnością bardziej preinstalowany niż Lynx.
Marcus
4

Zamiast tego użyj przesyłania przez SSH z lokalnego komputera

„Minimalna liczba bezgłowych * nix” oznacza, że ​​prawdopodobnie używasz SSH. Możesz więc użyć SSH, aby przesłać do niego. Co jest funkcjonalnie równoważne pobieraniu (pakietów oprogramowania itp.), Z wyjątkiem sytuacji, gdy chcesz, aby polecenie pobierania zawierało się w skrypcie na twoim serwerze bezgłowym.

Jak pokazano w tej odpowiedzi , należy wykonać następujące czynności na komputerze lokalnym , aby umieścić plik na zdalnym serwerze bezgłowym:

wget -O - http://example.com/file.zip | ssh user@host 'cat >/path/to/file.zip'

Szybsze przesyłanie przez SSH z trzeciego komputera

Wadą powyższego rozwiązania w porównaniu do pobierania jest niższa prędkość transferu, ponieważ połączenie z lokalną maszyną ma zwykle znacznie mniejszą przepustowość niż połączenie między serwerem bezgłowym a innymi serwerami.

Aby rozwiązać ten problem, możesz oczywiście wykonać powyższe polecenie na innym serwerze z przyzwoitą przepustowością. Aby uczynić to wygodniejszym (unikając ręcznego logowania na trzecim komputerze), oto polecenie, które należy wykonać na komputerze lokalnym .

Aby zabezpieczyć się, skopiuj i wklej to polecenie, w tym wiodący znak spacji ' ' . Powód wyjaśniono poniżej.

 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' \
"

Objaśnienia:

  • Polecenie prześle ssh na twój trzeci komputer intermediate-host, rozpocznie pobieranie pliku do niego przez wgeti rozpocznie przesyłanie go na target-hostSSH. Pobieranie i przesyłanie wykorzystuj przepustowość twojego intermediate-hosti dzieje się w tym samym czasie (ze względu na odpowiedniki rur Bash), więc postęp będzie szybki.

  • Korzystając z tego, musisz zastąpić dwa loginy serwera ( user@*-host), hasło hosta docelowego ( yourpassword), adres URL pobierania ( http://example.com/…) i ścieżkę wyjściową na hoście docelowym ( /path/to/output-file.zip) odpowiednimi własnymi wartościami.

  • Aby -T -e nonezapoznać się z opcjami SSH podczas przesyłania plików, zobacz te szczegółowe wyjaśnienia .

  • To polecenie jest przeznaczone dla przypadków, w których nie można użyć mechanizmu uwierzytelniania klucza publicznego SSH - nadal występuje w przypadku niektórych dostawców hostingu współdzielonego, w szczególności Host Europe . Aby nadal automatyzować proces, polegamy na tym, że możemy sshpasspodać hasło w poleceniu. Wymaga sshpassto zainstalowania na twoim hoście pośrednim ( sudo apt-get install sshpasspod Ubuntu).

  • Staramy się używać sshpassw bezpieczny sposób, ale nadal nie będzie tak bezpieczny, jak mechanizm klucza publicznego SSH (mówi man sshpass). W szczególności podajemy hasło SSH nie jako argument wiersza poleceń, ale poprzez plik, który jest zastępowany przez podstawienie procesu bash, aby upewnić się, że nigdy nie istnieje na dysku. Jest printfto wbudowany bash, upewniający się, że ta część kodu nie wyskakuje jako osobne polecenie pswyjściowe, ponieważ odsłoniłoby to hasło [ źródło ]. Myślę , że takie użycie sshpassjest tak samo bezpieczne, jak sshpass -d<file-descriptor>zalecany wariant man sshpass, ponieważ bash i tak mapuje go wewnętrznie na taki /dev/fd/*deskryptor pliku. I to bez użycia pliku tymczasowego [ źródło]. Ale żadnych gwarancji, może coś przeoczyłem.

  • Ponownie, aby sshpasskorzystanie było bezpieczne, musimy zapobiec zapisywaniu polecenia w historii bash na twoim komputerze lokalnym. W tym celu całe polecenie jest poprzedzone jednym znakiem spacji, który ma ten efekt.

  • -o StrictHostKeyChecking=noCzęść zapobiega polecenie braku w przypadku, gdy nie podłączonego do docelowego gospodarza. (Zwykle SSH czekałby następnie na dane wejściowe użytkownika, aby potwierdzić próbę połączenia. My i tak kontynuujemy.)

  • sshpassoczekuje polecenia sshlub scpjako ostatniego argumentu. Musimy więc przepisać typowe wget -O - … | ssh …polecenie do postaci bez potoku, jak wyjaśniono tutaj .

Tanius
źródło
3

Na podstawie przepisu @Chris Snow. Wprowadziłem kilka ulepszeń:

  • Sprawdzanie schematu http (obsługuje tylko http)
  • sprawdzanie poprawności odpowiedzi HTTP (sprawdzanie stanu odpowiedzi i dzielenie nagłówka i treści według wiersza „\ r \ n”, a nie „Połączenie: zamknij”, co czasami nie jest prawdą)
  • nie powiodło się na kodzie innym niż 200 (ważne jest, aby pobierać pliki przez Internet)

Oto kod:

function __wget() {
    : ${DEBUG:=0}
    local URL=$1
    local tag="Connection: close"

    if [ -z "${URL}" ]; then
        printf "Usage: %s \"URL\" [e.g.: %s http://www.google.com/]" \
               "${FUNCNAME[0]}" "${FUNCNAME[0]}"
        return 1;
    fi  
    read proto server path <<<$(echo ${URL//// })
    local SCHEME=${proto//:*}
    local PATH=/${path// //} 
    local HOST=${server//:*}
    local PORT=${server//*:}
    if [[ "$SCHEME" != "http" ]]; then
        printf "sorry, %s only support http\n" "${FUNCNAME[0]}"
        return 1
    fi  
    [[ x"${HOST}" == x"${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

    exec 3<>/dev/tcp/${HOST}/$PORT
    if [ $? -ne 0 ]; then
        return $?
    fi  
    echo -en "GET ${PATH} HTTP/1.1\r\nHost: ${HOST}\r\n${tag}\r\n\r\n" >&3
    if [ $? -ne 0 ]; then
        return $?
    fi  
    # 0: at begin, before reading http response
    # 1: reading header
    # 2: reading body
    local state=0
    local num=0
    local code=0
    while read line; do
        num=$(($num + 1))
        # check http code
        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 "failed to wget '%s', code is not 200 (%s)\n" "$URL" "$code"
                        exec 3>&-
                        return 1
                    fi
                    state=1
                else
                    printf "invalid http response from '%s'" "$URL"
                    exec 3>&-
                    return 1
                fi
            fi
        elif [ $state -eq 1 ]; then
            if [[ "$line" == $'\r' ]]; then
                # found "\r\n"
                state=2
            fi
        elif [ $state -eq 2 ]; then
            # redirect body to stdout
            # TODO: any way to pipe data directly to stdout?
            echo "$line"
        fi
    done <&3
    exec 3>&-
}
Yecheng Fu
źródło
Ładne ulepszenia +1
Chris Snow
Zadziałało, ale znalazłem problem, kiedy używam tych skryptów. Poczekaj kilka sekund, kiedy wszystkie dane zostaną odczytane zakończone, ten przypadek nie zdarza się w odpowiedzi @Chris Snow, czy ktoś może to wyjaśnić?
zw963
I, w tym odpowiedzi echo -en "GET ${PATH} HTTP/1.1\r\nHost: ${HOST}\r\n${tag}\r\n\r\n" >&3, ${tag}nie jest określona.
zw963
Edytuję tę odpowiedź, gdy tagzmienna ma poprawny zestaw, teraz działa dobrze.
zw963
nie działa z zsh, __wget google.com przepraszam, obsługuje tylko http / usr / bin / env: bash: Brak takiego pliku lub katalogu
vrkansagara