Protokol jaringan adalah kontrak yang menentukan urutan byte apa yang berarti apa. HTTP memulai dengan baris GET /path HTTP/1.0\r\n, DNS mengemas query ke 12-byte header lalu record berformat TLV, dan TLS handshake dimulai dengan byte 0x16 (handshake) diikuti versi dan panjang. Bab ini membahas dua kategori parsing: teks line-based (seperti HTTP request) yang menggunakan delimiter, dan biner terstruktur (seperti TLV) yang menggunakan field panjang. Keduanya memerlukan kehati-hatian untuk menghindari buffer overflow dan parsing yang ambigu.

Kategori Format Wire #

Format wire jaringan dibagi menjadi dua kategori besar:

  1. Teks line-based: pesan disusun dari baris-baris teks, masing-masing diakhiri \r\n (CRLF). HTTP/1.x, SMTP, FTP, IRC menggunakan format ini. Mudah dibaca manusia, mudah di-debug dengan tcpdump.
  2. Biner terstruktur: pesan terdiri dari field biner dengan tipe dan panjang eksplisit. DNS, TLS, gRPC, MQTT menggunakan format ini. Hemat bandwidth, tetapi butuh parser yang teliti.

Pilihan antara keduanya adalah tradeoff antara kejelasan (teks menang) dan efisiensi (biner menang). Banyak protokol modern menggunakan biner karena kebutuhan throughput tinggi—HTTP/2 dan HTTP/3 menggunakan biner, bukan teks seperti HTTP/1.x.

Parser HTTP-like Teks #

Mari kita implementasikan parser sederhana untuk request HTTP/1.0. Formatnya:

text
GET /index.html HTTP/1.0\r\n
Host: localhost\r\n
User-Agent: DemoClient/1.0\r\n
\r\n

Baris pertama adalah request line yang berisi method, path, dan versi dipisah spasi. Baris-baris berikutnya adalah header dalam format Key: Value. Baris kosong (\r\n saja) menandakan akhir header.

c
/* File: parse1.c - Parser protokol teks "GET /path HTTP/1.0" */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

typedef struct {
    char method[8];
    char path[256];
    char version[16];
} RequestLine;

/* Parse baris permintaan dengan sscanf */
static int parse_request_line(const char *line, RequestLine *req) {
    int matched = sscanf(line, "%7s %255s %15s",
                         req->method, req->path, req->version);
    if (matched != 3) {
        return -1;
    }
    return 0;
}

/* Parse baris header "Key: Value" */
static int parse_header(const char *line, char *key, size_t klen,
                       char *val, size_t vlen) {
    /* cari ":" */
    const char *colon = strchr(line, ':');
    if (colon == NULL) return -1;

    size_t klen_actual = (size_t)(colon - line);
    if (klen_actual >= klen) klen_actual = klen - 1;
    memcpy(key, line, klen_actual);
    key[klen_actual] = '\0';

    /* lewati spasi setelah ":" */
    const char *p = colon + 1;
    while (*p == ' ' || *p == '\t') p++;
    strncpy(val, p, vlen - 1);
    val[vlen - 1] = '\0';
    /* hapus trailing \r\n */
    char *cr = strchr(val, '\r');
    if (cr) *cr = '\0';
    char *lf = strchr(val, '\n');
    if (lf) *lf = '\0';
    return 0;
}

int main(void) {
    const char *request =
        "GET /index.html HTTP/1.0\r\n"
        "Host: localhost\r\n"
        "User-Agent: DemoClient/1.0\r\n"
        "\r\n";

    /* Pecah per baris */
    char buf[1024];
    strncpy(buf, request, sizeof(buf) - 1);
    buf[sizeof(buf) - 1] = '\0';

    char *saveptr = NULL;
    char *line = strtok_r(buf, "\n", &saveptr);
    int line_no = 0;
    RequestLine req;
    int header_count = 0;

    while (line != NULL) {
        line_no++;
        /* hapus \r di akhir */
        size_t len = strlen(line);
        if (len > 0 && line[len - 1] == '\r') line[len - 1] = '\0';

        if (line_no == 1) {
            if (parse_request_line(line, &req) != 0) {
                printf("Gagal parse request line: %s\n", line);
                return 1;
            }
            printf("Request line:\n");
            printf("  Method : %s\n", req.method);
            printf("  Path   : %s\n", req.path);
            printf("  Version: %s\n", req.version);
        } else if (line[0] == '\0') {
            printf("(Akhir header)\n");
            break;
        } else {
            char key[64], val[256];
            if (parse_header(line, key, sizeof(key), val, sizeof(val)) == 0) {
                header_count++;
                printf("Header %d: %s = %s\n", header_count, key, val);
            }
        }
        line = strtok_r(NULL, "\n", &saveptr);
    }

    printf("\nTotal header diparse: %d\n", header_count);
    return 0;
}

