comment télécharger un fichier en utilisant simplement bash et rien d'autre (no curl, wget, perl, etc.)

40

J'ai un * nix minimal headless qui ne dispose d' aucun utilitaire en ligne de commande pour le téléchargement de fichiers (par exemple, no curl, wget, etc.). Je n'ai que bash.

Comment puis-je télécharger un fichier?

Idéalement, j'aimerais une solution qui fonctionnerait sur une large gamme de * nix.

Chris Snow
la source
Que diriez-vousgawk
Neil McGuigan
Je ne me souviens plus maintenant si Gawk était disponible, mais j'aimerais bien voir une solution basée sur Gawk si vous en avez une :)
Chris Snow

Réponses:

64

Si vous avez bash 2.04 ou une version ultérieure avec le /dev/tcppseudo-périphérique activé, vous pouvez télécharger un fichier à partir de bash lui-même.

Collez le code suivant directement dans un shell bash (vous n'avez pas besoin de sauvegarder le code dans un fichier pour l'exécuter):

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>&-
}

Ensuite, vous pouvez l'exécuter à partir du shell comme suit:

__wget http://example.iana.org/

Source: réponse de Moreaki lors de la mise à niveau et de l'installation de packages via la ligne de commande cygwin?

Mise à jour: comme mentionné dans le commentaire, l'approche décrite ci-dessus est simpliste:

  • la readvolonté supprime les barres obliques inverses et les grands espaces.
  • Bash ne peut pas très bien gérer les octets NUL, donc les fichiers binaires sont supprimés.
  • non cité $lineva glob.
Chris Snow
la source
8
Vous avez donc répondu à votre propre question en même temps que vous l'avez posée. Vous avez une machine à remonter le temps intéressante;)
Merci Borg
11
@MeerBorg - lorsque vous posez une question, recherchez la case à cocher "répondez à votre propre question" - blog.stackoverflow.com/2011/07/…
Chris Snow
@ eestartup - Je ne pense pas que vous puissiez voter pour votre propre réponse. Puis-je expliquer le code? Pas encore! Mais cela fonctionne sur cygwin.
Chris Snow
3
Juste une remarque: cela ne fonctionnera pas avec certaines configurations de Bash. Je pense que Debian configure cette fonctionnalité en dehors de la distribution de Bash.
1
Urgh, bien que ce soit une bonne astuce, il peut trop facilement causer des téléchargements corrompus. while readcomme celui-ci, les barres obliques inverses et les espaces blancs les plus importants et Bash ne peuvent pas gérer les octets NUL de manière très satisfaisante, de sorte que les fichiers binaires sont supprimés. Et non $linecité va glob ... Rien de tout cela je vois mentionné dans la réponse.
ilkkachu
19

Utilisez le lynx.

C'est assez commun pour la plupart des Unix / Linux.

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

-dump: vide le premier fichier sur stdout et quitte

man lynx

Ou netcat:

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

Ou telnet:

(echo 'GET /'; echo ""; sleep 1; ) | telnet www.google.com 80
meule de bois
la source
5
L'OP a "* nix qui ne dispose d'aucun utilitaire de téléchargement de fichiers en ligne de commande", donc pas de lynx à coup sûr.
Celada
2
La note lynx -sourceest plus proche de wget
Steven Penny
C’est un commentaire très tardif, mais comment enregistrer la sortie de la commande telnet dans un fichier? La redirection avec ">" génère à la fois le contenu du fichier et la sortie telnet, tels que "Trying 93.184.216.34 ... Connected to www.example.com.". Je suis dans une situation où je ne peux utiliser que telnet, j'essaie de créer une prison chroot avec le moins de framework possible.
pixelomer
10

Adapté de Chris Snow answer Cela peut également gérer des fichiers de transfert binaires

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>&-
}
  • je brise le chat
  • J'utilise http 1.0 donc il n'y a pas besoin d'attendre / envoyer une connexion: fermer

