henry_flower: A melancholy wolf (Default)
henry_flower ([personal profile] henry_flower) wrote2024-02-11 11:09 am

HTTP get на баші

Побачив ув твіторі:

✙ цар криса
@retraut

блять, немає curl думав зараз завантажу. А як завантажити коли немає curl, wget?

Це про деб'янвського контейнера. В останніх є зазвичай баш, його ставить debootstrap навіть ув мінімальній конфігурації.

Як відкрити TCP сокета, якщо немає ніяких Рубі інстальованих, жодного компайлера, контейнера є пустий та порожній, і темрява над лайнаксом, і демон ssh ширяє над поверхнею контейнера без форми?

Якщо баш був зкомпайлений з --enable-net-redirections, він вміє оперувати з файлами /dev/tcp/example.com/80:

$ exec 3<> /dev/tcp/www.gnu.org/80
$ printf "%s\r\n" 'HEAD /robots.txt HTTP/1.1' >&3
$ printf "%s\r\n\r\n" 'Host: www.gnu.org' >&3
$ cat <&3
HTTP/1.1 200 OK
Date: Sun, 11 Feb 2024 07:02:40 GMT
Server: Apache/2.4.29
Content-Type: text/plain
Content-Language: non-html
…

Все це є вкрай цікаво, але позбавлене сенсу: сучасний інтервеб, а не уйоб сайти Столмана, розмовляє лише через TLS. Завантажити статичного байнарника curl з ґітхабу так не вийде.

Але можна на нормальному сервері запустити форвардний TLS проксі і під'єднуватися до нього. Ув мінімальному вигляді це може бути

$ ncat -vk -l 10.10.10.10 1234 -e proxy.sh

де proxy.sh:

#!/bin/sh
read -r host
openssl s_client -connect "$host":443 -quiet -no_ign_eof

Баш-клайент відправляє ім'я хоста, який ми на нашому проксі-сервері зчитуємо і за допомогою openssl під'єднуємося до нього. ncat тут працює ув галантному стилі inetd--форкає себе і під'єднує сокета башу до stdin/stdout скрипта proxy.sh, про що було ув цьому бложику на початку року.

Зі сторони баша-клайента залишається декілька питань:

  1. будь-який файл з "releases" сторінок ґітхабу мандрує через 302 рідайректа, тому щоб щось завантажити, треба робити 2 ріквести;

  2. як парсити відповіді? Як мінімум треба знаходити \r\n\r\n, який відділяє headers від омріяних байтів curl.tar.xz; це означає передивлятися байнарний стрім, що є напрочуд неприємно робити ув шелі;

  3. як парсити URL? це є необхідно для реакції на 302.

Почнемо з останнього.

В ідеалі, хотілося би мати хфункцію ув баші, яка би працювала як джаваскриптова URL():

$ node -pe 'new URL("https://q.example.com:8080/foo?q=1&w=2#lol")'
URL {
  href: 'https://q.example.com:8080/foo?q=1&w=2#lol',
  origin: 'https://q.example.com:8080',
  protocol: 'https:',
  username: '',
  password: '',
  host: 'q.example.com:8080',
  hostname: 'q.example.com',
  port: '8080',
  pathname: '/foo',
  search: '?q=1&w=2',
  searchParams: URLSearchParams { 'q' => '1', 'w' => '2' },
  hash: '#lol'
}

На SO є багато відповідей з прикладом ріґекспів, але жоден з них не пройшов у мене тестів з URL вище. Ріґекспи там такої довжини і такі страхітливі, що фіксити їх я не намагався, а спитав чатаджипіті, який зробив лише гірше, і гооглівського джéменай, який з Nї спроби його ніби полагодив.

$ cat lib.bash
declare -A URL

url_parse() {
    local pattern='^(([^:/?#]+):)?(//((([^:/?#]+)@)?([^:/?#]+)(:([0-9]+))?))?(/([^?#]*))?(\?([^#]*))?(#(.*))?'
    [[ "$1" =~ $pattern ]] && [ "${BASH_REMATCH[2]}" ] && [ "${BASH_REMATCH[4]}" ] || return 1
    URL=(
        [proto]=${BASH_REMATCH[2]}
        [host]=${BASH_REMATCH[4]}
        [hostname]=${BASH_REMATCH[7]}
        [port]=${BASH_REMATCH[9]}
        [pathname]=${BASH_REMATCH[10]:-/}
        [search]=${BASH_REMATCH[12]}
        [hash]=${BASH_REMATCH[14]}
    )
}

