namalzi

← 글 목록

Apache httpd 소스 파헤치기 9: 연결과 요청 읽기 — 소켓 하나가 request_rec가 되기까지

Apache httpd · 게시

MPM이 받아 넘긴 소켓 하나가 conn_rec가 되고, 요청 줄과 헤더를 읽어 request_rec가 되기까지를 httpd 소스로 따라갑니다. 코어가 훅의 맨 끝에 서는 이유, keep-alive 루프가 MPM에 따라 어디에 있는지, trunk에서 요청 줄을 읽는 일이 필터로 옮겨 간 것, 그리고 Host 헤더로 가상 호스트를 고르는 두 단계를 예제와 함께 확인합니다.

이 글에서 다루는 것

"Apache httpd 소스 파헤치기" 연재의 아홉 번째 글입니다. 8편에서는 MPM이 프로세스와 스레드를 어떻게 꾸리고 접속을 받아(accept) 일꾼에게 나눠 주는지 봤습니다. 이번 글은 그다음, 일꾼이 소켓 하나를 손에 쥔 순간부터 시작합니다.

답하려는 질문은 네 가지입니다.

항목 내용
대상 Apache httpd
버전 2.5.1-dev (trunk)
커밋 e4af30fe6d (2026-10-24)
주로 읽는 파일 server/connection.c, server/core.c, modules/http/http_core.c, server/protocol.c, modules/http/http_filters.c, server/vhost.c

글에 나오는 줄 번호와 세부 동작은 모두 이 커밋 기준입니다. 같은 커밋의 소스를 받고 예제 환경을 만드는 방법은 1편에 있습니다. 이 글은 처음 소스를 여는 분이 따라올 수 있게 큰 줄기만 골랐고, 빼놓은 것은 글 끝의 "이 글에서 뺀 것"에 이름만 적어 두었습니다.

먼저 알아 둘 말

conn_rec와 request_rec. httpd는 TCP 연결 하나를 conn_rec 구조체 하나로, 그 연결로 들어온 HTTP 요청 하나를 request_rec 하나로 나타냅니다. 연결 하나에 요청이 여러 개 있을 수 있으므로 conn_rec 하나에 request_rec가 여럿 차례로 붙습니다. 두 구조체의 전체 모양은 3편에서 봤습니다. 이 글은 request_rec의 칸이 언제, 누구에 의해 채워지는지를 봅니다.

풀(pool). httpd는 메모리를 하나씩 free하지 않고 "풀"에 몰아 받았다가 풀째 버립니다. 연결에는 연결 풀이, 요청에는 그 아래 자식인 요청 풀이 있어서 요청이 끝나면 요청 풀만, 연결이 끝나면 연결 풀을 통째로 버립니다. 자세한 것은 4편에 있습니다.

훅과 RUN_FIRST / RUN_ALL. 훅은 "이 시점이 되면 불러 달라"고 모듈들이 함수를 걸어 두는 자리입니다. RUN_FIRST 훅은 걸린 함수를 차례로 부르다가 누군가 DECLINED가 아닌 값을 돌려주면 거기서 멈추고, RUN_ALL 훅은 OK나 DECLINED가 나오는 동안 끝까지 부릅니다(그 밖의 값이 나오면 멈춥니다). 순서는 APR_HOOK_FIRST·MIDDLE·LAST·REALLY_LAST 같은 값으로 정합니다. 6편에서 매크로까지 풀어 봤습니다.

요청 줄과 헤더. HTTP/1.x 요청은 글자로 된 줄들입니다. 첫 줄이 요청 줄(메서드, 주소, 프로토콜 버전)이고, 그 아래로 이름: 값 꼴의 헤더가 이어지다가 빈 줄 하나로 끝납니다. 줄의 끝은 \r\n(CR LF) 두 글자입니다.

GET /index.txt?x=1 HTTP/1.1
Host: b.example
User-Agent: raw
Connection: close

keep-alive. 응답 하나를 보낸 뒤 연결을 끊지 않고 같은 연결로 다음 요청을 받는 것입니다. HTTP/1.1에서는 기본이 keep-alive이고, httpd에서는 KeepAlive·KeepAliveTimeout·MaxKeepAliveRequests 디렉티브로 조절합니다.

이름 기반 가상 호스트. IP와 포트가 같은 <VirtualHost> 여러 개를 두고, 요청의 Host 헤더에 적힌 이름으로 어느 사이트인지 고르는 방식입니다. 한 서버에서 여러 도메인을 운영할 때 흔히 씁니다.

필터. 연결로 들고 나는 데이터는 "필터"라는 함수의 줄을 지나갑니다. 소켓에서 바이트를 읽는 맨 아래 필터가 CORE_IN, 소켓에 쓰는 맨 아래 필터가 CORE입니다. 이 글에서는 "어느 필터가 언제 달리는가"만 보고, 필터와 버킷이 실제로 어떻게 움직이는지는 11편에서 봅니다.

1. MPM이 소켓을 넘기는 자리

연결 하나가 지나는 길. MPM이 접속을 받으면 create_connection 훅이 conn_rec와 연결 풀을 만들고, ap_update_vhost_given_ip가 받은 IP와 포트로 기본 가상 호스트를 정한다. 이어서 pre_connection 훅에서 모듈들이 먼저, 코어가 맨 끝에 불려 소켓 옵션과 CORE_IN, CORE 필터를 단다. process_connection 훅에서 HTTP/1.x는 http 모듈이 맡아 ap_read_request로 요청 줄과 헤더를 읽어 request_rec를 만든다. 요청을 처리하고 응답한 뒤 keep-alive면 다음 요청을 읽으러 돌아간다. 이 글이 따라가는 길입니다. 노란색은 구조체(와 그 풀)가 만들어지는 곳, 파란색은 모듈이 끼어드는 훅입니다.

8편에서 본 것처럼 접속을 받는 방식은 MPM마다 다릅니다. 그러나 받은 소켓을 넘기는 곳은 거의 같은 모양입니다. 가장 단순한 prefork를 보겠습니다.

        current_conn = ap_run_create_connection(ptrans, ap_server_conf, csd, my_child_num, sbh, bucket_alloc);
        if (current_conn) {
#if APR_HAS_THREADS
            current_conn->current_thread = thd;
#endif
            ap_process_connection(current_conn, csd);
            ap_lingering_close(current_conn);
        }

server/mpm/prefork/prefork.c 688–695행

세 줄이 전부입니다. create_connection 훅으로 conn_rec를 만들고, ap_process_connection()에 넘기고, 돌아오면 연결을 닫습니다. #if APR_HAS_THREADS … #endif는 C의 전처리기 조건문으로, APR이 스레드를 지원하도록 빌드됐을 때만 가운데 줄을 컴파일하라는 뜻입니다(2편). ptrans는 이 연결 하나를 위한 풀이고, csd는 받은 소켓입니다. worker MPM도 같은 세 단계를 거칩니다(server/mpm/worker/worker.c 492–498행).

ap_process_connection()은 짧습니다.

AP_CORE_DECLARE(void) ap_process_connection(conn_rec *c, void *csd)
{
    ap_update_vhost_given_ip(c);

    ap_pre_connection(c, csd);

    if (!c->aborted) {
        ap_run_process_connection(c);
    }
}