Vous pouvez tester des fichiers binaires comme celui-ci

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
la source
Cela ne gérera pas les fichiers de transfert binaires, il échouera avec des octets nuls.
Wildcard
@Wildcard, je ne comprends pas, j'ai édité avec un exemple de transfert de fichier binaire (contenant des octets nuls), pouvez-vous me signaler ce qui me manque?
131
2
@Wildcard, heheh, ouais, on dirait que cela devrait fonctionner, car il lit les données du fichier avec cat. Je ne suis pas sûr que ce soit de la triche (puisqu'il ne s'agit pas uniquement de la coque) ou une solution intéressante (puisqu'il cats'agit d'un outil standard, après tout). Mais @ 131, vous voudrez peut-être ajouter une note expliquant pourquoi cela fonctionne mieux que les autres solutions proposées ici.
ilkkachu
@Wildcard, j'ai ajouté la solution pure bash comme réponse ci-dessous. Et oui, tricher ou pas, c'est une solution valable qui mérite d'être votée :)
ilkkachu
7

En prenant strictement le " juste Bash et rien d' autre ", voici une adaptation des réponses précédentes ( @ Chris's , @ 131 ) qui n'appelle aucun utilitaire externe (même les plus classiques) mais fonctionne également avec des fichiers binaires:

#!/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>&-
}

Utilisez avec download http://path/to/file > file.

Nous traitons avec les octets NUL avec read -d ''. Il lit jusqu'à un octet NUL et renvoie true s'il en a trouvé un, false s'il ne l'a pas trouvé. Bash ne peut pas gérer les octets NUL dans les chaînes. Ainsi, lorsqu'il readrenvoie true, nous ajoutons manuellement l'octet NUL lors de l'impression et lorsqu'il renvoie false, nous savons qu'il n'y a plus d'octets NUL, ce qui devrait être la dernière donnée. .

Testé avec Bash 4.4 sur des fichiers avec des NUL au milieu et se terminant par zéro, un ou deux NUL, ainsi qu'avec les binaires wgetet les curlbinaires de Debian. Le wgettéléchargement du fichier binaire de 373 ko a pris environ 5,7 secondes. Une vitesse d'environ 65 ko / s ou un peu plus de 512 ko / s.

En comparaison, la solution cat de @ 131 se termine en moins de 0,1 s, soit presque cent fois plus vite. Pas très surprenant, vraiment.

C'est évidemment idiot, car sans utiliser d'utilitaires externes, nous ne pouvons pas faire grand chose avec le fichier téléchargé, pas même le rendre exécutable.

ilkkachu
la source
Echo n'est-il pas un binaire autonome? (: p)
131
1
@ 131, non! Bash a echoet printfcomme éléments intégrés (il a besoin d'un élément intégré printfà implémenter printf -v)
ilkkachu le
4

Si vous avez ce paquet libwww-perl

Vous pouvez simplement utiliser:

/usr/bin/GET
stackexchanger
la source
Considérant que les autres réponses ne respectent pas l'exigence de la question (uniquement bash), je pense que c'est en fait meilleur que la lynxsolution, étant donné que Perl a probablement plus de chances d'être préinstallé que Lynx.
Marcus
4

Utilisez plutôt le téléchargement via SSH à partir de votre machine locale

Une boîte "nix * minimal sans tête" signifie que vous y êtes probablement en SSH. Vous pouvez donc également utiliser SSH pour y télécharger . Ce qui est fonctionnellement équivalent au téléchargement (de progiciels, etc.), sauf si vous souhaitez qu'une commande de téléchargement soit incluse dans un script sur votre serveur sans affichage.

Comme indiqué dans cette réponse , vous devez exécuter les opérations suivantes sur votre ordinateur local pour placer un fichier sur votre serveur sans tête distant:

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

Téléchargement plus rapide via SSH depuis une troisième machine

L'inconvénient de la solution ci-dessus par rapport au téléchargement est une vitesse de transfert plus faible, car la connexion avec votre ordinateur local a généralement une bande passante bien inférieure à la connexion entre votre serveur sans interface utilisateur et d'autres serveurs.

Pour résoudre ce problème, vous pouvez bien sûr exécuter la commande ci-dessus sur un autre serveur disposant d'une bande passante suffisante. Pour rendre cela plus confortable (en évitant une connexion manuelle sur la troisième machine), voici une commande à exécuter sur votre machine locale .

Pour être sûr, copiez et collez cette commande, y compris le caractère d'espacement ' ' . Voir les explications ci-dessous pour la raison.

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

Explications:

  • La commande va ssh sur votre troisième machine intermediate-host, commencera à télécharger un fichier là-bas via wget, et commencera à le télécharger target-hostvia SSH. Le téléchargement et le téléchargement utilisent la bande passante de votre intermediate-hostet se produisent en même temps (en raison des équivalents Bash pipe), donc les progrès seront rapides.

  • Lorsque vous utilisez cette option, vous devez remplacer les deux connexions au serveur ( user@*-host), le mot de passe de l'hôte cible ( yourpassword), l'URL de téléchargement ( http://example.com/…) et le chemin de sortie sur votre hôte cible ( /path/to/output-file.zip) avec les propres valeurs appropriées.

  • Pour les -T -e noneoptions SSH lors de son utilisation pour transférer des fichiers, voir ces explications détaillées .

  • Cette commande est destinée aux cas où vous ne pouvez pas utiliser le mécanisme d'authentification par clé publique de SSH - elle survient toujours avec certains fournisseurs d'hébergement partagé, notamment Host Europe . Pour automatiser encore le processus, nous comptons sur sshpasspouvoir fournir le mot de passe dans la commande. Il doit sshpassêtre installé sur votre hôte intermédiaire ( sudo apt-get install sshpasssous Ubuntu).

  • Nous essayons d'utiliser sshpassde manière sécurisée, mais ce ne sera toujours pas aussi sécurisé que le mécanisme SSH de pubkey (dit man sshpass). En particulier, nous fournissons le mot de passe SSH non pas en tant qu'argument de ligne de commande, mais via un fichier, qui est remplacé par une substitution de processus bash afin de s’assurer qu’il n’existe jamais sur le disque. Le printfest un bash intégré, en s'assurant que cette partie du code ne s'affiche pas en tant que commande séparée en pssortie car cela exposerait le mot de passe [ source ]. Je pense que cette utilisation de sshpassest tout aussi sécurisée que la sshpass -d<file-descriptor>variante recommandée dans man sshpass, car bash le mappe de manière interne vers un tel /dev/fd/*descripteur de fichier. Et cela sans utiliser un fichier temporaire [ source]. Mais pas de garanties, j'ai peut-être oublié quelque chose.

  • Encore une fois pour sécuriser l' sshpassutilisation, nous devons empêcher l'enregistrement de la commande dans l'historique bash de votre ordinateur local. Pour cela, la commande entière est précédée d'un caractère d'espacement, qui a cet effet.

  • La -o StrictHostKeyChecking=nopièce empêche l'échec de la commande si elle ne s'est jamais connectée à l'hôte cible. (Normalement, SSH attendrait alors que l’utilisateur confirme la tentative de connexion. Nous le faisons quand même.)

  • sshpassattend une commande sshou scpcomme dernier argument. Nous devons donc réécrire la wget -O - … | ssh …commande typique dans un formulaire sans tuyau bash, comme expliqué ici .

Tanius
la source
3

Basé sur la recette de @Chris Snow. J'ai fait quelques améliorations:

  • vérification de schéma http (il ne supporte que http)
  • Validation de la réponse http (vérification de la ligne d'état de la réponse, et scission de l'en-tête et du corps par la ligne '\ r \ n', et non par 'Connection: close', ce qui n'est pas toujours vrai)
  • échec du code non-200 (il est important de télécharger des fichiers sur Internet)

Voici le code:

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
la source
Belles améliorations +1
Chris Snow
Cela a fonctionné, mais j'ai trouvé un problème lorsque j'utilise ce script, il faut attendre plusieurs secondes lorsque toutes les données sont lues, ce n'est pas le cas dans la réponse de @Chris Snow, quelqu'un peut-il expliquer cela?
Zw963
Et, dans cette réponse echo -en "GET ${PATH} HTTP/1.1\r\nHost: ${HOST}\r\n${tag}\r\n\r\n" >&3, ${tag}n'est pas spécifié.
Zw963
Je modifie cette réponse avec la tagvariable est correct, cela fonctionne bien maintenant.
Zw963
ne fonctionne pas avec zsh, __wget google.com désolé, ne supporte que http / usr / bin / env: bash: Aucun fichier ou répertoire de ce type
vrkansagara