cómo descargar un archivo usando solo bash y nada más (sin curl, wget, perl, etc.)

40

Tengo un mínimo * sin cabeza * nix que no tiene ninguna utilidad de línea de comandos para descargar archivos (por ejemplo, sin curl, wget, etc.). Solo tengo bash.

¿Cómo puedo descargar un archivo?

Idealmente, me gustaría una solución que funcione en una amplia gama de * nix.

Chris Snow
fuente
¿Qué talgawk
Neil McGuigan
Ahora no recuerdo si gawk estaba disponible, aunque me encantaría ver una solución basada en gawk si tienes una :)
Chris Snow
1
He
Neil McGuigan

Respuestas:

64

Si tiene bash 2.04 o superior con el /dev/tcppseudodispositivo habilitado, puede descargar un archivo de bash.

Pegue el siguiente código directamente en un shell bash (no necesita guardar el código en un archivo para ejecutarlo):

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

Luego puede ejecutarlo desde el shell de la siguiente manera:

__wget http://example.iana.org/

Fuente: ¿ Respuesta de Moreaki actualizando e instalando paquetes a través de la línea de comando cygwin?

Actualización: como se menciona en el comentario, el enfoque descrito anteriormente es simplista:

  • la readvoluntad destruye las barras invertidas y los espacios en blanco iniciales.
  • Bash no puede manejar muy bien los bytes NUL, por lo que los archivos binarios están fuera.
  • sin comillas $lineserá glob.
Chris Snow
fuente
8
Entonces respondiste tu propia pregunta al mismo tiempo que la hiciste. Esa es una máquina del tiempo interesante que tienes;)
Meer Borg
11
@MeerBorg: cuando haga una pregunta, busque la casilla 'responder su propia pregunta' - blog.stackoverflow.com/2011/07/…
Chris Snow
@eestartup - No creo que puedas votar por tu propia respuesta. ¿Puedo explicar el código? ¡Aún no! Pero funciona en Cygwin.
Chris Snow
3
Solo una nota: esto no funcionará con algunas configuraciones de Bash. Creo que Debian configura esta característica fuera de su distribución de Bash.
1
Urgh, si bien este es un buen truco, puede causar descargas corruptas con demasiada facilidad. while readasí, las barras invertidas y los espacios en blanco iniciales y Bash no pueden manejar muy bien los bytes NUL, por lo que los archivos binarios están fuera. Y sin comillas $linese glob ... Nada de esto veo mencionado en la respuesta.
ilkkachu
19

Usa lince.

Es bastante común para la mayoría de Unix / Linux.

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

-dump: volcar el primer archivo a stdout y salir

man lynx

O netcat:

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

O telnet:

(echo 'GET /'; echo ""; sleep 1; ) | telnet www.google.com 80
leña
fuente
55
El OP tiene "* nix que no tiene ninguna utilidad de línea de comandos para descargar archivos", por lo que no hay lince seguro.
Celada
2
La nota lynx -sourceestá más cerca de wget
Steven Penny
Oye, este es un comentario muy tardío, pero ¿cómo guardas la salida del comando telnet en un archivo? La redirección con ">" genera tanto el contenido del archivo como la salida de Telnet, como "Intentando 93.184.216.34 ... Conectado a www.example.com". Estoy en una situación en la que solo puedo usar telnet, estoy tratando de hacer una cárcel chroot con el menor marco posible.
pixelomer
10

Adaptado de la respuesta de Chris Snow Esto también puede manejar archivos de transferencia binarios

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>&-
}
  • rompo && cat para salir de la lectura
  • uso http 1.0, así que no hay necesidad de esperar / enviar una conexión: cerrar

Puedes probar archivos binarios como este

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
fuente
Esto no manejará archivos de transferencia binarios, fallará en bytes nulos.
Comodín
@Wildcard, no entiendo, he editado con un ejemplo de transferencia de archivos binarios (que contiene bytes nulos), ¿puede señalarme lo que me falta?
131
2
@Wildcard, jeje, sí, parece que debería funcionar, ya que lee los datos reales del archivo cat. No estoy seguro de si eso es trampa (ya que no es puramente el shell), o una buena solución (ya que cates una herramienta estándar, después de todo). Pero @ 131, es posible que desee agregar una nota sobre por qué funciona mejor que las otras soluciones aquí.
ilkkachu
@Wildcard, también agregué la solución bash pura como respuesta a continuación. Y sí, hacer trampa o no, esta es una solución válida y vale la pena un
voto a favor
7

Tomando el " solo Bash y nada más " estrictamente, aquí hay una adaptación de respuestas anteriores ( @ Chris's , @ 131's ) que no llama a ninguna utilidad externa (ni siquiera estándar) pero también funciona con archivos binarios:

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

Usar con download http://path/to/file > file.

Nos ocupamos de bytes NUL con read -d ''. Se lee hasta un byte NUL y devuelve verdadero si lo encontró, falso si no lo encontró. Bash no puede manejar bytes NUL en cadenas, por lo que cuando readregresa con verdadero, agregamos el byte NUL manualmente al imprimir, y cuando devuelve falso, sabemos que ya no hay bytes NUL, y este debería ser el último dato. .

Probado con Bash 4.4 en archivos con NUL en el medio, y que termina en cero, uno o dos NUL, y también con los binarios wgety curlde Debian. El wgetbinario de 373 kB tardó aproximadamente 5,7 segundos en descargarse. Una velocidad de aproximadamente 65 kB / so un poco más de 512 kb / s.

En comparación, la solución cat de @ 131 termina en menos de 0.1 s, o casi cien veces más rápido. No es muy sorprendente, de verdad.