server/connection.c 244–253행

  1. ap_update_vhost_given_ip() — 연결이 들어온 IP·포트로 "일단 이 가상 호스트"를 정합니다. 6절에서 봅니다.
  2. ap_pre_connection() — pre_connection 훅을 돌립니다. 연결에 필요한 준비(소켓 옵션, 필터)를 합니다. 2절에서 봅니다.
  3. ap_run_process_connection() — 이 연결에서 실제로 프로토콜을 주고받는 일을 맡깁니다. 3절에서 봅니다.

AP_CORE_DECLARE(void)는 반환형 void를 감싼 매크로로, 플랫폼마다 다른 "밖으로 내보내는 함수" 표시를 붙여 줍니다. 읽을 때는 void로 보면 됩니다.

헷갈리기 쉬운 곳: event MPM은 ap_process_connection()을 부르지 않습니다

grep으로 ap_process_connection(을 찾으면 prefork·worker에는 있는데 기본 MPM인 event에는 없습니다. event는 같은 세 단계를 자기 함수 process_socket() 안에 풀어 적었습니다. ap_run_create_connection()(event.c 1075행) → ap_update_vhost_given_ip()(1099행) → ap_pre_connection()(1101행) → ap_run_process_connection()(1153행) 순서입니다. 풀어 쓴 이유는 event가 한 연결을 여러 번 나눠서 처리하기 때문입니다. 처음 온 연결만 앞의 세 단계를 거치고, keep-alive로 다시 돌아온 연결은 곧바로 process_connection부터 시작합니다. 3절의 예제에서 눈으로 확인합니다.

2. conn_rec 만들기와 pre_connection — 코어가 맨 끝에 서는 이유

연결에 관한 훅 세 개는 server/connection.c 머리에서 만들어집니다.

AP_IMPLEMENT_HOOK_RUN_FIRST(conn_rec *,create_connection,
                            (apr_pool_t *p, server_rec *server, apr_socket_t *csd, long conn_id, void *sbh, apr_bucket_alloc_t *alloc),
                            (p, server, csd, conn_id, sbh, alloc), NULL)
AP_IMPLEMENT_HOOK_RUN_FIRST(int,process_connection,(conn_rec *c),(c),DECLINED)
AP_IMPLEMENT_HOOK_RUN_ALL(int,pre_connection,(conn_rec *c, void *csd),(c, csd),OK,DECLINED)

server/connection.c 40–44행

훅 실행 방식 뜻
create_connection RUN_FIRST (NULL이 아닌 값이 나오면 멈춤) conn_rec를 하나 만들어 돌려줄 모듈 하나를 찾는다
pre_connection RUN_ALL 관심 있는 모듈이 모두 연결을 준비한다
process_connection RUN_FIRST (DECLINED가 아니면 멈춤) 이 연결의 프로토콜을 맡을 모듈 하나를 찾는다

코어는 이 중 앞의 둘에 자기 함수를 겁니다. 등록하는 곳에 이유가 주석으로 적혀 있습니다.

    /* create_connection and pre_connection should always be hooked
     * APR_HOOK_REALLY_LAST by core to give other modules the opportunity
     * to install alternate network transports and stop other functions
     * from being run.
     */
    ap_hook_create_connection(core_create_conn, NULL, NULL,
                              APR_HOOK_REALLY_LAST);
    ap_hook_create_secondary_connection(core_create_secondary_conn, NULL, NULL,
                                        APR_HOOK_REALLY_LAST);
    ap_hook_pre_connection(core_pre_connection, NULL, NULL,
                           APR_HOOK_REALLY_LAST);

server/core.c 6148–6158행

"다른 모듈이 다른 네트워크 전송 방식을 끼워 넣고 뒤의 함수가 돌지 않게 막을 기회를 주려고" 코어는 맨 끝(REALLY_LAST) 에 섭니다. 소켓을 평범한 TCP가 아닌 다른 방식으로 다루고 싶은 모듈이 있으면 코어보다 먼저 불려서 자기 conn_rec를 돌려주면 됩니다(RUN_FIRST이므로 거기서 멈춥니다). 아무도 나서지 않으면 마지막에 코어가 기본 동작을 합니다.

core_create_conn — conn_rec에 무엇이 들어가나

core_create_conn()(server/core.c 5510–5597행)이 하는 일을 순서대로 적으면 이렇습니다.

  1. MPM이 건넨 풀(ptrans)에 conn_rec를 0으로 채워 만들고, 그 풀 아래에 연결 풀("master_conn"이라는 이름표가 붙습니다)을 따로 만들어 c->pool에 둡니다.
  2. 모듈별 연결 설정을 담는 c->conn_config, 메모용 표 c->notes 같은 그릇을 만듭니다.
  3. 소켓에서 서버 쪽 주소(c->local_addr, c->local_ip)와 클라이언트 주소(c->client_addr, c->client_ip)를 꺼내 적습니다.
  4. c->base_server에 주 서버를, c->id에 MPM이 건넨 번호를 적습니다.

여기에는 아직 소켓을 conn_rec에 매다는 코드도, 필터를 다는 코드도 없습니다. 그것은 다음 훅의 일입니다.

core_pre_connection — 소켓을 매달고 맨 아래 필터를 단다

static int core_pre_connection(conn_rec *c, void *csd)
{
    conn_config_t *conn_config;
    apr_status_t rv;

    /* only the master connection talks to the network */
    if (c->master) {
        return DONE;
    }

    /* The Nagle algorithm says that we should delay sending partial
     * packets in hopes of getting more data.  We don't want to do
     * this; we are not telnet.  There are bad interactions between
     * persistent connections and Nagle's algorithm that have very severe
     * performance penalties.  (Failing to disable Nagle is not much of a
     * problem with simple HTTP.)
     */
    rv = apr_socket_opt_set(csd, APR_TCP_NODELAY, 1);
    /* ... */
    rv = apr_socket_timeout_set(csd, c->base_server->timeout);
    /* ... */
    conn_config = apr_pcalloc(c->pool, sizeof(*conn_config));
    conn_config->socket = csd;
    ap_set_core_module_config(c->conn_config, conn_config);

    ap_add_input_filter_handle(ap_core_input_filter_handle, NULL, NULL, c);
    ap_add_output_filter_handle(ap_core_output_filter_handle, NULL, NULL, c);

    return DONE;
}

server/core.c 5647–5694행

맨 끝의 return DONE도 눈여겨볼 만합니다. pre_connection은 RUN_ALL이라 OK·DECLINED가 아닌 값이 나오면 거기서 멈추는데, 코어는 어차피 맨 끝이라 멈춰도 잃을 것이 없습니다. 이 결과를 받는 ap_pre_connection()은 OK와 DONE을 둘 다 성공으로 봅니다(server/core.c 5696–5718행). 앞의 모듈이 오류를 돌려주면 연결을 aborted로 표시하고, 그 때문에 코어가 불리지 않아 소켓이 아직 매달리지 않았으면 core_pre_connection()을 직접 한 번 불러 뒷정리가 가능하게 해 둡니다.

예제: 필터는 pre_connection의 맨 끝에서 달린다

훅마다 로그를 찍는 작은 모듈로 확인해 봅니다. pre_connection에는 APR_HOOK_MIDDLE로(코어보다 먼저), process_connection에는 APR_HOOK_FIRST로 걸고, 그때 연결에 달린 필터 이름을 찍습니다. 아래는 ex/u09-read-request/mod_u09_trace.c에서 두 함수만 옮긴 것입니다.

/* 이 글을 위해 쓴 예제입니다 (httpd 의 소스가 아닙니다) */
/* 연결마다 한 번: 코어(REALLY_LAST)보다 먼저 불린다 */
static int trace_pre_connection(conn_rec *c, void *csd)
{
    ap_log_cerror(APLOG_MARK, APLOG_NOTICE, 0, c,
                  "u09 pre_connection: id=%ld client_port=%d base_server=%s "
                  "in=[%s] out=[%s]",
                  c->id, c->client_addr->port, c->base_server->server_hostname,
                  chain(c->pool, c->input_filters),
                  chain(c->pool, c->output_filters));
    return OK;                      /* RUN_ALL: 다음 모듈로 넘긴다 */
}

/* process_connection 은 RUN_FIRST: DECLINED 를 돌려 http 모듈에 맡긴다 */
static int trace_process_connection(conn_rec *c)
{
    ap_log_cerror(APLOG_MARK, APLOG_NOTICE, 0, c,
                  "u09 process_connection: id=%ld client_port=%d keepalives=%d "
                  "in=[%s] out=[%s]",
                  c->id, c->client_addr->port, c->keepalives,
                  chain(c->pool, c->input_filters),
                  chain(c->pool, c->output_filters));
    return DECLINED;
}

chain()은 필터 줄을 따라가며 이름을 " > "로 이어 붙이는 작은 도우미 함수입니다(같은 파일에 있습니다). 이 예제는 기준 커밋(e4af30fe6d)을 그대로 빌드한 httpd 2.5.1-dev로 도커 컨테이너(Ubuntu 24.04)에서 실행했습니다. 요청 하나를 보냈을 때 오류 로그의 앞 두 줄입니다(id와 client_port는 실행마다 다릅니다).

[u09_trace:notice] u09 pre_connection: id=128 client_port=41194 base_server=a.example in=[(none)] out=[(none)]
[u09_trace:notice] u09 process_connection: id=128 client_port=41194 keepalives=0 in=[core_in] out=[core]

우리 모듈의 pre_connection이 불릴 때는 필터가 하나도 없고((none)), 바로 다음 훅인 process_connection에서는 core_in과 core가 달려 있습니다. 그 사이에 불린 것은 맨 끝의 core_pre_connection()뿐입니다.

연결 훅 세 개가 불리는 순서. create_connection 은 RUN_FIRST 라서 다른 전송 방식을 쓰는 모듈이 있으면 먼저 자기 conn_rec 를 돌려주고, 아무도 없으면 REALLY_LAST 인 core_create_conn 이 conn_rec 와 연결 풀 master_conn 을 만들고 주소, base_server, id 를 적는다. pre_connection 은 RUN_ALL 이라 MIDDLE 로 건 예제 모듈 u09 가 먼저 불리고 이때 필터는 in=[(none)] out=[(none)] 이다. 그다음 REALLY_LAST 인 core_pre_connection 이 Nagle 을 끄고 Timeout 을 걸고 소켓을 보관하고 CORE_IN 과 CORE 필터를 단 뒤 DONE 을 돌려준다. process_connection 은 RUN_FIRST 라서 FIRST 로 건 u09 가 in=[core_in] out=[core] 를 보고 DECLINED 를 돌려주면 http 모듈이 HTTP/1.x 로 처리한다 노란 칸이 코어, 파란 칸이 예제 모듈입니다. 필터 목록은 위 오류 로그 두 줄 그대로입니다.

헷갈리기 쉬운 곳

3. process_connection — keep-alive 루프는 어디에 있나

process_connection 훅에 HTTP/1.x를 맡기는 함수를 거는 것은 http 모듈입니다.

    ap_hook_process_connection(ap_process_http_connection, NULL, NULL,
                               APR_HOOK_REALLY_LAST);

modules/http/http_core.c 358–359행

역시 REALLY_LAST입니다. 다른 프로토콜을 다루는 모듈이 먼저 나서서 연결을 가져갈 수 있게 자리를 비워 둔 것입니다. 실제로 HTTP/2의 mod_http2(modules/http2/h2_c1.c 346행)와 받은 바이트를 그대로 돌려보내는 mod_echo(modules/echo/mod_echo.c 206행)도 이 훅에 함수를 겁니다. ap_process_http_connection()은 MPM에 따라 두 갈래로 나뉩니다.

static int ap_process_http_connection(conn_rec *c)
{
    if (async_mpm && !c->clogging_input_filters) {
        return ap_process_http_async_connection(c);
    }
    else {
        return ap_process_http_sync_connection(c);
    }
}

modules/http/http_core.c 247–255행

async_mpm은 서버가 켜질 때 ap_mpm_query(AP_MPMQ_IS_ASYNC, …)로 MPM에게 물어 정해 둔 값입니다(346행). event MPM은 여기에 1을 답합니다(server/mpm/event/event.c 697–698행). clogging_input_filters는 "입력을 막는(clog) 필터가 있어 비동기로 다루기 곤란한 연결"이라는 표시입니다.

동기: prefork·worker는 이 함수 안에서 돈다

static int ap_process_http_sync_connection(conn_rec *c)
{
    /* ... */
    ap_update_child_status_from_conn(c->sbh, SERVER_BUSY_READ, c);
    while ((r = ap_read_request(c)) != NULL) {
        /* ... */
        if (r->status == HTTP_OK) {
            if (cs)
                cs->state = CONN_STATE_HANDLER;
            ap_update_child_status(c->sbh, SERVER_BUSY_WRITE, r);
            ap_process_request(r);
            /* ... */
            r = NULL;
        }

        if (c->keepalive != AP_CONN_KEEPALIVE || c->aborted)
            break;

        ap_update_child_status(c->sbh, SERVER_BUSY_KEEPALIVE, NULL);
        /* ... */
        apr_socket_opt_set(csd, APR_INCOMPLETE_READ, 1);
        apr_socket_timeout_set(csd, keep_alive_timeout);
        /* Go straight to select() to wait for the next request */
    }

    return OK;
}

modules/http/http_core.c 184–245행

이것이 우리가 머릿속에 그리는 keep-alive 그대로입니다. 요청을 읽고(ap_read_request), 처리하고(ap_process_request, 10편), 연결을 유지하기로 했으면(c->keepalive == AP_CONN_KEEPALIVE) 소켓 타임아웃을 KeepAliveTimeout으로 바꾸고 다음 요청을 기다립니다. 그동안 이 프로세스(또는 스레드)는 다음 요청이 올 때까지 이 연결에 묶여 있습니다. 연결을 유지할지는 응답을 만들 때 ap_h1_set_keepalive()가 정합니다(modules/http/http_protocol.c 217–332행). MaxKeepAliveRequests에 닿았는지, 클라이언트가 Connection: close를 보냈는지 같은 조건이 거기 모여 있습니다.

비동기: event는 요청 하나만 읽고 돌아간다

static int ap_process_http_async_connection(conn_rec *c)
{
    /* ... */
    if (cs->state == CONN_STATE_PROCESSING) {
        ap_update_child_status_from_conn(c->sbh, SERVER_BUSY_READ, c);
        /* ... */
        if ((r = ap_read_request(c))) {
            if (r->status == HTTP_OK) {
                cs->state = CONN_STATE_HANDLER;
                /* ... */
                ap_update_child_status(c->sbh, SERVER_BUSY_WRITE, r);
                ap_process_async_request(r);
                /* ... */
                r = NULL;
            }

            if (cs->state != CONN_STATE_WRITE_COMPLETION &&
                cs->state != CONN_STATE_SUSPENDED) {
                /* Something went wrong; close the connection */
                cs->state = CONN_STATE_LINGER;
            }
        }
        else {   /* ap_read_request failed - client may have closed */
            cs->state = CONN_STATE_LINGER;
        }
    }

    return OK;
}

modules/http/http_core.c 139–182행

while가 없습니다. 요청 하나를 읽어 처리하고, 연결의 상태(cs->state)만 적어 두고 돌아갑니다. 요청 처리가 무사히 끝나면 ap_process_request_after_handler()가 상태를 CONN_STATE_WRITE_COMPLETION으로 바꿔 두고(modules/http/http_request.c 397행), event MPM은 이 상태를 보고 남은 응답을 마저 보낸 뒤 소켓을 지켜보는 목록에 넣고 스레드를 돌려받습니다(server/mpm/event/event.c 1161–1163행의 주석). 다음 요청의 데이터가 도착하면 그때 아무 스레드나 이 연결을 집어 process_connection 훅을 다시 부릅니다. keep-alive의 반복이 http 모듈이 아니라 MPM 쪽에 있는 셈입니다. 소켓을 지켜보는 쪽의 이야기는 8편에 있습니다.

동기와 비동기의 비교. prefork와 worker에서는 process_connection이 한 번 불리고 그 안의 while 루프가 ap_read_request와 ap_process_request를 keep-alive인 동안 되풀이하다가 연결을 닫는다. event에서는 process_connection이 요청 하나만 읽고 처리한 뒤 상태를 WRITE_COMPLETION으로 바꾸고 스레드를 돌려주며, MPM이 소켓을 지켜보다 다음 데이터가 오면 process_connection을 다시 부른다. keep-alive의 반복이 오른쪽은 http 모듈의 while 안에, 왼쪽은 MPM에 있습니다. 초록색은 스레드가 연결을 놓아 주는 구간입니다.

예제: 같은 연결에 요청 두 개 — event와 prefork

curl에 주소를 두 개 주면 연결 하나로 두 요청을 차례로 보냅니다. 접근 로그에는 클라이언트 포트(%{remote}p)와 이 연결에서 몇 번째 keep-alive 요청인지(%k)를 찍었습니다. 같은 포트면 같은 TCP 연결입니다.

curl -s -H 'Host: b.example' http://127.0.0.1:8080/index.txt http://127.0.0.1:8080/index.txt

event MPM일 때입니다(번호와 포트는 실행마다 다릅니다).

[u09_trace:notice] u09 pre_connection: id=142 client_port=41326 base_server=a.example in=[(none)] out=[(none)]
[u09_trace:notice] u09 process_connection: id=142 client_port=41326 keepalives=0 in=[core_in] out=[core]
[u09_trace:notice] u09 pre_read_request: server=a.example limit_req_line=100
[u09_trace:notice] u09 post_read_request: the_request="GET /index.txt HTTP/1.1" method=GET(0) uri=/index.txt args=(null) protocol=HTTP/1.1(1001) hostname=b.example server=b.example headers_in=3 conn_id=142 client_port=41326
[u09_trace:notice] u09 process_connection: id=143 client_port=41326 keepalives=1 in=[core_in] out=[core]
[u09_trace:notice] u09 pre_read_request: server=a.example limit_req_line=100
[u09_trace:notice] u09 post_read_request: the_request="GET /index.txt HTTP/1.1" method=GET(0) uri=/index.txt args=(null) protocol=HTTP/1.1(1001) hostname=b.example server=b.example headers_in=3 conn_id=143 client_port=41326
[u09_trace:notice] u09 process_connection: id=144 client_port=41326 keepalives=2 in=[core_in] out=[core]
[u09_trace:notice] u09 pre_read_request: server=a.example limit_req_line=100
--- access_log ---
port=41326 k=0 vhost=b.example "GET /index.txt HTTP/1.1" 200
port=41326 k=1 vhost=b.example "GET /index.txt HTTP/1.1" 200

같은 설정에서 MPM만 prefork로 바꿨을 때입니다.

[u09_trace:notice] u09 pre_connection: id=0 client_port=41328 base_server=a.example in=[(none)] out=[(none)]
[u09_trace:notice] u09 process_connection: id=0 client_port=41328 keepalives=0 in=[core_in] out=[core]
[u09_trace:notice] u09 pre_read_request: server=a.example limit_req_line=100
[u09_trace:notice] u09 post_read_request: the_request="GET /index.txt HTTP/1.1" method=GET(0) uri=/index.txt args=(null) protocol=HTTP/1.1(1001) hostname=b.example server=b.example headers_in=3 conn_id=0 client_port=41328
[u09_trace:notice] u09 pre_read_request: server=a.example limit_req_line=100
[u09_trace:notice] u09 post_read_request: the_request="GET /index.txt HTTP/1.1" method=GET(0) uri=/index.txt args=(null) protocol=HTTP/1.1(1001) hostname=b.example server=b.example headers_in=3 conn_id=0 client_port=41328
[u09_trace:notice] u09 pre_read_request: server=a.example limit_req_line=100
--- access_log ---
port=41328 k=0 vhost=b.example "GET /index.txt HTTP/1.1" 200
port=41328 k=1 vhost=b.example "GET /index.txt HTTP/1.1" 200

두 경우 모두 클라이언트 포트가 같으니 연결은 하나이고, pre_connection도 한 번입니다. 다른 것은 process_connection의 횟수입니다. prefork는 한 번, 그 안의 while이 요청 두 개를 읽습니다. event는 세 번, 요청마다 한 번씩 불리고 마지막 한 번은 다음 요청을 기다리다 클라이언트가 연결을 닫아 ap_read_request()가 NULL을 돌려준 것입니다(끝의 pre_read_request 뒤에 post_read_request가 없습니다). prefork에서도 같은 이유로 마지막 pre_read_request가 한 번 더 찍혔습니다.

헷갈리기 쉬운 곳: event에서는 c->id가 바뀝니다

위 event 로그에서 같은 연결인데 id가 142 → 143 → 144로 바뀌었습니다. event는 다시 돌아온 연결을 집은 스레드의 번호로 c->id를 덮어씁니다.

        /* Subsequent request on a conn, and thread number is part of ID */
        c->id = conn_id;

server/mpm/event/event.c 1130–1131행

conn_id는 ID_FROM_CHILD_THREAD(자식 번호, 스레드 번호)로 계산한 값이라(1065행) "지금 이 연결을 맡은 스레드"를 가리킵니다. 그래서 c->id로 "같은 연결인지"를 판단하면 event에서는 틀립니다. 연결이 같은지 보려면 위처럼 클라이언트 포트와 %k를 함께 보는 편이 확실합니다.

4. ap_read_request — trunk에서는 필터가 요청 줄을 읽는다

이제 이 글의 한가운데인 ap_read_request()입니다(server/protocol.c 1306–1529행). 함수가 길어서 뼈대만 옮깁니다.

AP_DECLARE(request_rec *) ap_read_request(conn_rec *conn)
{
    /* ... */
    request_rec *r = ap_create_request(conn);

    tmp_bb = apr_brigade_create(r->pool, r->connection->bucket_alloc);
    conn->keepalive = AP_CONN_UNKNOWN;

    ap_run_pre_read_request(r, conn);

    r->request_time = apr_time_now();
    rv = ap_get_brigade(r->proto_input_filters, tmp_bb, AP_MODE_READBYTES, APR_BLOCK_READ, 0);
    /* ... */
    for (e = APR_BRIGADE_FIRST(tmp_bb);
         e != APR_BRIGADE_SENTINEL(tmp_bb);
         e = APR_BUCKET_NEXT(e))
    {
        if (AP_BUCKET_IS_REQUEST(e)) {
            if (!breq) breq = e->data;
        }
        else if (AP_BUCKET_IS_ERROR(e)) {
            if (!berr) berr = e;
        }
        /* ... */
    }
    /* ... */
    if (breq) {
        /* ... */
        if (!ap_assign_request_line(r, method, uri, protocol)) {
            /* ... */
        }
    }

    if (berr) {
        access_status = ((ap_bucket_error *)(berr->data))->status;
        goto die_unusable_input;
    }
    /* ... */
    apply_server_config(r);
    /* ... */
    ap_add_input_filter_handle(ap_http_input_filter_handle,
                               NULL, r, r->connection);

    /* Validate Host/Expect headers and select vhost. */
    if (!ap_check_request_header(r)) {
        /* ... */
        goto die_before_hooks;
    }

    /* we may have switched to another server */
    apply_server_config(r);

    if ((access_status = ap_post_read_request(r))) {
        goto die;
    }
    /* ... */
    return r;
    /* ... */
}

server/protocol.c 1306–1529행

놀랍게도 이 함수 안에는 소켓에서 줄을 읽는 코드가 없습니다. ap_get_brigade()를 딱 한 번 부르고, 돌아온 브리게이드(버킷이라는 데이터 조각을 이어 놓은 목록, 11편)에서 REQUEST 버킷이나 ERROR 버킷을 찾을 뿐입니다. 요청 줄과 헤더를 읽는 일은 그사이 어딘가에서 이미 끝나 있습니다.

ap_read_request의 흐름. ap_create_request가 요청 풀과 request_rec를 만들고 r->server를 기본 가상 호스트로 둔다. pre_read_request 훅에서 HTTP1_REQUEST_IN 필터가 달리고, ap_get_brigade를 한 번 부르면 그 필터가 요청 줄을 읽고 자르고 헤더를 읽는다. 성공하면 method, uri, protocol, headers를 담은 REQUEST 버킷이, 실패하면 400, 414, 408 같은 상태를 담은 ERROR 버킷이 나온다. REQUEST 버킷은 ap_assign_request_line, ap_check_request_header, post_read_request 훅을 거쳐 완성된 request_rec가 되고, ERROR 버킷은 ap_die로 오류 응답을 보내고 연결을 닫는다. 읽는 일(가운데 회색 상자)은 필터가 하고, ap_read_request()는 결과 버킷을 받아 request_rec에 옮겨 적습니다.

누가 읽나: pre_read_request가 다는 HTTP1_REQUEST_IN 필터

답은 ap_run_pre_read_request()에 있습니다. http 모듈이 이 훅에 건 함수가 요청마다 필터 하나를 답니다.

static void h1_pre_read_request(request_rec *r, conn_rec *c)
{
    if (!r->main && !r->prev
        && !strcmp(AP_PROTOCOL_HTTP1, ap_get_protocol(c))) {
        if (r->proxyreq == PROXYREQ_NONE) {
            ap_add_input_filter_handle(ap_h1_request_in_filter_handle,
                                       NULL, r, r->connection);
        }
        ap_add_output_filter_handle(ap_h1_response_out_filter_handle,
                                    NULL, r, r->connection);
    }
}

modules/http/http_core.c 274–285행

이 연결의 프로토콜이 HTTP/1.x이면 입력 쪽에 HTTP1_REQUEST_IN 필터를 답니다(r->main·r->prev가 비어 있다는 것은 서브요청이나 내부 리다이렉트가 아닌 "진짜" 요청이라는 뜻입니다, 10편). 그러면 ap_get_brigade()가 이 필터를 부르고, 필터 ap_h1_request_in_filter()(modules/http/http_filters.c 2248–2355행)가 두 단계로 일합니다.

  1. REQ_LINE — read_request_line()으로 요청 줄 한 줄을 읽고, ap_h1_tokenize_request_line()으로 메서드·주소·프로토콜 세 토막으로 자릅니다.
  2. REQ_HEADERS — ap_get_mime_headers_core()로 빈 줄이 나올 때까지 헤더를 읽어 r->headers_in 표에 넣습니다.

둘 다 성공하면 세 토막과 헤더 표를 담은 REQUEST 버킷을 만들어 브리게이드 맨 앞에 넣고 돌아갑니다. 실패하면 상태 번호만 담은 ERROR 버킷을 넣습니다. 필터는 처음 불릴 때 자기 자신을 필터 줄에서 빼 둡니다(ap_remove_input_filter(f), 2272행). 소스 주석은 "read_request_line()의 길을 비켜 준다"고만 적어 두었는데, 줄을 읽는 ap_rgetline()이 같은 필터 줄(r->proto_input_filters)을 통해 읽으므로 자기가 남아 있으면 자기 자신을 다시 거치게 됩니다.

ap_read_request()로 돌아오면 REQUEST 버킷의 세 토막을 ap_assign_request_line()(server/protocol.c 1177–1304행)에 넘겨 request_rec의 칸에 옮겨 적습니다. 이어서 HTTP_IN 필터를 달아 본문 읽을 준비를 하고, ap_check_request_header()로 Host를 확인하며 가상 호스트를 다시 고르고(6절), 마지막으로 post_read_request 훅을 돌립니다.

헷갈리기 쉬운 곳: 2.4.x에서는 ap_read_request()가 직접 읽습니다

우리가 흔히 설치하는 2.4.x 브랜치의 ap_read_request()는 함수 안에서 read_request_line()과 ap_parse_request_line()을 부르고, 이어서 ap_get_mime_headers_core()를 직접 부릅니다. HTTP1_REQUEST_IN 필터도, REQUEST 버킷도 2.4.x에는 없습니다. trunk는 "HTTP라는 의미"와 "HTTP/1.x라는 글자 형식"을 떼어 놓느라 HTTP/1.x에만 해당하는 읽기를 필터로 옮겼습니다. 그래서 HTTP/2를 다루는 mod_http2도 같은 REQUEST 버킷을 만들어 넣을 수 있습니다(modules/http2/h2_request.c 314행). 2.4.x의 소스를 함께 읽는다면 이 부분의 구조가 다르다는 것을 기억해 두세요. 이름이 남아 있는 ap_parse_request_line()은 trunk에서 ap_h1_tokenize_request_line()과 ap_assign_request_line()을 차례로 부르는 짧은 함수가 됐습니다(server/protocol.c 687–694행).

request_rec에 채워지는 것들

요청 하나를 읽는 동안 request_rec의 주요 칸이 채워지는 곳을 모으면 이렇습니다.

칸 채우는 곳 값
pool ap_create_request() (1063행) 연결 풀의 자식으로 만든 요청 풀
server ap_create_request() (1069행) → update_server_from_aliases() (vhost.c 1075행) 처음엔 conn->base_server, Host로 고른 뒤 그 가상 호스트
status ap_create_request() (1099행) 일단 HTTP_OK(200), 문제가 생기면 바뀜
request_time ap_read_request() (1323행) 요청을 읽기 시작한 시각
headers_in 필터의 ap_get_mime_headers_core() → ap_read_request() (1378행) 요청 헤더 표
protocol, proto_num r_assign_protocol() (1172–1173행) "HTTP/1.1", 1001(1×1000+1)
method, method_number, header_only ap_assign_request_line() (1191–1194행) "GET", M_GET(0), HEAD면 header_only = 1
the_request ap_assign_request_line() (1199행) 세 토막을 공백 하나로 다시 이은 요청 줄
unparsed_uri, uri, args, parsed_uri ap_parse_uri() (612–675행) /index.txt?x=1 → uri=/index.txt, args=x=1
hostname ap_parse_uri()(주소에 호스트가 있을 때) 또는 fix_hostname()(Host 헤더) 가상 호스트를 고를 이름
useragent_ip ap_create_request() (1109행) 연결의 클라이언트 IP(mod_remoteip가 나중에 바꿀 수 있음)

예제 모듈이 post_read_request에서 찍은 값입니다. 보낸 요청은 "먼저 알아 둘 말"의 그 요청 그대로입니다.

[u09_trace:notice] u09 post_read_request: the_request="GET /index.txt?x=1 HTTP/1.1" method=GET(0) uri=/index.txt args=x=1 protocol=HTTP/1.1(1001) hostname=b.example server=b.example headers_in=3 conn_id=128 client_port=41194

주소의 ? 뒤가 args로 갈라졌고, 헤더 세 개(Host, User-Agent, Connection)가 headers_in에 들어갔습니다.

5. 요청 줄과 헤더 — 한도와 엄격함

요청은 밖에서 오는 데이터라서 서버는 "얼마나 길게, 얼마나 느슨하게" 받아 줄지 정해 둬야 합니다. 그 기준이 되는 디렉티브와 기본값입니다.

디렉티브 기본값 정의된 곳 쓰이는 곳
LimitRequestLine 8190바이트 include/httpd.h 194–196행 read_request_line()
LimitRequestFieldSize 8190바이트 198–200행 ap_get_mime_headers_core()
LimitRequestFields 100개 202–204행 ap_get_mime_headers_core()
(디렉티브 없음) 요청 앞 빈 줄 10줄 206–208행 read_request_line()
HttpProtocolOptions Strict, LenientMethods, Allow0.9 server/core.c 4286–4326행 줄 읽기·토막 내기·헤더 읽기 전부

LimitRequest* 세 디렉티브는 RSRC_CONF, 곧 서버 설정과 <VirtualHost> 안에만 쓸 수 있고(server/core.c 4890–4896행), 값은 server_rec의 limit_req_line 같은 칸에 들어갑니다(7편). 기본값이 8190인 이유는 httpd.h의 주석에 있습니다. 내부 버퍼는 한도보다 2바이트 크게 잡으므로, 한도를 8190으로 두면 버퍼가 8192가 됩니다.

요청 줄을 읽는 곳에서 그 + 2를 볼 수 있습니다.

    do {
        /* ensure ap_rgetline allocates memory each time thru the loop
         * if there are empty lines
         */
        ctx->request_line = NULL;
        len = 0;
        rv = ap_rgetline(&ctx->request_line, (apr_size_t)(ctx->r->server->limit_req_line + 2),
                         &len, ctx->r, strict ? AP_GETLINE_CRLF : 0, bb);

        if (rv != APR_SUCCESS) {
            return rv;
        }
        else if (len > 0) {
            /* got the line in ctx->r->the_request */
            return APR_SUCCESS;
        }
    } while (--num_blank_lines >= 0);
    /* too many blank lines */
    return APR_EINVAL;

modules/http/http_filters.c 2212–2230행

빈 줄은 건너뛰되 10줄을 넘으면 포기하고, 한 줄이 한도를 넘으면 ap_rgetline()이 APR_ENOSPC("자리 없음")를 돌려줍니다. 필터는 이 오류 번호를 HTTP 상태로 바꿉니다.

            if ((rv = read_request_line(ctx, bb)) != APR_SUCCESS) {
                /* certain failures are answered with a HTTP error bucket
                 * and are terminal for parsing a request */
                ctx->method = ctx->uri = "-";
                ctx->protocol = "HTTP/1.0";
                if (APR_STATUS_IS_ENOSPC(rv)) {
                    http_status = HTTP_REQUEST_URI_TOO_LARGE;
                }
                else if (APR_STATUS_IS_TIMEUP(rv)) {
                    http_status = HTTP_REQUEST_TIME_OUT;
                }
                else if (APR_STATUS_IS_BADARG(rv)) {
                    http_status = HTTP_BAD_REQUEST;
                }
                else if (APR_STATUS_IS_EINVAL(rv)) {
                    http_status = HTTP_BAD_REQUEST;
                }
                goto cleanup;
            }

            if (!ap_h1_tokenize_request_line(r, ctx->request_line,
                                             &ctx->method, &ctx->uri, &ctx->protocol)) {
                http_status = HTTP_BAD_REQUEST;
                goto cleanup;
            }

modules/http/http_filters.c 2277–2301행

줄이 너무 길면 414, 시간이 다 되면 408, 형식이 틀리면 400입니다. 헤더 개수 한도는 헤더를 읽으면서 하나씩 세어 확인합니다.

            if (r->server->limit_req_fields
                    && (++fields_read > r->server->limit_req_fields)) {
                r->status = HTTP_BAD_REQUEST;
                apr_table_setn(r->notes, "error-notes",
                               "The number of request header fields "
                               "exceeds this server's limit.");
                ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, APLOGNO(00563)
                              "Number of request headers exceeds "
                              "LimitRequestFields");
                return;
            }

