Foundationপ্রথম নীতি থেকে
LEVEL 7কঠিন~২০ ঘণ্টাC

HTTP/1.1 Server

HTTP/1.1 Server

Request-line আর header parser লিখে শুরু, keep-alive loop, Content-Length body, static file serving + mime table + path-traversal defense, If-Modified-Since-এ 304, chunked response — RFC 7230-এর মূল মেশিনারি C-তে হাতে-কলমে। curl -v acceptance checklist দিয়ে যাচাই।

মাইলস্টোন

আগে যা পড়া দরকার

কেন এই প্রজেক্ট

Http লেসনে request/response format আর method/status code-এর অভিধান দেখেছি; sockets-api-তে TCP-র byte stream। দুইয়ের সংযোগস্থলেই আসল প্রশ্ন: stream থেকে message বানানো। TCP “message” চেনে না — \r\n\r\n খুঁজে frame ভাগ করতে হয়, body length হিসাবে জানতে হয়, আর এক socket-এ টানা একাধিক request (keep-alive) এলে parser-কে state মনে রাখতে হয়।

এই প্রজেক্টের দ্বিতীয় শিক্ষা security: static file serving মানেই user-controlled path নিজের filesystem-এ খোলা — GET /../../../../etc/passwd ঠেকানো যে একটা string check নয়, canonical path verification, সেটা hand-on ছাড়া বোঝা যায় না। শেষ পর্যন্ত আপনি nginx-এর যে core loop বুঝবেন, সেটা ১৯৯০-এর CERN server থেকে আজও অপরিবর্তিত।

ধাপে ধাপে

১. Request-line + header parser, size limits সহ

typedef struct {
    char method[8];
    char target[2048];
    char version[16];
    char name[32][64];      /* header names */
    char value[32][256];    /* header values */
    int  nheaders;
} request;

/* ধারণা: recv করে '\r\n\r\n' পর্যন্ত accumulate, limit ছোঁয়া মাত্র 431 */
ssize_t read_headers(int c, char *buf, size_t cap) {
    size_t used = 0;
    for (;;) {
        ssize_t k = read(c, buf + used, cap - used);
        if (k <= 0) return k;
        used += k;
        if (used >= cap) { respond(c, 431); return -1; }
        if (memmem(buf, used, "\r\n\r\n", 4)) return used;
    }
}

Parse rules: request line = METHOD SP target SP version CRLF; প্রতিটা header = name ':' OWS value CRLF। Name case-insensitive (Content- Length, content-length এক) — compare-এর আগে lowercase করুন। Limit দুটো কেন জরুরি: attacker ১০ GB header পাঠালে memory ফুরাবে; cap + 431 response মানেই robust server আর crash-এর মধ্যে পার্থক্য।

Checkpoint: printf 'GET / HTTP/1.1\r\nHost: x' | nc localhost 8080 — incomplete request-এ server ঝুলবে না, timeout/close করবে।

২. Keep-alive loop

HTTP/1.1-এ connection ডিফল্ট persistent; বন্ধ করার শর্ত তিনটা: client-এ Connection: close, response-এ আপনি Connection: close পাঠালেন, বা parse error।

for (;;) {
    ssize_t n = read_request(c, &req);
    if (n <= 0) break;
    handle(c, &req);
    if (!keep_alive(&req)) break;
}
close(c);

সূক্ষ্ম জায়গাটা: response পাঠানোর পরে decide করবেন connection থাকবে কি না, আর next read-এ client বন্ধ করলে clean exit। Idle connection-এর জন্য read timeout রাখুন (SO_RCVTIMEO) — নইলে অর্ধেক খোলা socket server-এর fd খেয়ে ফেলবে।

৩. Content-Length bodies

POST request-এ headers শেষ মানে request শেষ না — ঠিক N byte আরো আছে:

const char *cl = header_get(&req, "content-length");
size_t body_len = cl ? strtoul(cl, NULL, 10) : 0;

char *body = malloc(body_len + 1);
size_t got = leftover_len;               /* parser-এ already-read excess! */
memcpy(body, leftover, got);
while (got < body_len) {
    ssize_t k = read(c, body + got, body_len - got);
    if (k <= 0) goto fail;
    got += k;
}

Classic bug এখানেই: parser buffer-এ header block-এর পরের byte-গুলো already read হয়ে আছে (pipelining-এ পরের request সহ!) — ওই excess না রেখে ফেলে দিলে body corrupt, আর keep-alive loop-এ পরের request-ও corrupt। Leftover bytes carry করাটাই stream-parsing-এর মূল শৃঙ্খলা।

৪. Static serving + mime table + traversal defense

