Skip to content

HTTP server quramiz: RFC-compliant, State Machines with Invariants. Part 1

Asinxron server? Noldan? Albatta. Xotira boshqaruvi, connection lifecycle va parserlarga alohida e'tibor beramiz. Va bu safar chuqurroq yondashuv - hamma narsa state machine!

Introduction

HTTP server invariantlar asosida fikrlashni rivojlantirishda eng ajoyib projectlardan biridir. U nazariy tushunchalarni aktiv amaliyot bilan bog'lashda juda qo'l keladi. Yana bir bor C dasturlash tilini tanlashim sababi esa, men bu yerda erkinman, hammasini o'z ko'zim bilan ko'ra olaman - til mendan hech nimani hide qilmaydi.

Note

Biz oldin ham HTTP server qurishga urinib ko'rganmiz: cserve. Biroq legacy projectimizda xuddi PM kabi fikrlash bo'lgan: fichalar checklistini to'ldirsam bo'ldi degan narsa bor edi. Shuningdek maqsad sifatli server chiqarish emas, socket programmingni yaqinroq o'rganish ham ediki, istagancha xatolar qilib tashlaganmiz.

Kinetic bilan esa niyatimiz "invariantcha" fikrlashni rivojlantirish, tizimlarni haqiqatan tizimli qurishni o'rganishdir.

Bu safar biz ko'r-ko'rona nginx dan ko'chirmaymiz. Aksincha, yanada zamonaviy, cloud-native yechimlarni ko'rib, xususan Traefik, ular bergan yechimlarni o'zimizga moslab qurishga harakat qilamiz. Shuningdek, har bir qator kod to'liq RFC asosida yoziladi, hech nimani rejasiz, yoki checklist sifatida implement qilmaymiz.

Document Role
RFC 9110 HTTP semantics (methods, status codes, fields, content, conditionals)
RFC 9112 HTTP/1.1 message syntax, framing, connection management
RFC 9111 HTTP caching (when serving cacheable responses)
RFC 9117 Expect: 100-continue
RFC 7230 - RFC 7235 / RFC 2616 superseded by the 911x series, historically relevant
RFC 2119 / RFC 8174 Meaning of MUST, SHOULD, MAY

RFC qoidalariga ko'ra, bizda 3 asosiy kalit so'zlar bor va biz ularga amal qilishimiz shart:

  • MUST / MUST NOT - non-negotiable for a conformant server.
  • SHOULD / SHOULD NOT - strong default; deviate only with documented reason.
  • MAY - optional; listed when it affects interoperability or security posture.

Invariantlar ro'yxati post oxirida berilgan bo'lib, sarguzashtimiz davomida qulay reference qilish uchun har bir invariant maxsus ID ga ega bo'ldi. Keling endi serverimiz o'zi qanday state machine, avval shuni tushunib olamiz. Quyidagi chizma bizga aniq ma'lumot beradi:

HTTP/1.1 RFC compliant server diagram

Rasmdan ko'rishimiz mumkinki, ko'zimizga kichik ko'ringan "parse-process-response" loopimiz aslida juda katta va sub-machine'larga boy system bo'lib chiqmoqda. Read va Write fazalarini alohida qismlar sifatida process qilishimiz keyinchalik pipelining invariantlari satisfy bo'lishiga yo'l ochadi (keyingi postlarda). Persistent connectionlarni handle qilish, taymerlar, barchasi tizimli o'z o'rnida joylashgan - chegaralar aniq, yo'llar ham. Implement qilish kerak xolos.

Requirements

Serverni qurishdan oldin toolchain va tashqi kutubxonalarni belgilab olamiz:

  • CMake - C/C++ dagi projectlar uchun standard build system. Har bir platformga alohida Makefile yozib chiqmaslik uchun bu build system ishlatishimiz ma'qulroq.
  • libuv - Node.js ostida yotgan asinxron I/O kutubxonasi. Bizga u event loop, non-blocking TCP socketlar, signal handling va timerlarni bitta portable API ostida beradi. Ya'ni biz epoll_create1/epoll_ctl/epoll_wait ni qo'lda yozmaymiz, libuv platformaga qarab o'zi to'g'ri backendni tanlaydi. cserveda biz to'g'ridan-to'g'ri epoll chaqirganmiz, natijada kod Linux'ga qaram bo'lib qolgandi. Kinetic bunday xatoni takrorlamaydi.
  • libyaml - YAML parser, sof C'da yozilgan. Ishlashi Event-based: hujjatni token stream sifatida o'qiydi, memory tree qurib o'tirmaydi.

Nega YAML?

Konfiguratsiya formati tanlashda variantlar ko'p edi: TOML, JSON, INI, yoki o'zimizning DSL. Biz YAML'ni tanladik, chunki maqsadimiz Traefik va boshqa cloud-native proxy'lar kabi yechim qurish. Ular deyarli barchasi YAML'da yozilgan config'lardan foydalanadi: inson uchun o'qish oson, ichma-ich (nested) strukturalarni tabiiy ifodalaydi, va izohlar (#) qo'llab-quvvatlanadi. JSON izoh bermaydi, INI esa chuqur nesting'da qiynaladi. YAML bu ikki chegaraning o'rtasida turadi (garchi uning ham o'ziga xos muammolari bo'lsa ham, bizni use-casega tushadi).

Sample config va static config location

