Index: buckets/response_buckets.c =================================================================== --- buckets/response_buckets.c (revision 1712525) +++ buckets/response_buckets.c (working copy) @@ -27,14 +27,16 @@ #include "serf_private.h" typedef struct response_context_t { serf_bucket_t *stream; serf_bucket_t *body; /* Pointer to the stream wrapping the body. */ - serf_bucket_t *headers; /* holds parsed headers */ + serf_bucket_t *incoming_headers; /* holds parsed headers */ + serf_bucket_t *fetch_headers; /* the current set of headers */ enum { STATE_STATUS_LINE, /* reading status line */ + STATE_NEXT_STATUS_LINE, STATE_HEADERS, /* reading headers */ STATE_BODY, /* reading body */ STATE_TRAILERS, /* reading trailers */ STATE_DONE /* we've sent EOF */ } state; @@ -61,14 +63,10 @@ typedef struct response_context_t { static int expect_body(response_context_t *ctx) { if (ctx->head_req) return 0; - /* 100 Continue and 101 Switching Protocols */ - if (ctx->sl.code >= 100 && ctx->sl.code < 200) - return 0; - /* 204 No Content */ if (ctx->sl.code == 204) return 0; /* 205? */ @@ -87,17 +85,19 @@ serf_bucket_t *serf_bucket_response_create( response_context_t *ctx; ctx = serf_bucket_mem_alloc(allocator, sizeof(*ctx)); ctx->stream = stream; ctx->body = NULL; - ctx->headers = serf_bucket_headers_create(allocator); + ctx->incoming_headers = serf_bucket_headers_create(allocator); + ctx->fetch_headers = ctx->incoming_headers; ctx->state = STATE_STATUS_LINE; ctx->chunked = 0; ctx->head_req = 0; ctx->decode_content = TRUE; ctx->error_on_eof = 0; ctx->config = NULL; + ctx->sl.reason = NULL; serf_linebuf_init(&ctx->linebuf); return serf_bucket_create(&serf_bucket_type_response, allocator, ctx); } @@ -119,27 +119,31 @@ void serf_bucket_response_decode_content(serf_buck } serf_bucket_t *serf_bucket_response_get_headers( serf_bucket_t *bucket) { - return ((response_context_t *)bucket->data)->headers; + return ((response_context_t *)bucket->data)->fetch_headers; } static void serf_response_destroy_and_data(serf_bucket_t *bucket) { response_context_t *ctx = bucket->data; - if (ctx->state != STATE_STATUS_LINE) { + if (ctx->sl.reason) { serf_bucket_mem_free(bucket->allocator, (void*)ctx->sl.reason); } serf_bucket_destroy(ctx->stream); if (ctx->body != NULL) serf_bucket_destroy(ctx->body); - serf_bucket_destroy(ctx->headers); + if (ctx->incoming_headers) + serf_bucket_destroy(ctx->incoming_headers); + if (ctx->fetch_headers && ctx->fetch_headers != ctx->incoming_headers) + serf_bucket_destroy(ctx->fetch_headers); + serf_default_destroy_and_data(bucket); } static apr_status_t fetch_line(response_context_t *ctx, int acceptable) { @@ -150,10 +154,15 @@ static apr_status_t parse_status_line(response_con serf_bucket_alloc_t *allocator) { int res; char *reason; /* ### stupid APR interface makes this non-const */ + if (ctx->sl.reason) { + serf_bucket_mem_free(allocator, (void*)ctx->sl.reason); + ctx->sl.reason = NULL; + } + /* ctx->linebuf.line should be of form: 'HTTP/1.1 200 OK', but we also explicitly allow the forms 'HTTP/1.1 200' (no reason) and 'HTTP/1.1 401.1 Logon failed' (iis extended error codes) NOTE: Since r1699995 linebuf.line is always NUL terminated string. */ res = apr_date_checkmask(ctx->linebuf.line, "HTTP/#.# ###*"); @@ -221,11 +230,11 @@ static apr_status_t fetch_headers(serf_bucket_t *b } /* Always copy the headers (from the linebuf into new mem). */ /* ### we should be able to optimize some mem copies */ serf_bucket_headers_setx( - ctx->headers, + ctx->incoming_headers, ctx->linebuf.line, end_key - ctx->linebuf.line, 1, c, ctx->linebuf.line + ctx->linebuf.used - c, 1); } return status; @@ -243,10 +252,11 @@ static apr_status_t run_machine(serf_bucket_t *bkt { apr_status_t status = APR_SUCCESS; /* initialize to avoid gcc warnings */ switch (ctx->state) { case STATE_STATUS_LINE: + case STATE_NEXT_STATUS_LINE: /* RFC 2616 says that CRLF is the only line ending, but we can easily * accept any kind of line ending. */ status = fetch_line(ctx, SERF_NEWLINE_ANY); @@ -294,14 +304,29 @@ static apr_status_t run_machine(serf_bucket_t *bkt if (ctx->linebuf.state == SERF_LINEBUF_READY && !ctx->linebuf.used) { const char *v; int chunked = 0; int gzip = 0; + if (ctx->fetch_headers != ctx->incoming_headers) { + /* We now only have one interesting set of headers remaining */ + serf_bucket_destroy(ctx->fetch_headers); + ctx->fetch_headers = ctx->incoming_headers; + } + + if (ctx->sl.code >= 100 && ctx->sl.code < 200) { + /* We received a set of informational headers. + + Prepare for the next set */ + ctx->incoming_headers = serf_bucket_headers_create( + bkt->allocator); + ctx->state = STATE_NEXT_STATUS_LINE; + break; + } /* Advance the state. */ ctx->state = STATE_BODY; - /* If this is a response to a HEAD request, or code 1xx, 204 or 304 + /* If this is a response to a HEAD request, or 204 or 304 then we don't receive a real body. */ if (!expect_body(ctx)) { ctx->body = serf_bucket_simple_create(NULL, 0, NULL, NULL, bkt->allocator); ctx->state = STATE_BODY; @@ -310,11 +335,12 @@ static apr_status_t run_machine(serf_bucket_t *bkt ctx->body = serf_bucket_barrier_create(ctx->stream, bkt->allocator); /* Are we chunked, C-L, or conn close? */ - v = serf_bucket_headers_get(ctx->headers, "Transfer-Encoding"); + v = serf_bucket_headers_get(ctx->fetch_headers, + "Transfer-Encoding"); /* Need a copy cuz we're going to write NUL characters into the string. */ if (v) { char *attrs = serf_bstrdup(bkt->allocator, v); @@ -342,11 +368,12 @@ static apr_status_t run_machine(serf_bucket_t *bkt else { /* RFC 7231 specifies that we should determine the message length via Transfer-Encoding chunked, when both chunked and Content-Length are passed */ - v = serf_bucket_headers_get(ctx->headers, "Content-Length"); + v = serf_bucket_headers_get(ctx->fetch_headers, + "Content-Length"); if (v) { apr_uint64_t length; length = apr_strtoi64(v, NULL, 10); if (errno == ERANGE) { return APR_FROM_OS_ERROR(ERANGE); @@ -363,11 +390,12 @@ static apr_status_t run_machine(serf_bucket_t *bkt serf_bucket_deflate_create(ctx->body, bkt->allocator, SERF_DEFLATE_GZIP); serf_bucket_set_config(ctx->body, ctx->config); } - v = serf_bucket_headers_get(ctx->headers, "Content-Encoding"); + v = serf_bucket_headers_get(ctx->fetch_headers, + "Content-Encoding"); if (v && ctx->decode_content) { /* Need to handle multiple content-encoding. */ if (v && strcasecmp("gzip", v) == 0) { ctx->body = serf_bucket_deflate_create(ctx->body, bkt->allocator, @@ -432,10 +460,51 @@ apr_status_t serf_bucket_response_wait_for_headers response_context_t *ctx = bucket->data; return wait_for_body(bucket, ctx); } +apr_status_t serf_bucket_response_wait_for_some_headers( + serf_bucket_t *bucket, + int wait_for_next) +{ + response_context_t *ctx = bucket->data; + + if (ctx->incoming_headers != ctx->fetch_headers) { + /* We have a good set of informational + headers */ + + if (!wait_for_next) + return APR_SUCCESS; + + /* We stop caring about a previous set, if there is one */ + serf_bucket_destroy(ctx->fetch_headers); + ctx->fetch_headers = ctx->incoming_headers; + + /* And fixup the state if we just read this one to avoid + theoretically returning success again */ + if (ctx->state == STATE_NEXT_STATUS_LINE) + ctx->state = STATE_STATUS_LINE; + } + + /* Keep reading and moving until we are in BODY or + STATE_NEXT_STATUS_LINE */ + while (ctx->state != STATE_BODY + && ctx->state != STATE_NEXT_STATUS_LINE) { + + apr_status_t status = run_machine(bucket, ctx); + + /* Anything other than APR_SUCCESS means that we cannot immediately + * read again (for now). + */ + if (status) + return status; + } + + /* in STATE_BODY or STATE_NEXT_STATUS_LINE */ + return APR_SUCCESS; +} + apr_status_t serf_bucket_response_status( serf_bucket_t *bkt, serf_status_line *sline) { response_context_t *ctx = bkt->data; @@ -550,11 +619,11 @@ apr_status_t serf_response_full_become_aggregate(s bkt = SERF_BUCKET_SIMPLE_STRING_LEN("\r\n", 2, bucket->allocator); serf_bucket_aggregate_append(bucket, bkt); /* Add headers and stream buckets in order. */ - serf_bucket_aggregate_append(bucket, ctx->headers); + serf_bucket_aggregate_append(bucket, ctx->fetch_headers); serf_bucket_aggregate_append(bucket, ctx->stream); if (ctx->body != NULL) serf_bucket_destroy(ctx->body); serf_bucket_mem_free(bucket->allocator, (void*)ctx->sl.reason); Index: serf_bucket_types.h =================================================================== --- serf_bucket_types.h (revision 1712525) +++ serf_bucket_types.h (working copy) @@ -124,10 +124,26 @@ apr_status_t serf_bucket_response_status( */ apr_status_t serf_bucket_response_wait_for_headers( serf_bucket_t *response); /** + * Wait for the first HTTP headers to be processed for a @a response. + * If 1XX informational responses are received before the actual headers + * this function will return APR_SUCCESS as soon as such a set is processed, + * while serf_bucket_response_wait_for_headers() will wait until the + * actual headers to be available. + * + * If @a wait_for_next is TRUE, the function will wait for the next set + * of informational header instead of returning success for the first set. + * + * @since New in 1.4. + */ +apr_status_t serf_bucket_response_wait_for_some_headers( + serf_bucket_t *response, + int wait_for_next); + +/** * Get the headers bucket for @a response. */ serf_bucket_t *serf_bucket_response_get_headers( serf_bucket_t *response); Index: test/test_buckets.c =================================================================== --- test/test_buckets.c (revision 1712525) +++ test/test_buckets.c (working copy) @@ -1319,14 +1319,17 @@ static void test_response_no_body_expected(CuTest tmp = SERF_BUCKET_SIMPLE_STRING(message_list[i], alloc); bkt = serf_bucket_response_create(tmp, alloc); status = read_all(bkt, buf, sizeof(buf), &len); + if (i == 0) { + /* blablablablabla is parsed as the next status line */ + CuAssertIntEquals(tc, SERF_ERROR_BAD_HTTP_RESPONSE, status); + } + else + CuAssertIntEquals(tc, 0, len); - CuAssertIntEquals(tc, APR_EOF, status); - CuAssertIntEquals(tc, 0, len); - serf_bucket_destroy(bkt); } } /* Test handling IIS 'extended' status codes (like 401.1) by response @@ -1507,10 +1510,98 @@ static void test_random_eagain_in_response(CuTest } apr_pool_destroy(iter_pool); } #undef BODY +static void test_response_continue(CuTest *tc) +{ + test_baton_t *tb = tc->testBaton; + serf_bucket_t *bkt, *headers; + serf_bucket_alloc_t *alloc = test__create_bucket_allocator(tc, tb->pool); + const char long_response[] = + "HTTP/1.1 100 Continue" CRLF + "H: 1" CRLF + "Foo: Bar" CRLF + CRLF + "HTTP/1.1 109 Welcome to HTTP-9" CRLF + "Connection: Upgrade" CRLF + "H: 2" CRLF + "Upgrade: h9c" CRLF + CRLF + "HTTP/9.0 200 OK" CRLF + "Content-Type: text/plain" CRLF + "Content-Length: 26" CRLF + "H: 3" CRLF + CRLF + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + CRLF + CRLF; + + /* 1: First verify that we just read the body*/ + bkt = SERF_BUCKET_SIMPLE_STRING(long_response, alloc); + bkt = serf_bucket_response_create(bkt, alloc); + + read_and_check_bucket(tc, bkt, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"); + serf_bucket_destroy(bkt); + + /* 2: Check the headers the normal way */ + bkt = SERF_BUCKET_SIMPLE_STRING(long_response, alloc); + bkt = serf_bucket_response_create(bkt, alloc); + + CuAssertIntEquals(tc, APR_SUCCESS, + serf_bucket_response_wait_for_headers(bkt)); + headers = serf_bucket_response_get_headers(bkt); + /* Verify that we just have the final set */ + CuAssertStrEquals(tc, "3", serf_bucket_headers_get(headers, "H")); + CuAssertStrEquals(tc, NULL, serf_bucket_headers_get(headers, "Foo")); + read_and_check_bucket(tc, bkt, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"); + serf_bucket_destroy(bkt); + + /* 3: Fetch the separate headers */ + bkt = SERF_BUCKET_SIMPLE_STRING(long_response, alloc); + bkt = serf_bucket_response_create(bkt, alloc); + + CuAssertIntEquals(tc, APR_SUCCESS, + serf_bucket_response_wait_for_some_headers(bkt, FALSE)); + headers = serf_bucket_response_get_headers(bkt); + CuAssertStrEquals(tc, "1", serf_bucket_headers_get(headers, "H")); + + /* Again*/ + CuAssertIntEquals(tc, APR_SUCCESS, + serf_bucket_response_wait_for_some_headers(bkt, FALSE)); + headers = serf_bucket_response_get_headers(bkt); + CuAssertStrEquals(tc, "1", serf_bucket_headers_get(headers, "H")); + + /* Now fetch second set */ + CuAssertIntEquals(tc, APR_SUCCESS, + serf_bucket_response_wait_for_some_headers(bkt, TRUE)); + headers = serf_bucket_response_get_headers(bkt); + CuAssertStrEquals(tc, "2", serf_bucket_headers_get(headers, "H")); + + /* Now fetch final set */ + CuAssertIntEquals(tc, APR_SUCCESS, + serf_bucket_response_wait_for_some_headers(bkt, TRUE)); + headers = serf_bucket_response_get_headers(bkt); + CuAssertStrEquals(tc, "3", serf_bucket_headers_get(headers, "H")); + + /* Fetch same again */ + CuAssertIntEquals(tc, APR_SUCCESS, + serf_bucket_response_wait_for_some_headers(bkt, TRUE)); + headers = serf_bucket_response_get_headers(bkt); + CuAssertStrEquals(tc, "3", serf_bucket_headers_get(headers, "H")); + + CuAssertIntEquals(tc, APR_SUCCESS, + serf_bucket_response_wait_for_headers(bkt)); + + headers = serf_bucket_response_get_headers(bkt); + CuAssertStrEquals(tc, "3", serf_bucket_headers_get(headers, "H")); + CuAssertStrEquals(tc, NULL, serf_bucket_headers_get(headers, "Foo")); + read_and_check_bucket(tc, bkt, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"); + serf_bucket_destroy(bkt); +} + + static void test_dechunk_buckets(CuTest *tc) { test_baton_t *tb = tc->testBaton; serf_bucket_t *mock_bkt, *bkt; serf_bucket_alloc_t *alloc = test__create_bucket_allocator(tc, tb->pool); @@ -2530,10 +2621,11 @@ CuSuite *test_buckets(void) SUITE_ADD_TEST(suite, test_response_body_chunked_incomplete_crlf); SUITE_ADD_TEST(suite, test_response_body_chunked_gzip_small); SUITE_ADD_TEST(suite, test_response_bucket_peek_at_headers); SUITE_ADD_TEST(suite, test_response_bucket_iis_status_code); SUITE_ADD_TEST(suite, test_response_bucket_no_reason); + SUITE_ADD_TEST(suite, test_response_continue); SUITE_ADD_TEST(suite, test_bucket_header_set); SUITE_ADD_TEST(suite, test_bucket_header_do); SUITE_ADD_TEST(suite, test_iovec_buckets); SUITE_ADD_TEST(suite, test_aggregate_buckets); SUITE_ADD_TEST(suite, test_aggregate_bucket_readline);