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 status 200 OK dengan seluruh isi file, bukan 206 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:

  1. Offset Binding: Token HMAC mengenkapsulasi file_id, allowed_start_byte, session_id, dan exp. Token yang diterbitkan untuk byte 10485760 tidak dapat digunakan untuk meminta byte 0.
  2. Monotonic High-Water Mark: Server atau payload token memastikan klien tidak dapat meminta ulang blok acak secara acak tanpa memperbarui handshake.
  3. 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:

  1. Hentikan buffer read stream lokal pada bita terakhir yang diterima secara utuh.
  2. Panggil endpoint POST /api/v1/downloads/refresh-token dengan melampirkan session_id dan current_offset.
  3. Server memverifikasi progres transfer lalu merilis token baru yang diikat khusus ke current_offset berikutnya.
  4. 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 Range sebelum menerbitkan 302 Redirect sementara ke backend storage.