Compile dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -o parse1 parse1.c
./parse1

Output yang Dihasilkan:

text
Request line:
  Method : GET
  Path   : /index.html
  Version: HTTP/1.0
Header 1: Host = localhost
Header 2: User-Agent = DemoClient/1.0
(Akhir header)

Total header diparse: 2

Mari kita telaah baris-baris kode di atas:

  1. Baris 14-21 (parse_request_line): Menggunakan sscanf dengan width specifier %7s %255s %15s yang membatasi jumlah byte yang dibaca untuk setiap field. Batasan ini mencegah buffer overflow bila input lebih panjang dari buffer tujuan. Tanpa width specifier, sscanf akan menulis tanpa batas dan menyebabkan overflow.
  2. Baris 23-43 (parse_header): Mencari : dengan strchr untuk memisahkan key dan value. colon - line memberi offset posisi colon dalam string. Setelah colon, lewati whitespace ( , \t) sebelum value. Akhirnya hapus \r\n di akhir value agar bersih.
  3. Baris 64 (strtok_r(buf, "\n", &saveptr)): Memecah string berdasarkan \n (line feed). strtok_r adalah versi reentrant dari strtok—aman dipanggil bersamaan dari multiple thread. Kita menggunakan \n sebagai delimiter, lalu menghapus \r secara manual untuk menangani baris CRLF (\r\n) yang umum di protokol jaringan.
  4. Baris 73 (line[0] == '\0'): Baris kosong (hanya \r\n) menandakan akhir header. Setelah strtok_r membuang \n dan kita menghapus \r, baris kosong menjadi string kosong "". Break dari loop di sini.

Format TLV (Type-Length-Value) Biner #

Untuk protokol biner, pola yang umum adalah TLV: setiap field memiliki 1 byte type, 2 byte length, lalu length byte value. Field diulang hingga akhir pesan. Format ini dipakai di DNS record, ASN.1, dan banyak protokol biner lain.

c
/* File: parse2.c - Parser biner TLV (Type-Length-Value) */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <arpa/inet.h>

/* Format: [type:1][length:2][value:length] berulang */
typedef enum {
    TYPE_STRING = 1,
    TYPE_INT32  = 2,
    TYPE_FLOAT  = 3
} TipeData;

/* Builder: tambah field TLV ke buffer */
static size_t tlv_append(unsigned char *buf, size_t off,
                         uint8_t type, const void *value, uint16_t len) {
    buf[off++] = type;
    uint16_t net = htons(len);
    memcpy(buf + off, &net, 2); off += 2;
    memcpy(buf + off, value, len); off += len;
    return off;
}