server/protocol.c 930–940행

limit_req_fields가 0이면 &&의 앞이 거짓이 되어 검사 자체를 건너뜁니다. 디렉티브 설명에 적힌 "0 = 무제한"이 이 한 줄입니다.

HttpProtocolOptions Strict — 얼마나 깐깐하게 볼 것인가

HttpProtocolOptions의 기본은 Strict입니다. 코드 곳곳에서 이 설정을 strict라는 변수로 꺼내 씁니다(conf->http_conformance != AP_HTTP_CONFORMANCE_UNSAFE). strict일 때 달라지는 것 가운데 소스에서 확인한 것만 꼽으면 이렇습니다.

예제: 400과 414

curl은 잘못된 요청을 보내 주지 않으므로 bash의 /dev/tcp로 날 요청을 그대로 보냅니다(컨테이너에 nc가 없어서 이 방법을 썼습니다). 응답의 첫 줄만 보는 도우미 함수입니다.

# 날 요청을 그대로 보내고 응답의 첫 줄(상태 줄)만 본다
raw() {
  exec 3<>/dev/tcp/127.0.0.1/8080
  printf '%b' "$1" >&3
  timeout 3 cat <&3 | head -1 | tr -d '\r'
  exec 3>&-
}

raw 'GET /index.txt HTTP/1.1 extra\r\nHost: b.example\r\n\r\n'
raw 'GET /index.txt http/1.1\r\nHost: b.example\r\n\r\n'
raw 'GET /index.txt HTTP/1.1\r\nConnection: close\r\n\r\n'

