Mengunduh file binary berukuran gigabyte seperti arsip aplikasi iOS (IPA) atau build artifact pada jaringan seluler memiliki risiko tinggi: koneksi terputus dan masa berlaku kredensial keamanan kedaluwarsa sebelum transfer tuntas. Jika arsitektur download hanya mengandalkan satu monolithic signed URL dengan masa aktif singkat (misalnya 15 menit), download 2 GB pada koneksi lambat akan menghasilkan 401 Unauthorized atau 403 Forbidden di tengah transmisi. Pengguna terpaksa mengulang proses dari bita ke-0.
Solusi teknis untuk masalah ini adalah memisahkan siklus hidup autentikasi dari durasi transfer menggunakan kontrak Resumable Chunked Transfer berbasis standar HTTP RFC 9110, dipadukan dengan token sesi efemeral.
Anatomi Masalah: Expired Auth pada Monolithic Streaming
Pola konvensional umumnya mengarahkan klien langsung ke Object Storage (misalnya S3 presigned URL) atau streaming langsung dari backend. Pendekatan ini rentan terhadap dua skenario kegagalan:
- Mid-stream Timeout: Token valid saat koneksi HTTP handshake dibuka, namun expired sebelum streaming buffer selesai dibaca oleh client.
- Network Interruption Pasca-Expiry: Jaringan terputus pada 80%, klien mencoba rekoneksi, tetapi URL kedaluwarsa sehingga server menolak kelanjutan range.
Desain Kontrak API: HTTP Range dan Ephemeral Session
Alih-alih memberikan akses penuh tak terbatas, arsitektur yang aman membagi file ke dalam batas transfer logis menggunakan header Range dan memvalidasi setiap rentang bita dengan token stateless berbasis HMAC.
1. Request Awal dan Inisiasi Sesi
Klien meminta metadata download untuk memperoleh ID file, ukuran total, ETag (hash konten untuk menjaga integritas file agar tidak berubah di tengah proses download), dan session token pertama.
GET /api/v1/downloads/artifacts/app-v2.ipa/session
Host: api.internal.net
Authorization: Bearer <user_jwt>
HTTP/1.1 200 OK
Content-Type: application/json
{
"file_id": "ipa-08f3a",
"total_bytes": 2147483648,
"etag": "\"c4ca4238a0b923820dcc509a6f75849b\"",
"chunk_size": 10485760,
"download_token": "eyJhbGciOiJIUzI1NiJ9..."
}2. Header Kontrak Chunk Transfer
Klien meminta potongan bita tertentu menggunakan header standar HTTP:
Range: bytes=0-10485759: Menentukan rentang byte yang diminta (misal: 10 MB).If-Range: "c4ca4238a0b923820dcc509a6f75849b": Memastikan file di server tidak mengalami perubahan versi antar-request. Jika ETag berubah, server mengembalikan status200 OKdengan seluruh isi file, bukan206 Partial Content.X-Download-Token: <token>: Ephemeral stateless token yang mengikat offset range.
Pencegahan Token Replay Attack dan Manipulasi Offset
Signed URL statis rawan disalahgunakan pihak ketiga jika bocor. Untuk mencegah pencurian hak akses dan replay attack:
- Offset Binding: Token HMAC mengenkapsulasi
file_id,allowed_start_byte,session_id, danexp. Token yang diterbitkan untuk byte10485760tidak dapat digunakan untuk meminta byte0. - Monotonic High-Water Mark: Server atau payload token memastikan klien tidak dapat meminta ulang blok acak secara acak tanpa memperbarui handshake.
- Handshake Refresh Deterministik: Jika token kedaluwarsa di tengah jalan, klien melakukan rotasi token menggunakan endpoint refresh ringan (tanpa membatalkan file stream yang telah diterima sebelumnya).
Implementasi Backend Handler (Node.js)
Handler berikut mengimplementasikan validasi token berbasis HMAC, verifikasi Range header, serta streaming bita partial tanpa merusak download yang sedang berjalan.
import http from 'node:http';
import fs from 'node:fs';
import crypto from 'node:crypto';
const SECRET_KEY = process.env.DOWNLOAD_SECRET || 'kunci-rahasia-arsip-biner';
const FILE_STORAGE_PATH = './storage/app-release.ipa';
function verifyRangeToken(token, fileId, rangeStart) {
try {
const raw = Buffer.from(token, 'base64url').toString('utf-8');
const [tFileId, tStart, exp, signature] = raw.split(':');
// 1. Validasi expiry
if (Date.now() > Number(exp)) return false;
// 2. Validasi kesesuaian target dan offset
if (tFileId !== fileId || Number(tStart) !== rangeStart) return false;
// 3. Validasi HMAC (timing-safe)
const expectedSig = crypto
.createHmac('sha256', SECRET_KEY)
.update(`${tFileId}:${tStart}:${exp}`)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSig));
} catch {
return false;
}
}
export function handleDownloadChunk(req, res) {
const fileId = 'ipa-08f3a';
const rangeHeader = req.headers['range'];
const token = req.headers['x-download-token'];
if (!rangeHeader) {
res.writeHead(400, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ error: 'Header Range diwajibkan' }));
}
const stat = fs.statSync(FILE_STORAGE_PATH);
const totalSize = stat.size;
// Format: bytes=start-end
const parts = rangeHeader.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : totalSize - 1;
if (start >= totalSize || end >= totalSize) {
res.writeHead(416, { 'Content-Range': `bytes */${totalSize}` });
return res.end();
}
// Validasi token yang terikat pada offset 'start'
if (!token || !verifyRangeToken(token, fileId, start)) {
// ponytail: fallback direct signed url check jika transisi ke S3
res.writeHead(401, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({
error: 'Token kedaluwarsa atau invalid untuk offset ini',
code: 'AUTH_OFFSET_EXPIRED',
next_valid_offset: start
}));
}
res.writeHead(206, {
'Content-Range': `bytes ${start}-${end}/${totalSize}`,
'Accept-Ranges': 'bytes',
'Content-Length': end - start + 1,
'Content-Type': 'application/octet-stream',
});
const stream = fs.createReadStream(FILE_STORAGE_PATH, { start, end });
stream.pipe(res);
}Skipped: integrasi Redis blacklist untuk token revocation; tambahkan ketika pembatalan sesi download di tengah jalan menjadi kebutuhan compliance.
Siklus Handshake Refresh Deterministik
Ketika klien menerima respons status 401 dengan kode AUTH_OFFSET_EXPIRED, download engine klien tidak boleh me-reset state. Urutan penanganannya adalah:
- Hentikan buffer read stream lokal pada bita terakhir yang diterima secara utuh.
- Panggil endpoint
POST /api/v1/downloads/refresh-tokendengan melampirkansession_iddancurrent_offset. - Server memverifikasi progres transfer lalu merilis token baru yang diikat khusus ke
current_offsetberikutnya. - Klien menyambung kembali pengunduhan via
Range: bytes={current_offset}-tanpa kehilangan data yang sudah terunduh.
Trade-offs dan Pertimbangan Performa
Peringatan: Hindari ukuran chunk yang terlalu kecil (misal: di bawah 1 MB). Overhead SSL/TLS handshake dan round-trip TCP akan menurunkan throughput pengunduhan secara drastis pada jaringan berlatensi tinggi.
- Throughput vs Keamanan: Chunk ukuran 10 MB sampai 20 MB memberikan rasio optimal antara efisiensi TCP windowing dan frekuensi refresh token keamanan.
- Integrasi Storage CDN/Object: Bila menggunakan Amazon S3 atau Cloudflare R2, gantikan endpoint streaming dengan proxy presigned URL pendek yang memvalidasi header
Rangesebelum menerbitkan 302 Redirect sementara ke backend storage.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!