Пошук \r\n\r\n. Лайнаксний grep має опції -a -b -o, які разом друкують byte offset знайденого патерну. Busybox'сний grep про -ab не знає. Нарід каже про od(1), але без прикладів. Якщо їм друкувати файла як

0000000 68
0000001 20
0000002 3a
…

де лівий стовпчик то є offset, то можна 1ші 32КБ ріспонзу такого формату конвертнути в 1 рядок і шукати \r\n\r\n grep'ом:

offset_calc() {
    od -N $((32*1024)) -t x1 -Ad -w1 -v "$tmp" | tr '\n' ' ' | \
        grep -o '....... 0d ....... 0a ....... 0d ....... 0a' | \
        head -1 | cut -d' ' -f7 || printf -- -1
}

Успішна відповідь там буде 0000345, що вимагає потім видаляння leading zeroes.

Повна версія баш-клайенту, яка підтримує лише https та спочатку зберігає кожну відповідь ув темпоральному файлі, а результат друкує на stdout:

#!/usr/bin/env bash

set -e -o pipefail
. "$(dirname "$(readlink -f "$0")")/lib.bash"

tmp=`mktemp fetch.XXXXXX`
trap 'rm -f $tmp' 0 1 2 15
eh() { echo "$*" 1>&2; exit 2; }

[ $# = 3 ] || eh Usage: fetch.bash proxy_host proxy_port url
proxy_host=$1
proxy_port=$2
url=$3

get() {
    url_parse "$1"; [ "${URL[proto]}" = https ] || return 2

    exec 3<> "/dev/tcp/$proxy_host/$proxy_port" || return 2
    echo "${URL[hostname]}" >&3
    printf "GET %s HTTP/1.1\r\n" "${URL[pathname]}${URL[search]}${URL[hash]}" >&3
    printf '%s: %s\r\n' Host "${URL[hostname]}" Connection close >&3
    printf '\r\n' >&3
    cat <&3
}

get "$url" > "$tmp" || eh ':('

offset_calc() {
    if strings "`command -v sh`" | grep busybox >/dev/null; then
        od -N $((32*1024)) -t x1 -Ad -w1 -v "$tmp" | tr '\n' ' ' | \
            grep -o '....... 0d ....... 0a ....... 0d ....... 0a' | \
            awk '{if (NR==1) print $7+0}'
    else
        grep -aobx $'\r' "$tmp" | head -1 | tr -d '\r\n:' | \
            xargs -r expr 1 +
    fi || echo -1
}
offset=`offset_calc`
headers() { head -c "$offset" "$tmp" | tr -d '\r'; }
hdr() { headers | grep -m1 -i "^$1:" | cut -d' ' -f2; }

status=`head -1 "$tmp" | cut -d' ' -f2`
case "$status" in
    200) [ "$offset" = -1 ] && offset=-2 # invalid responce, dump all
         tail -c+$((offset + 2)) "$tmp" ;;
    302)
        headers 1>&2; echo 1>&2
        hdr location | xargs "$0" "$1" "$2" ;;
    *) headers 1>&2; exit 1
esac

Коли воно бачить 302, то викалупує Location: і рекурсивно кличе самого себе з новою URL. Має працювати навіть на вбогому Alpine Linux.

$ grep PRETTY /etc/os-release
PRETTY_NAME="Alpine Linux v3.19"
$ ./fetch.bash 192.168.11.109 1234 https://github.com/stunnel/static-curl/releases/download/8.6.0/curl-linux-arm64-8.6.0.tar.xz > curl.tar.xz
HTTP/1.1 302 Found
Location: https://objects.githubusercontent.com/…
…

$ file curl.tar.xz
curl.tar.xz: XZ compressed data, checksum CRC64
$ tar xf curl.tar.xz
$ file curl
curl: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, stripped

Post a comment in response:

If you don't have an account you can create one now.
HTML doesn't work in the subject.
More info about formatting