세 요청 모두 HTTP/1.1 400 Bad Request가 돌아왔습니다. LogLevel core:debug http:debug로 남긴 이유와 접근 로그입니다. 오류 로그에서 예제 모듈의 u09 줄은 뺐습니다(포트는 실행마다 다릅니다).

[http:debug] AH03448: HTTP Request Line; Excess whitespace (disallowed by HttpProtocolOptions Strict)
[core:debug] AH10388: HTTP Request: Unrecognized protocol 'http/1.1' (perhaps whitespace was injected?)
[core:debug] AH00569: client sent HTTP/1.1 request without hostname (see RFC2616 section 14.23): /index.txt
--- access_log ---
port=41234 k=0 vhost=a.example "-" 400
port=41238 k=0 vhost=a.example "GET /index.txt http/1.1" 400
port=41254 k=0 vhost=a.example "GET /index.txt HTTP/1.1" 400

세 400은 걸린 곳이 모두 다릅니다. 첫째는 필터 안의 ap_h1_tokenize_request_line()(http 모듈), 둘째는 ap_read_request()로 돌아온 뒤의 r_assign_protocol()(코어), 셋째는 줄 읽기가 다 끝난 뒤 ap_check_request_header()가 HTTP/1.1인데 Host가 없다고 거절한 것입니다(server/protocol.c 729–744행).