Traefik configlarini static va dynamic alohida qismlarga ajratadi: static config server ishga tushishida bir marta o'qiladi (portlar, log levels, providerlar, etc.); dynamic config esa runtimeda o'zgarishi mumkin (masalan docker compose configlari dynamic config bo'la oladi). Static config'ni default pathini quyidagicha define qildik:

#define DEFAULT_STATIC_CONFIG_DIR "/etc/kinetic/kinetic.yaml"

Namunaviy configni Traefik'dan tikka chopdik, albatta maqsad bilan:

log:
  level: INFO
  format: common

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: ":443"

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
  file:
    filename: "/etc/traefik/dynamic_config.yml"
    watch: true

Hozircha to'liq configni o'qishimiz shart emas, bizga 3 ta asosiy data yetarli:

typedef struct {
    char name[128];
    int listen_port;
    char log_level[32];
} ktc_config_t;

Base TCP Server

Endi asos - libuv event loop ustidagi eng oddiy TCP echo server. Bu bosqichda HTTP haqida hali gap yo'q: biz faqat connectionni qabul qilamiz, kelgan baytlarni aynan qaytaramiz (echo), va connection lifecycle'ni to'g'ri boshqarishni o'rganamiz. Aynan mana shu bosqichda Global Lifecycle invariantlarining poydevorini qo'yamiz.

Kod: 777d68f

Butun server global app struct atrofida qurilgan:

typedef struct {
    uv_loop_t *loop;
    uv_tcp_t server;
    uv_signal_t sig_int, sig_term, sig_hup;
    bool is_shutting_down;
} ktc_app_t;

static ktc_app_t app;

Har bir client connection uchun esa alohida kontekst:

typedef struct {
    uv_tcp_t client_handle;
    ktc_arena_t *arena;
    bool is_closing;
    int pending_closes_cnt;
} ktc_conn_t;

main oqimi tanish: config o'qiladi, loop yaratiladi, socket bind/listen qilinadi, signallar ulanadi, so'ng uv_run bilan loop aylanadi.

r = uv_listen((uv_stream_t *)&app.server, 128, on_connection);
// ...
uv_run(app.loop, UV_RUN_DEFAULT);

Lifecycle invariantlari

Endi asosiy savollarga o'tamiz. Server "ishlayapti" degani yetarli emas - u qanday ishlayotgani, ayniqsa connection o'lishi va shutdown paytida, muhim.

Savol: connection'ni yopganda xotirani qachon ozod qilamiz?

Bu naqadar oddiy ko'rinsa, o'shancha aldamchi savol. libuv asinxron: uv_close chaqirsak, handle darhol o'lmaydi - loop keyingi iteratsiyada callback orqali "endi o'ldi" deb xabar beradi. Agar biz uv_closedan keyin darhol free(conn) qilsak, loop hali handle'ga tegib turgan bo'lishi mumkin → use-after-free.

Talab. Connection strukturasi faqat unga bog'liq barcha handle'lar uv_closeni tugatgandan keyingina ozod qilinishi kerak. Bu bizning H11-LIFE-006 (bosqichli yopish) invariantimizning quyi qatlamidagi mexanika.

Implementatsiya. Reference counting. pending_closes_cnt har bir ochilgan handle'ni sanaydi, on_handle_closed esa har handle yopilganda uni kamaytiradi va nolga yetganda - va faqat o'shanda - xotirani ozod qiladi.

static void on_handle_closed(uv_handle_t *h) {
    ktc_conn_t *c = h->data;
    if (--c->pending_closes_cnt == 0) {
        if (c->arena) {
            ktc_arena_destroy(c->arena);
        }
        free(c);
    }
}

static void conn_close(ktc_conn_t *c) {
    if (c->is_closing) {
        return;
    }
    c->is_closing = true;

    uv_read_stop((uv_stream_t *)&c->client_handle);

    c->pending_closes_cnt = 1;
    uv_close((uv_handle_t *)&c->client_handle, on_handle_closed);
}

Nega. is_closing flagi idempotentlikni beradi: conn_close bir necha marta chaqirilsa ham (masalan, ham read xatosi, ham write xatosi bir vaqtda), yopish faqat bir marta boshlanadi. Bu double-free'ni oldini oladi. Hozir bitta handle bor, lekin keyin taymer qo'shsak, pending_closes_cnt ni oshirib, xuddi shu skelet ishlayveradi - dizayn oldindan kengaytiriladigan qilib qo'yilgan.

Savol: echo o'zi qanday ishlaydi va buferni kim ozod qiladi?

libuv read'dan oldin bufer so'raydi (on_alloc), keyin o'qilgan baytlarni on_readga beradi. Echo'da biz o'sha buferni to'g'ridan-to'g'ri qaytarib yozamiz.

static void on_alloc(uv_handle_t *handle, size_t suggested, uv_buf_t *buf) {
    (void)handle;
    buf->base = malloc(suggested);
    buf->len = buf->base ? suggested : 0;
}

static void on_read(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) {
    ktc_conn_t *c = stream->data;

    if (nread > 0) {
        ktc_write_req_t *wr = malloc(sizeof(ktc_write_req_t));
        if (!wr) {
            free(buf->base);
            conn_close(c);
            return;
        }
        wr->base = buf->base;

        uv_buf_t wbuf = uv_buf_init(buf->base, (unsigned int)nread);
        int r = uv_write(&wr->req, stream, &wbuf, 1, on_write);
        if (r) {
            free(wr->base);
            free(wr);
            conn_close(c);
        }
    } else {
        if (nread == UV_EOF) {
            log_info("Client disconnected cleanly (EOF)");
        }
        free(buf->base);
        conn_close(c);
    }
}

Nega bufer write callback'gacha yashaydi. uv_write asinxron - u qaytganda yozuv hali tugamagan bo'lishi mumkin. Shu sabab buferni write tugagandan keyin (on_write) ozod qilamiz, aks holda libuv o'chib ketgan xotiradan o'qiydi. Buni ktc_write_req_t ichida base pointerini saqlab hal qildik:

static void on_write(uv_write_t *req, int status) {
    ktc_write_req_t *wr = (ktc_write_req_t *)req;
    if (status < 0) {
        log_error("Write failed: %s", uv_strerror(status));
    }
    free(wr->base);
    free(wr);
}

Savol: server SIGINT/SIGTERM olganda nima bo'ladi? Ochiq connectionlar-chi?

Bu graceful shutdown savoli. Xom exit() - bu vahshiylik: yarim yozilgan responselar, resurs leaklar, TCP RST. Bizga tartibli to'xtash kerak.

Talab. Signal kelganda: (1) yangi connection qabul qilishni to'xtatish, (2) mavjudlarini drain qilish (tugatishga imkon berish), (3) loop toza yopilishi.

static void on_signal(uv_signal_t *handle, int signum) {
    (void)handle;
    if (app.is_shutting_down) {
        return;
    }
    app.is_shutting_down = true;
    log_info("Caught signal %d, stopping server and draining...", signum);

    if (!uv_is_closing((uv_handle_t *)&app.server)) {
        uv_close((uv_handle_t *)&app.server, NULL);
    }

    uv_signal_stop(&app.sig_int);
    uv_close((uv_handle_t *)&app.sig_int, NULL);
    // ... sig_term, sig_hup ham
    uv_stop(app.loop);
}

on_connection boshida esa tekshiruv bor - shutdown paytida yangi socketlar rad etiladi:

static void on_connection(uv_stream_t *server, int status) {
    if (status < 0) { /* ... */ return; }
    if (app.is_shutting_down) {
        return;
    }
    // ... accept
}

Nega ikki bosqichli drain. mainda uv_run tugagach, loopda hali yopilmagan handle'lar qolishi mumkin. Biz uv_walk bilan barchasini yopamiz va yana bir marta loopni aylantiramiz - bu barcha uv_close callbacklariga ishlash imkonini beradi:

uv_run(app.loop, UV_RUN_DEFAULT);

log_info("Cleaning up remaining connections...");
uv_walk(app.loop, close_walk_cb, NULL);
uv_run(app.loop, UV_RUN_DEFAULT);

uv_loop_close(app.loop);

is_shutting_down flagi bu yerda ham idempotentlikni beradi: bir nechta signal ketma-ket kelsa, shutdown mantiqi faqat bir marta bajariladi.

Kelajakka ishora

Diagrammadagi taymerlar hali yo'q. Slowloris kabi hujumlarni (sekin bayt yuborib connectionni ushlab turish) to'xtatish uchun har connectionga read/idle timeout kerak bo'ladi. Skelet tayyor - pending_closes_cnt va conn_close allaqachon ko'p-handle'li hayotni ko'targan.

Request Line Parsing

TCP asos tayyor. Endi kelgan baytlarga ma'no beramiz. HTTP so'rovning birinchi qatori - request-line: GET /index.html HTTP/1.1\r\n. Uch bo'lak: method, target, version, bo'shliq bilan ajratilgan, CRLF bilan tugaydi.

Kod: 4dd4c3d

Bu yerda muhim dizayn qarori: parser incremental va zero-copy bo'ladi. Incremental - chunki TCP baytlarni ixtiyoriy bo'laklarda yetkazadi, biz butun qatorni kutib o'tira olmaymiz. Zero-copy - chunki tokenlarni strndup bilan ko'chirish (cserveda qilingan xato) har so'rovda malloc bosimini keltiradi. O'rniga biz faqat indekslarni eslab qolamiz, keyin ktc_str view'lar sifatida resolve qilamiz.

typedef struct {
    ktc_req_line_state_t state;
    ktc_req_line_err_t error;

    ktc_str method;
    ktc_str target;
    ktc_str version;

    size_t method_start;
    size_t method_len;
    // ... target_start/len, version_start/len
    size_t bytes_consumed;
} ktc_req_line_parser_t;

Parser - sof state machine. Har bir kelgan bayt joriy holatga qarab bir tranzitsiyani yuzaga keltiradi:

IDLE → METHOD → TARGET → VERSION → CRLF → COMPLETE
                                         ↘ ERROR

Grammar va oktet oqimi

Savol: so'rovni matn (string) sifatida o'qiymizmi yoki bayt oqimi sifatida?

Talab (H11-PARSE-001). HTTP xabari - bu oktetlar oqimi (US-ASCII superset), hech qachon Unicode belgilar oqimi emas. Agar biz UTF-8 dekodlashga urinsak, smuggling va noto'g'ri chegaralashga yo'l ochamiz.

Implementatsiya. Parser const uint8_t *data qabul qiladi - char emas, uint8_t. Bu C darajasida "bu baytlar, belgilar emas" degan aniq signal. Har bir bayt raqamli qiymati bo'yicha tekshiriladi.

bool ktc_req_line_parser_feed(ktc_req_line_parser_t *parser, const uint8_t *data, size_t len) {
    // ...
    for (size_t i = 0; i < len; i++) {
        uint8_t c = data[i];
        // ...
    }
}

Savol: request-line'dan oldin bo'sh qatorlar (CRLF) kelsa nima qilamiz?

Talab (H11-PARSE-006). Server request-line'dan oldingi kamida bitta bo'sh qatorni robustlik uchun e'tiborsiz qoldirishi kerak (SHOULD). Ba'zi eski klientlar POST'dan keyin ortiqcha CRLF yuboradi.

Implementatsiya. IDLE holatida \r kelsa SKIP_EMPTYga o'tamiz, u yerda \n kutamiz; xom \n esa IDLEda qoladi.

case KTC_REQ_LINE_STATE_IDLE:
    if (c == '\r') {
        parser->state = KTC_REQ_LINE_STATE_SKIP_EMPTY;
    } else if (c == '\n') {
        // xom LF boshda - IDLE'da qolamiz
    } else if (c == ' ' || c == '\t') {
        // method oldidagi bo'shliq - invalid
        parser->error = KTC_REQ_LINE_ERR_BAD_SYNTAX;
        return false;
    } else {
        if (!is_token_char(c)) { /* 400 */ }
        parser->method_start = current_idx;
        parser->state = KTC_REQ_LINE_STATE_METHOD;
    }
    break;

Nega method oldidagi bo'shliqni rad etamiz. Leading whitespace - bu klassik request smuggling vektori. Agar bizning parser bo'shliqni "kechirsa", lekin downstream boshqacha talqin qilsa, xabar chegaralari ikki xil ko'rinadi. RFC bu yerda qat'iy bo'lishga chaqiradi (H11-PARSE-004/005).

Method va target

Savol: method qanday belgilardan iborat bo'lishi mumkin?

Talab. Method - bu token (RFC 9110). Faqat ma'lum belgilar to'plami ruxsat etilgan: harflar, raqamlar, va !#$%&'*+-.^_\|~`.

Implementatsiya.

static bool is_token_char(uint8_t c) {
    if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) {
        return true;
    }
    const char *specials = "!#$%&'*+-.^_`|~";
    return strchr(specials, c) != NULL;
}