Esto es obviamente una tontería, ya que sin usar utilidades externas, no hay mucho que podamos hacer con el archivo descargado, ni siquiera hacerlo ejecutable.

ilkkachu
fuente
¿No es echo un binario independiente -no shell-? (: p)
131
1
@ 131, no! Bash tiene echoy printfcomo incorporado (necesita un incorporado printfpara implementar printf -v)
ilkkachu
4

Si tiene este paquete libwww-perl

Simplemente puede usar:

/usr/bin/GET
stackexchanger
fuente
Teniendo en cuenta que otras respuestas no respetan el requisito de la pregunta (solo bash), creo que esto es realmente mejor que la lynxsolución, ya que es más probable que Perl esté preinstalado que Lynx.
Marcus
4

Utilice la carga en su lugar, a través de SSH desde su máquina local

Un cuadro "mínimo sin cabeza * nix" significa que probablemente se SSH en él. Por lo tanto, también puede usar SSH para cargarlo . Que es funcionalmente equivalente a la descarga (de paquetes de software, etc.), excepto cuando desea que se incluya un comando de descarga en un script en su servidor sin cabeza, por supuesto.

Como se muestra en esta respuesta , ejecutaría lo siguiente en su máquina local para colocar un archivo en su servidor remoto sin cabeza:

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

Carga más rápida a través de SSH desde una tercera máquina

La desventaja de la solución anterior en comparación con la descarga es una velocidad de transferencia más baja, ya que la conexión con su máquina local generalmente tiene mucho menos ancho de banda que la conexión entre su servidor sin cabeza y otros servidores.

Para resolver eso, por supuesto, puede ejecutar el comando anterior en otro servidor con un ancho de banda decente. Para hacerlo más cómodo (evitando un inicio de sesión manual en la tercera máquina), aquí hay un comando para ejecutar en su máquina local .

Para estar seguro, copie y pegue ese comando, incluido el carácter de espacio inicial ' ' . Consulte las explicaciones a continuación para conocer el motivo.

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

Explicaciones:

  • El comando enviará ssh a su tercera máquina intermediate-host, comenzará a descargar un archivo allí a través de wgety comenzará a cargarlo a target-hosttravés de SSH. La descarga y la carga utilizan el ancho de banda de su intermediate-hosty suceden al mismo tiempo (debido a los equivalentes de la tubería Bash), por lo que el progreso será rápido.

  • Al usar esto, debe reemplazar los dos inicios de sesión del servidor ( user@*-host), la contraseña del host de destino ( yourpassword), la URL de descarga ( http://example.com/…) y la ruta de salida en su host de destino ( /path/to/output-file.zip) con los valores propios apropiados.

  • Para conocer las -T -e noneopciones de SSH al usarlo para transferir archivos, consulte estas explicaciones detalladas .

  • Este comando está destinado a casos en los que no puede utilizar el mecanismo de autenticación de clave pública de SSH; todavía ocurre con algunos proveedores de alojamiento compartido, especialmente Host Europe . Para automatizar aún el proceso, confiamos en sshpasspoder proporcionar la contraseña en el comando. Requiere sshpassestar instalado en su host intermedio ( sudo apt-get install sshpassen Ubuntu).

  • Intentamos usarlo sshpassde forma segura, pero aún no será tan seguro como el mecanismo SSH pubkey (dice man sshpass). En particular, proporcionamos la contraseña SSH no como un argumento de línea de comando sino a través de un archivo, que se reemplaza por la sustitución del proceso bash para asegurarnos de que nunca exista en el disco. El printfes un bash incorporado, asegurándose de que esta parte del código no aparezca como un comando separado en la pssalida, ya que eso expondría la contraseña [ fuente ]. Yo creo que este uso de sshpasses tan segura como la sshpass -d<file-descriptor>variante recomendada en man sshpass, debido fiesta de la asigna internamente a un tal /dev/fd/*descriptor de archivo de todos modos. Y eso sin usar un archivo temporal [ fuente] Pero no hay garantías, tal vez pasé por alto algo.

  • Nuevamente para que el sshpassuso sea seguro, debemos evitar que el comando se grabe en el historial de bash en su máquina local. Para eso, todo el comando se antepone con un carácter de espacio, que tiene este efecto.

  • La -o StrictHostKeyChecking=noparte evita que el comando falle en caso de que nunca se conecte al host de destino. (Normalmente, SSH esperaría la entrada del usuario para confirmar el intento de conexión. De todos modos, hacemos que continúe).

  • sshpassespera un comando ssho scpcomo último argumento. Entonces, tenemos que reescribir el wget -O - … | ssh …comando típico en un formulario sin una tubería bash, como se explica aquí .

Tanius
fuente
3

Basado en la receta @Chris Snow. Hice algunas mejoras:

  • comprobación del esquema http (solo es compatible con http)
  • validación de respuesta http (verificación de línea de estado de respuesta, y división de encabezado y cuerpo por línea '\ r \ n', no 'Conexión: cerrar', que a veces no es cierto)
  • falló en código no 200 (es importante descargar archivos en Internet)

Aquí está el código:

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
fuente
Bonitas mejoras +1
Chris Snow
Funcionó, pero encontré una preocupación, cuando uso estos scripts, espera varios segundos cuando todos los datos se leen terminados, este caso no sucede en la respuesta de @Chris Snow, ¿alguien podría explicar esto?
zw963
Y, en esta respuesta, echo -en "GET ${PATH} HTTP/1.1\r\nHost: ${HOST}\r\n${tag}\r\n\r\n" >&3, ${tag}no se especifica.
zw963
Edito esta respuesta con la tagvariable es el conjunto correcto, funciona bien ahora.
zw963
no funciona con zsh, __wget google.com lo siento, solo admite http / usr / bin / env: bash: No
existe