int main(void) {
    unsigned char buf[256];
    size_t off = 0;

    /* Field 1: string "Budi" */
    const char *nama = "Budi";
    off = tlv_append(buf, off, TYPE_STRING, nama, (uint16_t) strlen(nama));

    /* Field 2: int32 usia (network order) */
    uint32_t usia = htonl(25);
    off = tlv_append(buf, off, TYPE_INT32, &usia, 4);

    /* Field 3: float tinggi (network order) */
    float tinggi_h = 168.5f;
    uint32_t tinggi_raw;
    memcpy(&tinggi_raw, &tinggi_h, 4);
    uint32_t tinggi_n = htonl(tinggi_raw);
    off = tlv_append(buf, off, TYPE_FLOAT, &tinggi_n, 4);

    printf("Total byte serialized: %zu\n", off);
    printf("Hex: ");
    for (size_t i = 0; i < off; i++) printf("%02x ", buf[i]);
    printf("\n\n");

    /* Parser: baca kembali TLV */
    size_t pos = 0;
    int field_no = 0;
    while (pos < off) {
        uint8_t type = buf[pos++];
        uint16_t len_net;
        memcpy(&len_net, buf + pos, 2); pos += 2;
        uint16_t len = ntohs(len_net);

        field_no++;
        printf("Field %d: type=%u, len=%u, ", field_no, type, len);
        switch (type) {
            case TYPE_STRING:
                printf("value(string)=\"%.*s\"\n", len, buf + pos);
                break;
            case TYPE_INT32: {
                uint32_t v_net;
                memcpy(&v_net, buf + pos, 4);
                printf("value(int32)=%u\n", ntohl(v_net));
                break;
            }
            case TYPE_FLOAT: {
                uint32_t v_net;
                memcpy(&v_net, buf + pos, 4);
                uint32_t v_host = ntohl(v_net);
                float f;
                memcpy(&f, &v_host, 4);
                printf("value(float)=%.2f\n", f);
                break;
            }
            default:
                printf("value(unknown type)\n");
        }
        pos += len;
    }
    return 0;
}

Compile dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -o parse2 parse2.c
./parse2

Output yang Dihasilkan:

text
Total byte serialized: 21
Hex: 01 00 04 42 75 64 69 02 00 04 00 00 00 19 03 00 04 43 28 80 00 

Field 1: type=1, len=4, value(string)="Budi"
Field 2: type=2, len=4, value(int32)=25
Field 3: type=3, len=4, value(float)=168.50

Mari kita telaah baris-baris kode di atas:

  1. Baris 21-27 (tlv_append): Membangun satu field TLV ke buffer. Type (1 byte) ditulis langsung, lalu length (2 byte) dikonversi ke network order dengan htons. Value disalin apa adanya—pemanggil bertanggung jawab mengkonversi value ke network order sebelum memanggil.
  2. Baris 39 (off = tlv_append(buf, off, TYPE_STRING, nama, strlen(nama))): Type 1 (string) dengan length 4 (“Budi” = 4 byte). Tidak ada konversi byte order karena byte string adalah byte—urutannya sama di semua arsitektur.
  3. Baris 43 (usia = htonl(25)): Type 2 (int32), value harus dikonversi ke network order sebelum diserahkan ke tlv_append. Tanpa htonl, mesin penerima little-endian akan membaca 25 sebagai 419430400 (0x19000000).
  4. Baris 50-56 (loop while parsing): Loop terus berjalan selama pos < off. Untuk setiap field: baca 1 byte type, 2 byte length (dengan ntohs), lalu decode value sesuai type. Pemakaian switch (type) memungkinkan extensible—menambah type baru cukup menambah case baru.
  5. Hex output 01 00 04 42 75 64 69 ...: 01 adalah type=1 (string), 00 04 adalah length=4 dalam big-endian, 42 75 64 69 adalah ASCII “Budi” (0x42=‘B’, 0x75=‘u’, 0x64=’d’, 0x69=‘i’).

Keamanan Parsing: Bounds Check #

Parser yang baik selalu memeriksa batas buffer sebelum membaca. Tanpa bounds check, attacker bisa mengirim pesan dengan field length yang sangat besar (mis. 0xFFFF), menyebabkan pembacaan keluar dari buffer. Berikut checklist keamanan parsing:

  • Selalu cek pos + len <= buffer_total sebelum membaca field value.
  • Batasi maksimum len untuk tipe string (mis. if (len > 4096) return -1;).
  • Gunakan memcpy (panjang eksplisit), bukan strcpy/sprintf (panjang tidak diketahui).
  • Hitung total panjang field sebelumnya untuk memastikan total tidak melebihi buffer.
c
/* Contoh: bounds check sebelum membaca value */
if (pos + len > buffer_total) {
    return -1;  /* pesan rusak: length melebihi sisa buffer */
}