METHOD holatida bo'shliq kelguncha token belgilarni yig'amiz; bo'shliq kelsa uzunlikni yopamiz va uzunlik chegarasini tekshiramiz:

case KTC_REQ_LINE_STATE_METHOD:
    if (c == ' ') {
        parser->method_len = current_idx - parser->method_start;
        if (parser->method_len == 0) { /* 400 */ }
        if (parser->method_len > MAX_METHOD_LEN) {
            parser->error = KTC_REQ_LINE_ERR_METHOD_NOT_IMPLEMENTED;
            return false;
        }
        parser->state = KTC_REQ_LINE_STATE_TARGET;
    } else {
        if (!is_token_char(c)) { /* 400 */ }
    }
    break;

Nega juda uzun method → 501. H11-REQLINE-003: implement qilingan har qanday methoddan uzunroq method kelsa, uni parse qilib o'tirmasdan 501 (Not Implemented) bilan javob beramiz. Bu bufer overflow va DoS'ga qarshi erta himoya.

Savol: request-target'da qaysi belgilar taqiqlangan va uzunlik chegarasi nima?

Talab (H11-REQLINE-002). Target server chegarasidan uzun bo'lsa - 414 (URI Too Long). Bundan tashqari target ichida control belgilari va bo'shliq bo'lmasligi kerak.