요청을 읽는 길에서 각 오류가 걸리는 자리. HTTP1_REQUEST_IN 필터 안의 read_request_line() 은 r->server->limit_req_line + 2 를 한도로 읽는데, 이때 r->server 는 아직 c->base_server 다. 한도를 넘으면 APR_ENOSPC 로 414 가 되고, 그래서 긴 요청 줄 예제는 Host: b.example 이어도 a.example 의 LimitRequestLine 100 이 적용된다. 다음 ap_h1_tokenize_request_line()(http 모듈)에서 공백이 하나씩이 아니면 1번째 요청 GET /index.txt HTTP/1.1 extra 처럼 400(AH03448 Excess whitespace)이다. 같은 필터 안에서 ap_get_mime_headers_core() 가 LimitRequestFieldSize 와 LimitRequestFields 를 보며 헤더를 읽는다. ap_read_request() 로 돌아와 코어의 r_assign_protocol() 에서 대문자 HTTP/x.y 가 아니면 2번째 요청 GET /index.txt http/1.1 처럼 400(AH10388 Unrecognized protocol)이다. 마지막으로 ap_check_request_header() 에서 HTTP/1.1 인데 Host 가 없으면 3번째 요청처럼 400(AH00569)이고, 통과하면 가상 호스트 고르기로 간다 위 세 400과 아래 414 가 걸린 자리를 읽는 순서대로 놓았습니다. 헤더를 필터 안에서 읽는다는 것은 http_filters.c 2297–2307행, 그 뒤의 검사는 protocol.c 1385·1466행에서 확인했습니다.

