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 প্রতিটা সারি এক কমান্ডে:
- 200 + সঠিক Content-Type + Content-Length (static GET)
- 304 with If-Modified-Since, zero body
- Keep-alive:
curl -v http://host/a http://host/b— এক connection-এ দুই request (Re-using existing connectionলাইনে প্রমাণ) - POST + Content-Length echo-back handler
- Traversal attempts → 404, log-এ attempt record
- Malformed request (binary garbage) → 400, server বেঁচে আছে
- Chunked endpoint
--raw-তে framing visible
নিজেকে চ্যালেঞ্জ করুন
- Pipelined requests — এক packet-ে দুই request, parser sequential dispatch করবে
- HEAD method — header same, body zero-byte (Content-Length তবু আসল!)
- Range requests —
Range: bytes=0-99→ 206 Partial Content - ETag support বনাম If-Modified-Since — strong validator-এর সুবিধা
- epoll integration — Tcp Echo Server প্রজেক্টের event loop-এ এই parser বসান, per-connection state machine বানিয়ে
- Slowloris defense — header timer, partial-header connection cutoff
যা এখানে শিখলেন, পরে যেখানে লাগবে
| এখানে | পরে |
|---|---|
| Stream→message framing | DNS resolver প্রজেক্ট — সেখানে binary framing |
| Parser size limits | সব network-facing code-এ fuzz-safe design |
| Path traversal canonicalization | Web framework-এর middleware, container mount escape |
| Conditional request flow | CDN/cache layer-এর revalidation economics |
| Chunked transfer | Streaming API, SSE — একই framing family |