Implementatsiya.

case KTC_REQ_LINE_STATE_TARGET:
    if (parser->target_len == 0) {
        if (c == ' ' || c == '\t' || c == '\r' || c == '\n') { /* 400 */ }
        parser->target_start = current_idx;
        parser->target_len = 1;
    } else {
        if (c == ' ') {
            parser->target_len = current_idx - parser->target_start;
            parser->state = KTC_REQ_LINE_STATE_VERSION;
        } else if (c == '\r' || c == '\n' || c < 0x20 || c == 0x7F) {
            // Control belgilar va bo'shliq URI'da taqiqlangan
            parser->error = KTC_REQ_LINE_ERR_BAD_SYNTAX;
            return false;
        } else {
            size_t current_len = current_idx + 1 - parser->target_start;
            if (current_len > MAX_TARGET_LEN) {
                parser->error = KTC_REQ_LINE_ERR_URI_TOO_LONG;
                return false;
            }
            parser->target_len = current_len;
        }
    }
    break;

Nega uzunlikni oqim davomida tekshiramiz. MAX_TARGET_LEN (8192) chegarasini har bir baytda tekshiramiz, oxirida emas. Bu muhim: yovuz klient 10 GB'lik URI yuborsa, biz 8193-baytdayoq uzamiz - butun narsani xotiraga yig'ib o'tirmaymiz.

Yakunlash va resolve

VERSION holati target'ga o'xshaydi, \r kelguncha token yig'ib, CRLFCOMPLETE. Yakunda bytes_consumed request-line tugagan aniq pozitsiyani ko'rsatadi - bu keyingi bosqich (header) qayerdan boshlashini biladi degani.

Tokenlar faqat parse tugagach, bufer manzili bilan resolve qilinadi - mana zero-copy'ning yakuni:

void ktc_req_line_parser_resolve(ktc_req_line_parser_t *parser, const uint8_t *buf_base) {
    if (parser->state == KTC_REQ_LINE_STATE_COMPLETE) {
        parser->method = ktc_str_from(buf_base + parser->method_start, parser->method_len);
        parser->target = ktc_str_from(buf_base + parser->target_start, parser->target_len);
        parser->version = ktc_str_from(buf_base + parser->version_start, parser->version_len);
    } else {
        parser->method = ktc_str_null();
        // ...
    }
}

Hozircha ochiq gap: version validatsiya qilinmayapti

Joriy implementatsiya VERSION tokenini shaklan qabul qiladi, lekin uni HTTP/1.1 ga tenglashtirmaydi. RFC 9112 §2.3 bo'yicha noto'g'ri versiyaga 505 (HTTP Version Not Supported) kerak. Bu keyingi milestone'da yopiladi - ayniqsa HTTP/1.0 vs 1.1 farqi keep-alive semantikasi uchun zarur bo'ladi.

Header Parsing

Request-line'dan keyin header bloki keladi: Name: value\r\n qatorlari, bo'sh \r\n (double CRLF) bilan yakunlanadi. Bu - HTTP'ning eng ko'p smuggling zaifligi to'plangan joyi, shu sabab parser qat'iy bo'lishi shart.

Kod: d0f62e9

Yana state machine, kengroq alfavit bilan:

NAME → VALUE_START → VALUE → CR → CRLF → NAME (keyingi header)
                                    ↘ DOUBLE_CRLF → COMPLETE

Har bir header nomi/qiymati indekslar bilan saqlanadi (zero-copy), va parser umumiy chegaralarni kuzatadi:

#define KTC_MAX_HEADERS 64
#define MAX_HEADERS_SIZE 16384

typedef struct {
    ktc_header_state_t state;
    ktc_header_err_t error;
    ktc_header_t headers[KTC_MAX_HEADERS];
    size_t header_count;
    ktc_str host;
    // internal indices...
    size_t total_headers_size;
} ktc_header_parser_t;

Chegaralar va DoS himoyasi

Savol: klient cheksiz katta header bloki yuborsa?

Talab (H11-HDR-004). Header qatori, qiymati yoki butun header seksiyasi server chegarasidan oshsa - 4xx. Chegarasiz o'sish = OOM = DoS.