/* defense: canonicalize, তারপর docroot prefix check */
char full[4096], real[4096];
snprintf(full, sizeof full, "%s%s", DOCROOT, url_path);
if (!realpath(full, real) ||
    strncmp(real, DOCROOT_REAL, strlen(DOCROOT_REAL)) != 0) {
    respond(c, 404);
    return;
}

static const struct { const char *ext, *type; } mime[] = {
    {"html", "text/html; charset=utf-8"},
    {"css",  "text/css"},
    {"js",   "text/javascript"},
    {"png",  "image/png"},
    {"txt",  "text/plain; charset=utf-8"},
};

Path-traversal attack নিজে ঘটান: curl --path-as-is 'http://localhost:8080/../etc/passwd' — raw %2e%2e%2f decode করে ও চেষ্টা করুন। Defense-এর সারমর্ম: string-এ .. খোঁজা যথেষ্ট নয় (encoding, symlink, double-slash সব bypass), canonical resolved path docroot-এর ভেতরে কি না সেটাই প্রশ্ন। Symlink escape-ও টেস্ট করুন — docroot-এর ভেতরে /etc point করা link বসিয়ে।

Response: status line, Content-Type (mime table), Content-Length, Last-Modified, তারপর sendfile() বা read-write loop-এ body।

৫. If-Modified-Since → 304

/* stat() করে mtime → RFC 1123 format */
gmtime_r(&st.st_mtime, &tm);
strftime(buf, sizeof buf, "%a, %d %b %Y %H:%M:%S GMT", &tm);

/* request-এ If-Modified-Since থাকলে timegm() দিয়ে compare */
if (ims && st.st_mtime <= ims_time) {
    dprintf(c, "HTTP/1.1 304 Not Modified\r\nLast-Modified: %s\r\n\r\n", buf);
    return;                               /* body নেই — 304-এ কখনোই না */
}

Flow যাচাই: প্রথম curl -v-এ 200 + Last-Modified header copy করুন, দ্বিতীয়বার -H 'If-Modified-Since: <that>' পাঠান — 304, zero body। Browser cache validation এভাবেই চলে; bandwidth saving-এর পুরো মেকানিজম দুটো header-এর হাতছাড়া।

৬. Chunked response writer

Body-size আগে জানা না থাকলে (dynamic content) chunked encoding:

dprintf(c, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n");
while ((n = produce_next_block(block, sizeof block)) > 0) {
    dprintf(c, "%zx\r\n", n);             /* hex size */
    write_all(c, block, n);
    write_all(c, "\r\n", 2);
}
write_all(c, "0\r\n\r\n", 5);              /* terminal chunk */

প্রতিটা chunk-এ size line, data, CRLF; শেষে 0\r\n\r\n। Acceptance: curl --raw -v http://localhost:8080/stream — wire-এ chunk framing চোখে দেখুন; curl নিজে decode করে ফেলে বলে --raw দরকার।

৭. curl -v acceptance checklist

$ curl -sv http://localhost:8080/index.html -o /dev/null
< HTTP/1.1 200 OK
< Content-Type: text/html; charset=utf-8
< Content-Length: 512
< Last-Modified: Sun, 23 Aug 2026 ...
$ curl -sv http://localhost:8080/missing.html -o /dev/null
< HTTP/1.1 404 Not Found

Checklist প্রতিটা সারি এক কমান্ডে:

  1. 200 + সঠিক Content-Type + Content-Length (static GET)
  2. 304 with If-Modified-Since, zero body
  3. Keep-alive: curl -v http://host/a http://host/b — এক connection-এ দুই request (Re-using existing connection লাইনে প্রমাণ)
  4. POST + Content-Length echo-back handler
  5. Traversal attempts → 404, log-এ attempt record
  6. Malformed request (binary garbage) → 400, server বেঁচে আছে
  7. Chunked endpoint --raw-তে framing visible

নিজেকে চ্যালেঞ্জ করুন

  1. Pipelined requests — এক packet-ে দুই request, parser sequential dispatch করবে
  2. HEAD method — header same, body zero-byte (Content-Length তবু আসল!)
  3. Range requests — Range: bytes=0-99 → 206 Partial Content
  4. ETag support বনাম If-Modified-Since — strong validator-এর সুবিধা
  5. epoll integration — Tcp Echo Server প্রজেক্টের event loop-এ এই parser বসান, per-connection state machine বানিয়ে
  6. Slowloris defense — header timer, partial-header connection cutoff

যা এখানে শিখলেন, পরে যেখানে লাগবে

এখানেপরে
Stream→message framingDNS resolver প্রজেক্ট — সেখানে binary framing
Parser size limitsসব network-facing code-এ fuzz-safe design
Path traversal canonicalizationWeb framework-এর middleware, container mount escape
Conditional request flowCDN/cache layer-এর revalidation economics
Chunked transferStreaming API, SSE — একই framing family