다음은 긴 요청 줄입니다. 예제 설정에서는 첫 번째 가상 호스트 a.example에만 LimitRequestLine 100을 두고 b.example은 기본값(8190)으로 두었습니다. 그리고 150글자짜리 주소를 Host: b.example 로 보냈습니다.

=== 3. long request line (Host: b.example, a.example has LimitRequestLine 100) ===
HTTP/1.1 414 Request-URI Too Long
--- error_log (u09/core/http) ---
[u09_trace:notice] u09 pre_connection: id=64 client_port=41286 base_server=a.example in=[(none)] out=[(none)]
[u09_trace:notice] u09 process_connection: id=64 client_port=41286 keepalives=0 in=[core_in] out=[core]
[u09_trace:notice] u09 pre_read_request: server=a.example limit_req_line=100
--- access_log ---
port=41286 k=0 vhost=a.example "-" 414

b.example의 한도는 8190인데 414가 났습니다. 다음 "헷갈리기 쉬운 곳"이 그 이유입니다.

헷갈리기 쉬운 곳

6. 가상 호스트 고르기와 post_read_request

두 단계로 나눠 고른다

가상 호스트는 한 번에 골라지지 않습니다. 연결이 들어온 순간에는 IP와 포트만 알고, Host 이름은 요청 헤더를 다 읽어야 압니다. 그래서 httpd는 두 번에 나눠 고릅니다.