Implementatsiya. Har bir baytda ikki hisoblagichni oshiramiz va tekshiramiz - umumiy o'lcham, va header soni:

parser->total_headers_size++;
if (parser->total_headers_size > MAX_HEADERS_SIZE) {
    parser->error = KTC_HEADER_ERR_TOO_LARGE;
    return false;
}
// ... CR holatida yangi header qo'shishdan oldin:
if (parser->header_count >= KTC_MAX_HEADERS) {
    parser->error = KTC_HEADER_ERR_TOO_LARGE;
    return false;
}

mainda bu xato 431 (Request Header Fields Too Large) ga map qilinadi. Nega har baytda tekshiramiz - yana o'sha erta uzish printsipi: 1 MB header kutib o'tirmaymiz, chegarada darrov to'xtatamiz.

Xavfli syntax: bo'shliq, obs-fold, control belgilar

Savol: Host : localhost - nom va ikki nuqta orasidagi bo'shliqchi?

Talab (H11-HDR-001). Header nomi va : orasidagi bo'shliq - 400, istisnosiz. Bu klassik smuggling vektori.

Implementatsiya. NAME holatida : dan oldin bo'shliq/tab/CR/LF kelsa - darhol xato:

case KTC_HEADER_STATE_NAME:
    if (c == ':') {
        if (parser->current_name_len == 0) { /* 400: bo'sh nom */ }
        parser->state = KTC_HEADER_STATE_VALUE_START;
    } else if (c == ' ' || c == '\t' || c == '\r' || c == '\n') {
        // Colon oldidagi bo'shliq taqiqlangan (H11-HDR-001)
        parser->error = KTC_HEADER_ERR_BAD_SYNTAX;
        return false;
    } else {
        if (!is_token_char(c)) { /* 400 */ }
        if (parser->current_name_len == 0) {
            parser->current_name_start = current_idx;
        }
        parser->current_name_len++;
    }
    break;

Savol: qiymatni bir necha qatorga bo'lib yozish (obs-fold) ruxsatmi?

