Uji HTTP API Tanpa Utilitas Eksternal

Container berbasis hardened atau minimal base image sering kali memangkas utilitas jaringan standar seperti curl, wget, atau netcat demi mereduksi attack surface dan ukuran image. Kondisi ini menyulitkan eksekusi validasi kontrak API, pipeline sanity-check, atau konfigurasi HEALTHCHECK lokal.

Jika image container menyertakan Bash yang dikompilasi dengan flag --enable-net-redirections, manipulasi raw socket TCP dapat dilakukan langsung melalui virtual path /dev/tcp/HOST/PORT. Pendekatan ini mengeksekusi I/O jaringan murni via kernel syscall tanpa dependensi binary tambahan.

1. Inisialisasi Socket via File Descriptor

Bash merepresentasikan koneksi TCP dua arah dengan mengaitkan /dev/tcp ke File Descriptor (FD) tertentu menggunakan command exec.

# Membuka koneksi read-write pada File Descriptor 3
exec 3<>/dev/tcp/api.internal.local/8080

# Menutup File Descriptor setelah selesai
exec 3>&-
exec 3<&-

Perintah exec 3<>... menginstruksikan shell untuk memanggil syscall socket() dan connect(). Angka 3 dipilih untuk menghindari konflik dengan standard stream: 0 (stdin), 1 (stdout), dan 2 (stderr).

2. Konstruksi Payload HTTP/1.1 Mentah

Berkomunikasi langsung dengan raw socket mengharuskan kepatuhan mutlak terhadap spesifikasi RFC 7230 (HTTP/1.1). Dua hal kritis yang wajib dipenuhi:

  • Line Terminator CRLF: Setiap baris header harus diakhiri dengan . Perintah echo default Linux hanya mengirim (LF), yang ditolak oleh HTTP server ketat (seperti Go net/http atau Nginx). Gunakan printf.
  • Header Connection: close: HTTP/1.1 secara default mengaktifkan persistent connection (Keep-Alive). Tanpa header Connection: close, server tidak akan mengirim sinyal TCP FIN, menyebabkan loop pembacaan di Bash mengalami blocking/hang selamanya.

Contoh konstruksi request POST JSON dengan autentikasi:

PAYLOAD='{"event":"ping","status":"active"}'
CONTENT_LEN=${#PAYLOAD}

printf "POST /v1/webhook HTTP/1.1\r\n" >&3
printf "Host: api.internal.local:8080\r\n" >&3
printf "Authorization: Bearer sec-token-xyz\r\n" >&3
printf "Content-Type: application/json\r\n" >&3
printf "Content-Length: %d\r\n" "$CONTENT_LEN" >&3
printf "Connection: close\r\n" >&3
printf "\r\n" >&3
printf "%s" "$PAYLOAD" >&3

3. Parsing Status Code dan Response Body Streaming

Response HTTP terdiri dari Status Line, Response Headers, dan Response Body yang dipisahkan oleh baris kosong tunggal ( ). Parsing dilakukan secara linear menggunakan built-in read.

# 1. Baca Status Line
read -r -u 3 PROTOCOL STATUS_CODE STATUS_TEXT
# Hapus carriage return (CR) dari status text jika ada
STATUS_TEXT=$(printf '%s' "$STATUS_TEXT" | tr -d '\r')

# 2. Skip Headers hingga bertemu baris kosong
while IFS= read -r -u 3 line; do
    clean_line=$(printf '%s' "$line" | tr -d '\r')
    [[ -z "$clean_line" ]] && break
done

# 3. Stream Response Body
RESPONSE_BODY=""
while IFS= read -r -u 3 -n 4096 chunk; do
    RESPONSE_BODY+="$chunk"
done

4. Mitigasi Network Timeout

Koneksi socket mentah rentan hang jika backend mengalami deadlock atau paket TCP hilang. Bash menyediakan flag timeout bawaan pada builtin read melalui opsi -t <detik>.

Gunakan read -t untuk membatasi durasi tunggu per baris data guna mencegah container healthcheck hang tanpa batas.
TIMEOUT_SEC=3
if ! read -t "$TIMEOUT_SEC" -r -u 3 PROTOCOL STATUS_CODE STATUS_TEXT; then
    echo "ERR: Connection timed out setelah ${TIMEOUT_SEC}s" >&2
    exec 3>&-; exec 3<&-
    exit 1
fi

5. Skrip Lengkap: Validator Kontrak API & Healthcheck

Skrip berikut mengeksekusi uji kontrak POST webhook internal, memverifikasi HTTP response code, dan mengevaluasi JSON response body.

#!/usr/bin/env bash
set -euo pipefail

TARGET_HOST="127.0.0.1"
TARGET_PORT="8080"
TARGET_PATH="/healthz"
AUTH_TOKEN="secret-audit-key"
PAYLOAD='{"check":"liveness"}'

# Buka TCP socket via FD 3
exec 3<>"/dev/tcp/${TARGET_HOST}/${TARGET_PORT}"

# Kirim HTTP Request
printf "POST %s HTTP/1.1\r\n" "$TARGET_PATH" >&3
printf "Host: %s:%s\r\n" "$TARGET_HOST" "$TARGET_PORT" >&3
printf "Authorization: Bearer %s\r\n" "$AUTH_TOKEN" >&3
printf "Content-Type: application/json\r\n" >&3
printf "Content-Length: %d\r\n" "${#PAYLOAD}" >&3
printf "Connection: close\r\n" >&3
printf "\r\n" >&3
printf "%s" "$PAYLOAD" >&3

# Evaluasi status line dengan timeout 5 detik
if ! read -t 5 -r -u 3 PROTOCOL STATUS_CODE STATUS_MSG; then
    echo "FAIL: Request timeout ke ${TARGET_HOST}:${TARGET_PORT}" >&2
    exec 3>&-; exec 3<&-
    exit 2
fi

# Lewati response header
while IFS= read -t 2 -r -u 3 line; do
    clean_line=$(printf '%s' "$line" | tr -d '\r')
    [[ -z "$clean_line" ]] && break
done

# Ambil response body
BODY=""
while IFS= read -t 2 -r -u 3 line; do
    BODY+="$line"
done

# Tutup socket
exec 3>&-; exec 3<&-

# Verifikasi Kontrak HTTP Status
echo "HTTP Status: $STATUS_CODE"
if [[ "$STATUS_CODE" -ne 200 ]]; then
    echo "Contract Violation: Expected 200, got $STATUS_CODE" >&2
    echo "Response: $BODY" >&2
    exit 1
fi

echo "API Contract Valid: Endpoint merespons 200 OK."

Batasan Teknis

Mekanisme /dev/tcp memiliki batasan struktural:

  • Tanpa Dukungan TLS/HTTPS: Stream /dev/tcp beroperasi pada raw Layer 4 TCP. Handshake enkripsi TLS tidak dapat ditangani tanpa bantuan wrapper seperti openssl s_client. Gunakan metode ini untuk komunikasi intra-pod, internal mesh, service-to-service tanpa TLS termination, atau direct loopback healthcheck.
  • Ketergantungan Shell: Fitur ini spesifik Bash (bukan /bin/sh standar POSIX atau ash default Alpine) yang dikompilasi dengan net-redirections aktif. Pastikan shell target adalah bash.