가상 호스트를 고르는 두 단계. 연결이 *:8080으로 들어오면 1단계 ap_update_vhost_given_ip가 IP와 포트 해시 표를 찾아 c->base_server에 그 주소의 첫 가상 호스트를, c->vhost_lookup_data에 이름 목록을 넣는다. 요청 줄과 헤더는 이때의 base_server 한도로 읽는다. 2단계 ap_update_vhost_from_headers_ex가 Host의 이름으로 목록을 훑어 ServerName이나 ServerAlias와 맞으면 r->server를 그 가상 호스트로 바꾸고, 맞는 것이 없으면 base_server를 그대로 두며 StrictHostCheck On이면 400을 낸다. 빨간 상자가 5절의 함정입니다. 2단계 전에 읽는 것은 1단계에서 정한 서버의 한도를 따릅니다.

1단계는 1절에서 본 ap_update_vhost_given_ip()입니다.

AP_DECLARE(void) ap_update_vhost_given_ip(conn_rec *conn)
{
    ipaddr_chain *trav;
    apr_port_t port;

    /* scan the hash table for an exact match first */
    trav = find_ipaddr(conn->local_addr);

    if (trav) {
        /* save the name_chain for later in case this is a name-vhost */
        conn->vhost_lookup_data = trav->names;
        conn->base_server = trav->server;
        return;
    }

    /* maybe there's a default server or wildcard name-based vhost
     * matching this port
     */
    port = conn->local_addr->port;

    trav = find_default_server(port);
    if (trav) {
        conn->vhost_lookup_data = trav->names;
        conn->base_server = trav->server;
        return;
    }

    /* otherwise we're stuck with just the main server
     * and no name-based vhosts
     */
    conn->vhost_lookup_data = NULL;
}

server/vhost.c 1267–1298행

설정을 읽을 때 만들어 둔 "IP:포트 → 가상 호스트" 해시 표(iphash_table)에서 연결이 들어온 서버 쪽 주소(local_addr)를 찾고, 없으면 *:포트 같은 와일드카드 목록(default_list)에서 찾습니다. 찾으면 두 가지를 연결에 적어 둡니다. base_server에는 그 주소의 첫 번째 가상 호스트를, vhost_lookup_data에는 같은 주소를 쓰는 이름 목록을 둡니다. 이 표는 설정을 읽은 뒤로 바뀌지 않으므로 여러 스레드가 동시에 읽어도 안전하다고 소스 주석에 적혀 있습니다(server/vhost.c 87–94행).