Talab (H11-HDR-002). Obs-fold (qiymatni davom ettirish uchun keyingi qatorni bo'shliq bilan boshlash) - request'da 400 (afzal). Bu deprecated va smuggling xavfli.

Implementatsiya. Header tugagach (CRLF holati), keyingi qator bo'shliq/tab bilan boshlansa - bu obs-fold, rad etamiz:

case KTC_HEADER_STATE_CRLF:
    if (c == '\r') {
        parser->state = KTC_HEADER_STATE_DOUBLE_CRLF;
    } else if (c == ' ' || c == '\t') {
        // Obs-fold taqiqlangan (H11-HDR-002)
        parser->error = KTC_HEADER_ERR_BAD_SYNTAX;
        return false;
    } else {
        // yangi header nomi boshlandi
        parser->current_name_start = current_idx;
        parser->current_name_len = 1;
        parser->state = KTC_HEADER_STATE_NAME;
    }
    break;

Savol: qiymat ichidagi xom CR/LF/NUL?

Talab (H11-HDR-003). Field-value ichida CR, LF yoki NUL - xabarni rad etamiz. Bu belgilar header injection'ga yo'l ochadi.

Implementatsiya. VALUE holatida \r normal yakun; boshqa control belgi (tab bundan mustasno) - xato:

case KTC_HEADER_STATE_VALUE:
    if (c == '\r') {
        parser->state = KTC_HEADER_STATE_CR;
    } else if (c < 0x20 || c == 0x7F) {
        if (c == '\t') {
            parser->current_value_len++;
        } else {
            // Control/NUL/xom LF invalid (H11-HDR-003)
            parser->error = KTC_HEADER_ERR_BAD_SYNTAX;
            return false;
        }
    } else {
        parser->current_value_len++;
    }
    break;

Qiymat resolve paytida esa oldingi/keyingi OWS (optional whitespace) trim qilinadi:

size_t vstart = parser->value_start[i];
size_t vlen = parser->value_len[i];
while (vlen > 0 &&
       (buf_base[vstart + vlen - 1] == ' ' || buf_base[vstart + vlen - 1] == '\t')) {
    vlen--;
}
parser->headers[i].value = ktc_str_from(buf_base + vstart, vlen);

Host invariantlari

Savol: HTTP/1.1 so'rovda Host yo'q, yoki ikkita, yoki noto'g'ri bo'lsa?

Talab (H11-HOST-001). Host yo'q, bir nechta, yoki invalid → 400. HTTP/1.1'da Host majburiy - virtual hosting'ning asosi.

Talab (H11-HOST-002). Agar target absolute-form (http://host/path) bo'lsa - Host header'ni e'tiborsiz qoldiramiz va authorityni target'dan olamiz.

Implementatsiya. Avval absolute-form tekshiramiz; bo'lsa authorityni URI'dan ajratamiz:

bool is_absolute = false;
if (starts_with_case_insensitive(request_target.ptr, request_target.len, "http://", 7) ||
    starts_with_case_insensitive(request_target.ptr, request_target.len, "https://", 8)) {
    is_absolute = true;
}

if (is_absolute) {
    const uint8_t *p = request_target.ptr;
    size_t rem = request_target.len;
    // scheme'ni o'tkazamiz...
    const uint8_t *slash = memchr(p, '/', rem);
    size_t host_len = slash ? (size_t)(slash - p) : rem;
    parser->host = ktc_str_from(p, host_len);
    return true;
}

Aks holda Host header'ini qidiramiz, dublikatni rad etamiz, yo'qligini rad etamiz:

bool host_found = false;
for (size_t i = 0; i < parser->header_count; i++) {
    if (ktc_str_eq_case_insensitive(parser->headers[i].name, ktc_str_from_cstr("Host"))) {
        if (host_found) {
            parser->error = KTC_HEADER_ERR_DUPLICATE_HOST;
            return false;
        }
        host_found = true;
        parser->host = parser->headers[i].value;
    }
}
if (!host_found) {
    parser->error = KTC_HEADER_ERR_MISSING_HOST;
    return false;
}

Nega absolute-form Hostdan ustun. RFC 9112 §3.2.3: agar target authorityni to'liq bersa, Host header (klient yuborgan bo'lsa ham) ishonchsiz - target haqiqat manbai. Bu ikki xil authority talqin qilinib smuggling bo'lishining oldini oladi.

Body Parsing

Oxirgi bosqich - message body. Bu HTTP'ning eng nozik qismi: bu yerda request smuggling hujumlarining aksariyati yashaydi. Sabab - body uzunligini aniqlashning bir necha yo'li bor (Content-Length, Transfer-Encoding: chunked), va ular kelishmovchilikka tushsa, ikki server (masalan proxy va origin) xabar chegarasini boshqacha ko'radi.

Kod: 89fb402

Shu sabab body parsing ikki bosqichga bo'linadi: avval framing'ni aniqlash (qanday o'qiymiz?), keyin body'ni o'qish.

typedef enum {
    KTC_BODY_FRAMING_NONE,
    KTC_BODY_FRAMING_LENGTH,
    KTC_BODY_FRAMING_CHUNKED
} ktc_body_framing_t;

Framing aniqlash: §6.3 pretsedenti

Savol: body uzunligini qanday aniqlaymiz? Va Content-Length bilan Transfer-Encoding ikkalasi ham bo'lsa-chi?

Talab (H11-FRAME-001). RFC 9112 §6.3 pretsedent tartibini aynan amalga oshirish kerak - ad-hoc yorliqlarsiz.

Talab (H11-FRAME-004, H11-SEC-003). Transfer-Encoding va Content-Length ikkalasi bir vaqtda kelsa - bu smuggling belgisi, darhol rad etamiz.

Implementatsiya. Header'lar bo'ylab yuramiz, TE va CL'ni topamiz, dublikat CL'ni ham ushlaymiz:

for (size_t i = 0; i < header_parser->header_count; i++) {
    if (ktc_str_eq_case_insensitive(header_parser->headers[i].name,
                                    ktc_str_from_cstr("Transfer-Encoding"))) {
        has_te = true;
        te_val = header_parser->headers[i].value;
    }
    if (ktc_str_eq_case_insensitive(header_parser->headers[i].name,
                                    ktc_str_from_cstr("Content-Length"))) {
        if (has_cl) {
            return false; // Dublikat Content-Length invalid (H11-FRAME-003)
        }
        has_cl = true;
        cl_val = header_parser->headers[i].value;
    }
}

// TE va CL ikkalasi ham → ambiguity → smuggling'ni oldini olib darhol rad
if (has_te && has_cl) {
    return false;
}

Savol: HEAD so'rovda body bo'lishi mumkinmi?

Talab (H11-FRAME-001, 4-band). HEAD so'rov body ko'tarmaydi. Aniq NONE.

if (ktc_str_eq_case_insensitive(method, ktc_str_from_cstr("HEAD"))) {
    parser->framing = KTC_BODY_FRAMING_NONE;
    return true;
}

Savol: Transfer-Encoding: gzip, chunked - chunked oxirgi bo'lmasa?

Talab (H11-FRAME-002). Request'da TE bor, lekin chunked oxirgi coding emas → 400, close. Faqat oxirgi coding chunked bo'lsagina uzunlik aniqlanadi.

Implementatsiya. Oxirgi comma'dan keyingi tokenni ajratib, trim qilib, chunkedga tenglashtiramiz:

const uint8_t *last_comma = custom_memrchr(te_val.ptr, ',', te_val.len);
ktc_str last_token = te_val;
if (last_comma) {
    size_t offset = (size_t)(last_comma - te_val.ptr);
    last_token = ktc_str_from(last_comma + 1, te_val.len - offset - 1);
}
// last_token'ni bo'shliqdan trim qilamiz...

if (ktc_str_eq_case_insensitive(last_token, ktc_str_from_cstr("chunked"))) {
    parser->framing = KTC_BODY_FRAMING_CHUNKED;
    parser->chunk_parser.state = KTC_CHUNK_STATE_SIZE;
} else {
    return false; // chunked oxirgi emas (H11-FRAME-002)
}

Savol: Content-Length ni qanday parse qilamiz? Overflow-chi?

Talab (H11-FRAME-003). Invalid CL → 400. Bu yerda "invalid" ichiga integer overflow ham kiradi.

Implementatsiya. Qo'lda, raqamma-raqam, har qadamda overflow'ni tekshirib:

if (has_cl) {
    if (cl_val.len == 0) {
        return false;
    }
    size_t cl_len = 0;
    for (size_t i = 0; i < cl_val.len; i++) {
        if (cl_val.ptr[i] < '0' || cl_val.ptr[i] > '9') {
            return false; // Faqat raqamlar (H11-FRAME-003)
        }
        size_t next_val = cl_len * 10 + (size_t)(cl_val.ptr[i] - '0');
        if (next_val < cl_len) {
            return false; // Overflow
        }
        cl_len = next_val;
    }
    parser->framing = KTC_BODY_FRAMING_LENGTH;
    parser->content_length = cl_len;
    return true;
}

Nega strtoul emas, qo'lda. strtoul +, -, oldingi bo'shliq, 0x prefikslarni "kechiradi" - bularning har biri smuggling yuzasi. Qo'lda parse qilib, faqat sof raqamlarni qabul qilamiz va overflowni aniq nazorat qilamiz.

Body o'qish: length va chunked

Length framing sodda - belgilangan miqdorda bayt ko'chiramiz, bufer overflow'dan himoyalanib:

if (parser->framing == KTC_BODY_FRAMING_LENGTH) {
    size_t rem = parser->content_length - parser->body_consumed;
    size_t chunk = len < rem ? len : rem;

    if (*out_len + chunk > max_len) {
        return false; // Output bufer overflow himoyasi
    }
    memcpy(out_buf + *out_len, data, chunk);
    *out_len += chunk;
    parser->body_consumed += chunk;
    return parser->body_consumed < parser->content_length;
}

Chunked esa yana bir state machine:

SIZE → [EXTENSION] → SIZE_CRLF → DATA → DATA_CRLF → SIZE (keyingi chunk)
                                                    ↘ (size 0) TRAILERS → COMPLETE

Savol: ulkan hex chunk-size (FFFF...) yuborilsa?

Talab (H11-CHUNK-003). Katta hex chunk-size'larni oldindan ko'zda tutish; overflow/aniqlik yo'qolishini oldini olish.

Implementatsiya. Har hex raqamdan oldin siljish overflow'ini tekshiramiz:

case KTC_CHUNK_STATE_SIZE:
    if (/* hex digit */) {
        // ... digit ni hisoblaymiz
        if (cp->chunk_size > (SIZE_MAX >> 4)) {
            cp->state = KTC_CHUNK_STATE_ERROR;
            return false;
        }
        cp->chunk_size = (cp->chunk_size << 4) | digit;
    } else if (c == ';') {
        cp->state = KTC_CHUNK_STATE_EXTENSION;
    } else if (c == '\r') {
        cp->state = KTC_CHUNK_STATE_SIZE_CRLF;
    } else {
        cp->state = KTC_CHUNK_STATE_ERROR;
        return false;
    }
    break;

Savol: chunk extension'lar (;name=value)?

Talab (H11-CHUNK-002). Tanilmagan chunk extension'larni e'tiborsiz qoldiramiz (skip).

case KTC_CHUNK_STATE_EXTENSION:
    if (c == '\r') {
        cp->state = KTC_CHUNK_STATE_SIZE_CRLF;
    } else if (c == '\n') {
        cp->state = KTC_CHUNK_STATE_ERROR; // CRsiz LF invalid
        return false;
    }
    // aks holda extension baytini jimgina o'tkazamiz (H11-CHUNK-002)
    break;

Chunk data'ni bayt-bayt out buferga yozamiz, chunk_remaining nolga yetganda DATA_CRLFga o'tamiz, va 0-o'lchamli chunk kelganda TRAILERSCOMPLETE.

Hozircha ochiq gap: trailer parsing va body limit

Ikki nuqta keyingi milestone'ga qoldi. Birinchisi - chunked trailer parsing hozir faqat trailersiz holatni (0\r\n\r\n) to'g'ri ushlaydi; haqiqiy trailer field'lari bilan erta yakunlanadi. Ikkinchisi - Content-Length yoki chunked body uchun umumiy o'lcham chegarasi yo'q (413 kerak). Bularni RFC bo'yicha to'liq yopganimizda alohida yoritamiz.


Shu bilan birinchi qism yakunlandi. Bizda endi: libuv event loop ustida to'g'ri lifecycle boshqaruvi, va uch bosqichli - request-line, header, body - zero-copy, incremental, smuggling-aware parser zanjiri bor. Har bir qadam RFC invariantiga bog'landi, checklistga emas.

Keyingi postda: persistent connection (keep-alive), pipelining, va timerlar. Aynan diagrammada ko'rgan Read/Write fazalarini ajratish shu yerda meva beradi.


Invariants

Invariantlar to'liq ro'yxatini serverimiz official reposida ko'rishingiz mumkin: INVARIANTS.

Global Lifecycle Rules

ID Level Invariant RFC
H11-LIFE-001 MUST A connection carries ordered request/response pairs; responses on a connection MUST correspond to requests in the same order received (pipelining). 9112 §9.2, 9112 §9.3.2
H11-LIFE-002 MUST Do not apply a request to a resource until the entire request header section has been received. 9110 §5.3
H11-LIFE-003 MUST On a persistent connection, after sending a response, either read the entire request body of the current request or close the connection. 9112 §9.3
H11-LIFE-004 MUST Every message on a persistent connection MUST have a self-defined length (not connection-close delimited), except allowed cases in §6.3. 9112 §9.3
H11-LIFE-005 MUST NOT After sending or honoring Connection: close, process no further requests on that connection. 9112 §9.6
H11-LIFE-006 SHOULD Close connections in stages (half-close write, drain read) to avoid TCP RST wiping the final response. 9112 §9.6

Message Parsing and Grammar

Octet Stream and Line Endings

ID Level Invariant RFC
H11-PARSE-001 MUST Parse messages as a sequence of octets in a superset of US-ASCII - never as a Unicode character stream. 9112 §2.2
H11-PARSE-002 MUST NOT Generate bare CR (CR not followed by LF) in protocol elements except message content. 9112 §2.2
H11-PARSE-003 MUST Treat received bare CR as invalid or replace each with SP before processing. 9112 §2.2
H11-PARSE-004 MUST NOT Send whitespace between start-line and first header field. 9112 §2.2
H11-PARSE-005 MUST On whitespace between start-line and first header: reject or consume/ignore lines per §2.2 (smuggling mitigation). 9112 §2.2
H11-PARSE-006 SHOULD Ignore at least one empty line (CRLF) before the request-line. 9112 §2.2
H11-PARSE-007 SHOULD On grammar violation (outside robustness exceptions): respond 400 and close connection. 9112 §2.2

Request Line

ID Level Invariant RFC
H11-REQLINE-001 MUST Accept origin-form, absolute-form, authority-form (CONNECT), and asterisk-form (OPTIONS *). 9112 §3.2
H11-REQLINE-002 MUST Respond 414 (URI Too Long) if request-target exceeds server limit. 9112 §3.2, 9110 §15.5.15
H11-REQLINE-003 SHOULD Respond 501 (Not Implemented) for method longer than any implemented method. 9112 §3.2
H11-REQLINE-004 SHOULD On invalid request-line: 400 or 301 with properly encoded target - do not silently autocorrect. 9112 §3.2
H11-REQLINE-005 MAY Lenient whitespace parsing in request-line - discouraged (smuggling risk if inconsistent with other parsers). 9112 §3.2

Status-line (responses the server sends)

ID Level Invariant RFC
H11-STATUS-001 MUST Send the SP between status-code and reason-phrase even when reason-phrase is empty. 9112 §4
H11-STATUS-002 MUST NOT Send an HTTP version in responses that the server is not conformant with. 9110 §6.2
H11-STATUS-003 SHOULD Response version = highest conformant version with major ≤ request major. 9110 §6.2
H11-STATUS-004 MAY Send 505 (HTTP Version Not Supported) to refuse client major version. 9110 §6.2
H11-STATUS-005 MUST NOT Send 1xx response to an HTTP/1.0 client. 9110 §15.2

Host and Request Target

ID Level Invariant RFC
H11-HOST-001 MUST Respond 400 to HTTP/1.1 request without Host, with multiple Host field lines, or with invalid Host value. 9112 §3.2, 9110 §7.2
H11-HOST-002 MUST On absolute-form request-target: ignore received Host; use authority from request-target. 9112 §3.2.3
H11-HOST-003 MUST Accept absolute-form even from direct clients (not only proxies). 9112 §3.2.3
H11-HOST-004 MUST Reject CONNECT with empty/invalid port → 400. 9110 §9.3.6
H11-HOST-005 MUST Reject https scheme requirements if not received over valid TLS for that origin (unless trusted gateway). 9110 §4.3.3
H11-HOST-006 MAY For empty authority on http/https URI: reject or apply configured default consistent with connection context. 9112 §3.2.3

Headers

Syntax and Dangerous Fields

ID Level Invariant RFC
H11-HDR-001 MUST Reject 400 any request with whitespace between header field name and :. 9112 §5.2
H11-HDR-002 MUST On obs-fold in request (outside message/http): 400 (preferred) or replace fold with SP before interpreting. 9112 §5.2
H11-HDR-003 MUST On CR, LF, or NUL in field value: reject message or replace with SP. 9110 §5.5
H11-HDR-004 MUST Respond 4xx when header line, value, or entire header section exceeds server limits. 9110 §5.4
H11-HDR-005 MUST NOT Apply request until full header section received. 9110 §5.3
H11-HDR-006 SHOULD Ignore unrecognized header/trailer fields (unless field definition says otherwise). 9110 §5.1

Response header obligations

ID Level Invariant RFC
H11-HDR-007 MUST Origin server with clock: generate Date on all 2xx, 3xx, 4xx responses. 9110 §6.6.1
H11-HDR-008 MAY Generate Date on 1xx and 5xx. 9110 §6.6.1
H11-HDR-009 MUST Generate Allow on 405 (Method Not Allowed). 9110 §10.2.1
H11-HDR-010 MUST Generate WWW-Authenticate on 401 (≥1 challenge). 9110 §11.6.1

Message Body Framing

Body length is determined by RFC 9112 §6.3 precedence (implement exactly this order):

  1. Transfer-Encoding: chunked (final coding) → decode chunks until zero chunk + trailers.
  2. Transfer-Encoding present in response, chunked not final → read until connection close.
  3. Transfer-Encoding in request, chunked not final → 400, close.
  4. HEAD response, or 1xx / 204 / 304 → no body; terminated by empty line after headers.
  5. 2xx CONNECT → tunnel after header empty line; ignore TE/CL in that response.
  6. Both Transfer-Encoding and Content-Length → TE wins; treat as error (smuggling); server MUST close after response.
  7. Invalid Content-Length (unrecoverable) → 400, close (request).
  8. Valid Content-Length without TE → that many octets; incomplete → close.
  9. Request, none of above → body length 0.
  10. Response, none of above → body until connection close.
ID Level Invariant RFC
H11-FRAME-001 MUST Implement §6.3 precedence exactly - no ad-hoc shortcuts. 9112 §6.3
H11-FRAME-002 MUST On request TE present, chunked not final: 400, close. 9112 §6.3
H11-FRAME-003 MUST On invalid CL in request: 400, close (unless comma-list duplicate same values). 9112 §6.3
H11-FRAME-004 MUST On both TE and CL in request: process per TE or reject; always close after response. 9112 §6.3
H11-FRAME-005 MUST On HTTP/1.0 message with Transfer-Encoding: treat framing faulty, close after processing. 9112 §6.2
H11-FRAME-006 MUST NOT Send Content-Length in message that has Transfer-Encoding. 9112 §6.2
H11-FRAME-007 SHOULD Prefer length-delimited or chunked responses over close-delimited. 9112 §6.3
H11-FRAME-008 MAY Reject request body without Content-Length (non-chunked) with 411. 9112 §6.3

Responses Without Body

ID Level Invariant RFC
H11-FRAME-009 MUST NOT Send Content-Length on 1xx or 204 responses. 9110 §8.6, 9112 §6.1
H11-FRAME-010 MUST NOT Send Transfer-Encoding on 1xx or 204 responses. 9112 §6.1
H11-FRAME-011 MUST NOT Send Content-Length or Transfer-Encoding on 2xx CONNECT response. 9110 §9.3.6, 9112 §6.1
H11-FRAME-012 MUST NOT Send content in HEAD response. 9110 §9.3.2
H11-FRAME-013 MUST NOT Generate content in 205 response. 9110 §15.3.6

Chunked Transfer Encoding

ID Level Invariant RFC
H11-CHUNK-001 MUST Be able to parse and decode chunked transfer coding. 9112 §7.1
H11-CHUNK-002 MUST Ignore unrecognized chunk extensions. 9112 §7.1.1
H11-CHUNK-003 MUST Anticipate large hex chunk-size values; prevent overflow/precision loss. 9112 §7.1
H11-CHUNK-004 SHOULD Treat chunk extension parameters as error. 9112 §7.1
H11-CHUNK-005 ought Limit total chunk-extension length; 4xx if exceeded. 9112 §7.1.1
H11-CHUNK-006 MUST NOT Send Transfer-Encoding on response unless request indicates HTTP/1.1+. 9112 §6.1
H11-CHUNK-007 SHOULD Respond 501 to transfer coding not understood. 9112 §6.1