![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
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, про що було ув цьому бложику на початку року.
Зі сторони баша-клайента залишається декілька питань:
будь-який файл з "releases" сторінок ґітхабу мандрує через 302 рідайректа, тому щоб щось завантажити, треба робити 2 ріквести;
як парсити відповіді? Як мінімум треба знаходити
\r\n\r\n
, який відділяє headers від омріяних байтів curl.tar.xz; це означає передивлятися байнарний стрім, що є напрочуд неприємно робити ув шелі;як парсити 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