2단계는 헤더를 다 읽은 뒤 ap_check_request_header()가 부르는 ap_update_vhost_from_headers_ex()입니다(server/vhost.c 1160–1211행). Host 헤더(요청 줄에 http://이름/…처럼 호스트가 들어 있으면 그쪽이 우선)를 정리해 r->hostname에 두고, 1단계에서 적어 둔 이름 목록을 update_server_from_aliases()로 훑습니다(986–1077행). ServerName이나 ServerAlias와 맞는 첫 서버를 찾으면 r->server = s로 바꿉니다. 이 함수의 주석은 Host에 포트가 적혀 있어도 무시하고 실제 소켓의 포트를 쓴다고 밝히고, 그 이유 다섯 가지를 적어 두었습니다(988–1000행).

맞는 이름이 없으면 update_server_from_aliases()는 HTTP_BAD_REQUEST를 돌려주지만, 그 값을 실제로 400으로 쓰는 것은 StrictHostCheck On일 때뿐입니다.

    strict_host_check = (conf->strict_host_check == AP_CORE_CONFIG_ON);
    access_status = ap_update_vhost_from_headers_ex(r, strict_host_check);
    if (strict_host_check && access_status != HTTP_OK) { 

server/protocol.c 708–710행

그렇지 않으면 r->server는 base_server, 곧 그 주소의 첫 번째 가상 호스트로 남습니다. "이름 기반 가상 호스트에서는 맨 처음 적은 것이 기본"이라는 익숙한 규칙이 코드에서는 이렇게 나옵니다.

예제: Host에 따라 다른 사이트

*:8080에 a.example(먼저 적음)과 b.example 두 가상 호스트를 두고, 문서 폴더에 각각 "site A", "site B"를 적은 파일을 두었습니다. httpd -S로 본 가상 호스트 표입니다.

VirtualHost configuration:
*:8080                 is a NameVirtualHost
         default server a.example (/tmp/u09.conf:20)
         port 8080 namevhost a.example (/tmp/u09.conf:20)
         port 8080 namevhost b.example (/tmp/u09.conf:25)

Host만 바꿔 세 번 요청했습니다.

for h in a.example b.example c.example; do
  printf '%s -> ' "$h"; curl -s -H "Host: $h" http://127.0.0.1:8080/index.txt
done
a.example -> site A
b.example -> site B
c.example -> site A

어느 이름과도 맞지 않는 c.example은 default server인 a.example로 갔습니다. 예제 모듈의 로그도 같은 이야기를 합니다. 모든 요청에서 pre_read_request 때는 server=a.example이었고, post_read_request 때 b.example 요청만 server=b.example로 바뀌었으며, c.example 요청은 hostname=c.example server=a.example이었습니다.

post_read_request — 요청을 다 읽은 뒤 처음 끼어드는 자리

ap_read_request()의 마지막 단계는 ap_post_read_request()입니다.

AP_DECLARE(int) ap_post_read_request(request_rec *r)
{
    int status;

    if ((status = ap_run_post_read_request(r))) {
        return status;
    }

    /* Enforce http(s) only scheme for non-forward-proxy requests */
    if (!r->proxyreq
            && r->parsed_uri.scheme
            && (ap_cstr_casecmpn(r->parsed_uri.scheme, "http", 4) != 0
| (r->parsed_uri.scheme[4] != '\0'
                    && (apr_tolower(r->parsed_uri.scheme[4]) != 's'
| r->parsed_uri.scheme[5] != '\0')))) {
        return HTTP_BAD_REQUEST;
    }

    return OK;
}

server/protocol.c 1531–1550행

post_read_request는 RUN_ALL 훅입니다(2554–2555행). 요청 줄과 헤더가 다 있고 가상 호스트도 정해졌지만 아직 URL을 파일로 옮기는 일은 시작하지 않은 시점이라, "모든 요청에 대해 가장 먼저 하고 싶은 일"이 여기에 걸립니다.

훅이 모두 끝나면, 프록시 요청이 아닌데 주소의 스킴이 http·https가 아니면 400으로 막습니다. 여기까지 무사히 오면 ap_read_request()가 완성된 request_rec를 돌려주고, 3절의 ap_process_request()가 이어받습니다.

헷갈리기 쉬운 곳: 오류가 나도 post_read_request는 불리지 않습니다

ap_read_request()의 실패 출구는 die_unusable_input → die_before_hooks → die 순서로 이어지는 꼬리입니다. 헤더 확인에서 실패하면 die_before_hooks로 가서 post_read_request 훅을 건너뛰고 곧바로 ap_die()로 오류 응답을 보냅니다. 입력이 망가진 경우(die_unusable_input)에는 CORE_IN 위에 쌓인 입력 필터를 걷어내고 연결을 닫기로 표시합니다(server/protocol.c 1487–1529행). 5절의 예제에서 400·414 요청에 post_read_request 로그가 하나도 없었던 이유입니다. 그러므로 post_read_request에 건 코드는 "형식이 올바른 요청"만 본다고 생각해야 합니다.

이 글에서 뺀 것

큰 줄기를 위해 다음은 이름과 위치만 적어 둡니다.

정리

개념 한 줄 요약
ap_process_connection() 기본 가상 호스트 정하기 → pre_connection → process_connection. event는 같은 순서를 process_socket()에 풀어 썼다
create_connection / pre_connection 코어가 REALLY_LAST로 걸어, 다른 모듈이 먼저 나설 기회를 준다
core_pre_connection() Nagle 끄기, 타임아웃, 소켓 보관, 맨 아래 필터 CORE_IN·CORE 달기
process_connection HTTP/1.x는 http 모듈이 REALLY_LAST로 맡는다
keep-alive 루프 prefork·worker는 http 모듈의 while 안, event는 MPM이 연결을 다시 넘길 때마다 훅이 다시 불린다
ap_read_request() 요청 풀과 request_rec를 만들고, 읽기는 필터에 맡기고, 결과를 옮겨 적고, 가상 호스트를 다시 고르고, post_read_request를 돌린다
HTTP1_REQUEST_IN trunk에서 요청 줄과 헤더를 읽어 REQUEST(또는 ERROR) 버킷으로 내놓는 필터. 2.4.x에는 없다
LimitRequest* 줄 길이·헤더 크기·헤더 개수 한도. Host를 알기 전에 쓰이므로 기본 가상 호스트의 값이 적용된다
HttpProtocolOptions Strict CR LF, 공백 하나, 대문자 HTTP/, 토큰 글자만 허용 — 어기면 400
가상 호스트 고르기 1단계 IP:포트(연결마다), 2단계 Host 이름(요청마다). 못 찾으면 첫 가상 호스트
post_read_request 요청을 다 읽은 뒤 처음 끼어드는 RUN_ALL 훅. 올바른 요청에만 불린다

다음에 볼 것

소켓 하나가 conn_rec가 되고, 요청 줄과 헤더가 request_rec가 되기까지를 봤습니다. 이제 request_rec에는 메서드와 주소, 헤더, 그리고 어느 가상 호스트인지가 들어 있습니다. 하지만 /index.txt가 디스크의 어느 파일인지, 이 요청에 <Directory>와 <Location>의 어느 설정이 적용되는지, 누가 응답을 만들지는 아직 아무도 정하지 않았습니다. 그 일을 하는 ap_process_request()와 ap_process_request_internal()의 단계 순서를 10편: 요청 처리 단계 — URL에서 파일과 핸들러까지에서 따라갑니다.

Spring과 비교하면, Spring MVC는 이 글의 일을 직접 하지 않습니다. 소켓을 받고 요청 줄과 헤더를 읽어 HttpServletRequest를 만드는 것은 Tomcat 같은 서블릿 컨테이너이고, Spring의 DispatcherServlet은 그렇게 다 만들어진 요청 객체를 받는 데서 시작합니다(Spring 21편). httpd의 request_rec가 하는 역할이 HttpServletRequest의 자리라고 보면 됩니다.

다른 글