From 37025e98d62f42907e596d1fdcff5b04afe59eb7 Mon Sep 17 00:00:00 2001 From: Ichito Nagata Date: Wed, 1 Aug 2018 01:12:23 +0900 Subject: [PATCH 01/10] move chunked encoding code to http1 protocol handler --- CMakeLists.txt | 1 - h2o.xcodeproj/project.pbxproj | 8 --- include/h2o.h | 11 --- lib/core/config.c | 1 - lib/handler/chunked.c | 123 ---------------------------------- lib/http1.c | 86 ++++++++++++++++++++++-- 6 files changed, 81 insertions(+), 149 deletions(-) delete mode 100644 lib/handler/chunked.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 9c323afad3..8c58b8d3c9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -303,7 +303,6 @@ SET(LIB_SOURCE_FILES lib/core/util.c lib/handler/access_log.c - lib/handler/chunked.c lib/handler/compress.c lib/handler/compress/gzip.c lib/handler/errordoc.c diff --git a/h2o.xcodeproj/project.pbxproj b/h2o.xcodeproj/project.pbxproj index 74ce6d27f4..d227721a59 100644 --- a/h2o.xcodeproj/project.pbxproj +++ b/h2o.xcodeproj/project.pbxproj @@ -104,7 +104,6 @@ 107923A619A3215F00C52AD6 /* http2.h in Headers */ = {isa = PBXBuildFile; fileRef = 107923A119A3215F00C52AD6 /* http2.h */; }; 107923A719A3215F00C52AD6 /* token.h in Headers */ = {isa = PBXBuildFile; fileRef = 107923A219A3215F00C52AD6 /* token.h */; }; 107923A919A3215F00C52AD6 /* h2o.h in Headers */ = {isa = PBXBuildFile; fileRef = 107923A419A3215F00C52AD6 /* h2o.h */; }; - 107923C219A3217300C52AD6 /* chunked.c in Sources */ = {isa = PBXBuildFile; fileRef = 107923AE19A3217300C52AD6 /* chunked.c */; }; 107923C319A3217300C52AD6 /* file.c in Sources */ = {isa = PBXBuildFile; fileRef = 107923AF19A3217300C52AD6 /* file.c */; }; 107923C519A3217300C52AD6 /* http1.c in Sources */ = {isa = PBXBuildFile; fileRef = 107923B119A3217300C52AD6 /* http1.c */; }; 107923C619A3217300C52AD6 /* connection.c in Sources */ = {isa = PBXBuildFile; fileRef = 107923B319A3217300C52AD6 /* connection.c */; }; @@ -124,7 +123,6 @@ 1079242919A325BE00C52AD6 /* simple.c in Sources */ = {isa = PBXBuildFile; fileRef = 107923FC19A3238500C52AD6 /* simple.c */; settings = {COMPILER_FLAGS = "-Wno-deprecated-declarations"; }; }; 1079243019A3260E00C52AD6 /* libh2o.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1079231519A320A700C52AD6 /* libh2o.a */; }; 1079244119A3264300C52AD6 /* picohttpparser.c in Sources */ = {isa = PBXBuildFile; fileRef = 1079235A19A3210D00C52AD6 /* picohttpparser.c */; }; - 1079244419A3265C00C52AD6 /* chunked.c in Sources */ = {isa = PBXBuildFile; fileRef = 107923AE19A3217300C52AD6 /* chunked.c */; }; 1079244719A3265C00C52AD6 /* http1.c in Sources */ = {isa = PBXBuildFile; fileRef = 107923B119A3217300C52AD6 /* http1.c */; }; 1079244819A3265C00C52AD6 /* connection.c in Sources */ = {isa = PBXBuildFile; fileRef = 107923B319A3217300C52AD6 /* connection.c */; }; 1079244A19A3266700C52AD6 /* frame.c in Sources */ = {isa = PBXBuildFile; fileRef = 107923B819A3217300C52AD6 /* frame.c */; }; @@ -307,7 +305,6 @@ E9708B681E52C80F0029E0A5 /* throttle_resp.c in Sources */ = {isa = PBXBuildFile; fileRef = 10C45D4E1CFD15FA0096DB06 /* throttle_resp.c */; }; E9708B691E52C80F0029E0A5 /* http2_debug_state.c in Sources */ = {isa = PBXBuildFile; fileRef = 084FC7C31D54BB9200E89F66 /* http2_debug_state.c */; }; E9708B6A1E52C81D0029E0A5 /* access_log.c in Sources */ = {isa = PBXBuildFile; fileRef = 101B670B19ADD3380084A351 /* access_log.c */; }; - E9708B6B1E52C81D0029E0A5 /* chunked.c in Sources */ = {isa = PBXBuildFile; fileRef = 107923AE19A3217300C52AD6 /* chunked.c */; }; E9708B6C1E52C81D0029E0A5 /* compress.c in Sources */ = {isa = PBXBuildFile; fileRef = 1070866B1B787D06002B8F18 /* compress.c */; }; E9708B6D1E52C8250029E0A5 /* gzip.c in Sources */ = {isa = PBXBuildFile; fileRef = 107E340F1C7EB13F00AEF5F8 /* gzip.c */; }; E9708B721E52C82A0029E0A5 /* errordoc.c in Sources */ = {isa = PBXBuildFile; fileRef = 106C22FE1C05436100405689 /* errordoc.c */; }; @@ -654,7 +651,6 @@ 107923A319A3215F00C52AD6 /* websocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = websocket.h; sourceTree = ""; }; 107923A419A3215F00C52AD6 /* h2o.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = h2o.h; sourceTree = ""; }; 107923AB19A3216B00C52AD6 /* tokens.pl */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.perl; path = tokens.pl; sourceTree = ""; }; - 107923AE19A3217300C52AD6 /* chunked.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = chunked.c; sourceTree = ""; }; 107923AF19A3217300C52AD6 /* file.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = file.c; sourceTree = ""; }; 107923B019A3217300C52AD6 /* headers.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = headers.c; sourceTree = ""; }; 107923B119A3217300C52AD6 /* http1.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = http1.c; sourceTree = ""; }; @@ -1123,7 +1119,6 @@ children = ( 105534D91A3C7A2000627ECB /* configurator */, 101B670B19ADD3380084A351 /* access_log.c */, - 107923AE19A3217300C52AD6 /* chunked.c */, 7D9372FD202AC70F005FE6AB /* server_timing.c */, 1070866B1B787D06002B8F18 /* compress.c */, 107E340E1C7EB10700AEF5F8 /* compress */, @@ -2470,7 +2465,6 @@ 104B9A491A5F9638009EEE64 /* headers.c in Sources */, 106C22FF1C05436100405689 /* errordoc.c in Sources */, 10C45D4F1CFD15FA0096DB06 /* throttle_resp.c in Sources */, - 107923C219A3217300C52AD6 /* chunked.c in Sources */, 10A3D4081B50DAB700327CF9 /* close.c in Sources */, 105534EF1A440FC800627ECB /* config.c in Sources */, 10E299581A67E68500701AA6 /* scheduler.c in Sources */, @@ -2545,7 +2539,6 @@ 107D4D451B5880BC004A9B21 /* api.c in Sources */, E987E5F31FD8C04D00DE4346 /* compress_fragment.c in Sources */, 1079245119A3266700C52AD6 /* token.c in Sources */, - 1079244419A3265C00C52AD6 /* chunked.c in Sources */, 1070866A1B70A00F002B8F18 /* casper.c in Sources */, E987E5F51FD8C04D00DE4346 /* encode.c in Sources */, E987E5FC1FD8C04D00DE4346 /* utf8_util.c in Sources */, @@ -2664,7 +2657,6 @@ E9708B651E52C80F0029E0A5 /* redirect.c in Sources */, 08F320DD1E7A9CA60038FA5A /* redis.c in Sources */, 10756E2A1AC126420009BF57 /* api.c in Sources */, - E9708B6B1E52C81D0029E0A5 /* chunked.c in Sources */, 7D9372FE202AC70F005FE6AB /* server_timing.c in Sources */, 10756E2B1AC126420009BF57 /* dumper.c in Sources */, E9708B721E52C82A0029E0A5 /* errordoc.c in Sources */, diff --git a/include/h2o.h b/include/h2o.h index d8c697dc44..b9b1a038b4 100644 --- a/include/h2o.h +++ b/include/h2o.h @@ -1125,10 +1125,6 @@ struct st_h2o_req_t { * For delegated responses, redirect responses would be handled internally. */ unsigned char res_is_delegated : 1; - /** - * whether if the bytes sent is counted by ostreams other than final ostream - */ - unsigned char bytes_counted_by_ostream : 1; /** * set by the generator if the protocol handler should replay the request upon seeing 425 */ @@ -1758,13 +1754,6 @@ h2o_access_log_filehandle_t *h2o_access_log_open_handle(const char *path, const h2o_logger_t *h2o_access_log_register(h2o_pathconf_t *pathconf, h2o_access_log_filehandle_t *handle); void h2o_access_log_register_configurator(h2o_globalconf_t *conf); -/* lib/chunked.c */ - -/** - * registers the chunked encoding output filter (added by default) - */ -void h2o_chunked_register(h2o_pathconf_t *pathconf); - /* lib/handler/server_timing.c */ void h2o_server_timing_register(h2o_pathconf_t *pathconf, int enforce); void h2o_server_timing_register_configurator(h2o_globalconf_t *conf); diff --git a/lib/core/config.c b/lib/core/config.c index 2bd2762d6b..6d73eae294 100644 --- a/lib/core/config.c +++ b/lib/core/config.c @@ -133,7 +133,6 @@ void h2o_config_init_pathconf(h2o_pathconf_t *pathconf, h2o_globalconf_t *global { memset(pathconf, 0, sizeof(*pathconf)); pathconf->global = globalconf; - h2o_chunked_register(pathconf); if (path != NULL) pathconf->path = h2o_strdup(NULL, path, SIZE_MAX); h2o_mem_addref_shared(mimemap); diff --git a/lib/handler/chunked.c b/lib/handler/chunked.c deleted file mode 100644 index a6fc92cd73..0000000000 --- a/lib/handler/chunked.c +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (c) 2014 DeNA Co., Ltd. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ -#include -#include -#include -#include "h2o.h" - -typedef struct st_chunked_encoder_t { - h2o_ostream_t super; - char buf[64]; -} chunked_encoder_t; - -static void send_chunk(h2o_ostream_t *_self, h2o_req_t *req, h2o_iovec_t *inbufs, size_t inbufcnt, h2o_send_state_t state) -{ - chunked_encoder_t *self = (void *)_self; - h2o_iovec_t *outbufs = alloca(sizeof(h2o_iovec_t) * (inbufcnt + 2)); - size_t chunk_size, outbufcnt = 0, i; - - /* calc chunk size */ - chunk_size = 0; - for (i = 0; i != inbufcnt; ++i) - chunk_size += inbufs[i].len; - req->bytes_sent += chunk_size; - - /* create chunk header and output data */ - if (chunk_size != 0) { - outbufs[outbufcnt].base = self->buf; - outbufs[outbufcnt].len = sprintf(self->buf, "%zx\r\n", chunk_size); - assert(outbufs[outbufcnt].len < sizeof(self->buf)); - outbufcnt++; - memcpy(outbufs + outbufcnt, inbufs, sizeof(h2o_iovec_t) * inbufcnt); - outbufcnt += inbufcnt; - if (state != H2O_SEND_STATE_ERROR) { - outbufs[outbufcnt].base = "\r\n0\r\n\r\n"; - outbufs[outbufcnt].len = state == H2O_SEND_STATE_FINAL ? (req->send_server_timing_trailer ? 5 : 7) : 2; - outbufcnt++; - } - } else if (state == H2O_SEND_STATE_FINAL) { - outbufs[outbufcnt].base = "0\r\n\r\n"; - outbufs[outbufcnt].len = req->send_server_timing_trailer ? 3 : 5; - outbufcnt++; - } - - /* if state is error, send a broken chunk to pass the error down to the browser */ - if (state == H2O_SEND_STATE_ERROR) { - outbufs[outbufcnt].base = "\r\n1\r\n"; - outbufs[outbufcnt].len = 5; - outbufcnt++; - } - - h2o_ostream_send_next(&self->super, req, outbufs, outbufcnt, state); -} - -static void on_setup_ostream(h2o_filter_t *self, h2o_req_t *req, h2o_ostream_t **slot) -{ - chunked_encoder_t *encoder; - - /* TODO: make chunked filter a submodule of lib/http1.c so that we could eliminate this flag, protocol version checks, etc. */ - if (req->is_subrequest) - goto Next; - - /* do nothing with HTTP/2 */ - if (req->version >= 0x200) - goto Next; - /* do nothing with HTTP/1.0, but drop trailer flag */ - if (req->version != 0x101) - goto NextWithoutTrailer; - /* do nothing if content-length is known */ - if (req->res.content_length != SIZE_MAX) - goto NextWithoutTrailer; - /* RFC 2616 4.4 states that the following status codes (and response to a HEAD method) should not include message body */ - if ((100 <= req->res.status && req->res.status <= 199) || req->res.status == 204 || req->res.status == 304) - goto NextWithoutTrailer; - else if (h2o_memis(req->input.method.base, req->input.method.len, H2O_STRLIT("HEAD"))) - goto NextWithoutTrailer; - - /* we cannot handle certain responses (like 101 switching protocols) */ - if (req->res.status != 200) { - req->http1_is_persistent = 0; - } - - /* set content-encoding header */ - h2o_add_header(&req->pool, &req->res.headers, H2O_TOKEN_TRANSFER_ENCODING, NULL, H2O_STRLIT("chunked")); - - /* set the flag that tells finalostream that req->bytes_sent is already counted */ - req->bytes_counted_by_ostream = 1; - - /* setup filter */ - encoder = (void *)h2o_add_ostream(req, H2O_ALIGNOF(*encoder), sizeof(*encoder), slot); - encoder->super.do_send = send_chunk; - slot = &encoder->super.next; - goto Next; - -NextWithoutTrailer: - req->send_server_timing_trailer = 0; -Next: - h2o_setup_next_ostream(req, slot); -} - -void h2o_chunked_register(h2o_pathconf_t *pathconf) -{ - h2o_filter_t *self = h2o_create_filter(pathconf, sizeof(*self)); - self->on_setup_ostream = on_setup_ostream; -} diff --git a/lib/http1.c b/lib/http1.c index d65f92a7aa..5d4102570e 100644 --- a/lib/http1.c +++ b/lib/http1.c @@ -34,6 +34,10 @@ struct st_h2o_http1_finalostream_t { h2o_ostream_t super; int sent_headers; + struct { + unsigned char enabled : 1; + char buf[64]; + } chunked; struct { void *buf; h2o_ostream_pull_cb cb; @@ -756,11 +760,60 @@ static void on_delayed_send_complete(h2o_timeout_entry_t *entry) on_send_complete(conn->sock, 0); } +static int should_use_chunked_encoding(h2o_req_t *req) +{ + if (req->version != 0x101) + return 0; + /* do nothing if content-length is known */ + if (req->res.content_length != SIZE_MAX) + return 0; + /* RFC 2616 4.4 states that the following status codes (and response to a HEAD method) should not include message body */ + if ((100 <= req->res.status && req->res.status <= 199) || req->res.status == 204 || req->res.status == 304) + return 0; + if (h2o_memis(req->input.method.base, req->input.method.len, H2O_STRLIT("HEAD"))) + return 0; + + return 1; +} + +static size_t encode_chunked(struct st_h2o_http1_finalostream_t *self, h2o_req_t *req, h2o_iovec_t *inbufs, size_t inbufcnt, h2o_iovec_t *outbufs, h2o_send_state_t state, size_t chunk_size) +{ + size_t outbufcnt = 0; + + /* create chunk header and output data */ + if (chunk_size != 0) { + outbufs[outbufcnt].base = self->chunked.buf; + outbufs[outbufcnt].len = sprintf(self->chunked.buf, "%zx\r\n", chunk_size); + assert(outbufs[outbufcnt].len < sizeof(self->chunked.buf)); + outbufcnt++; + memcpy(outbufs + outbufcnt, inbufs, sizeof(h2o_iovec_t) * inbufcnt); + outbufcnt += inbufcnt; + if (state != H2O_SEND_STATE_ERROR) { + outbufs[outbufcnt].base = "\r\n0\r\n\r\n"; + outbufs[outbufcnt].len = state == H2O_SEND_STATE_FINAL ? (req->send_server_timing_trailer ? 5 : 7) : 2; /* FIXME */ + outbufcnt++; + } + } else if (state == H2O_SEND_STATE_FINAL) { + outbufs[outbufcnt].base = "0\r\n\r\n"; + outbufs[outbufcnt].len = req->send_server_timing_trailer ? 3 : 5; + outbufcnt++; + } + + /* if state is error, send a broken chunk to pass the error down to the browser */ + if (state == H2O_SEND_STATE_ERROR) { + outbufs[outbufcnt].base = "\r\n1\r\n"; + outbufs[outbufcnt].len = 5; + outbufcnt++; + } + + return outbufcnt; +} + void finalostream_send(h2o_ostream_t *_self, h2o_req_t *req, h2o_iovec_t *inbufs, size_t inbufcnt, h2o_send_state_t send_state) { struct st_h2o_http1_finalostream_t *self = (void *)_self; struct st_h2o_http1_conn_t *conn = (struct st_h2o_http1_conn_t *)req->conn; - h2o_iovec_t *bufs = alloca(sizeof(h2o_iovec_t) * (inbufcnt + 1)); + h2o_iovec_t *bufs = alloca(sizeof(h2o_iovec_t) * (inbufcnt + 1 + 2)); /* 1 for header, 2 for chunked encoding */ int i; int bufcnt = 0; @@ -775,16 +828,32 @@ void finalostream_send(h2o_ostream_t *_self, h2o_req_t *req, h2o_iovec_t *inbufs } /* count bytes_sent if other ostreams haven't counted */ - if (req->bytes_counted_by_ostream == 0) { - for (i = 0; i != inbufcnt; ++i) { - req->bytes_sent += inbufs[i].len; - } + size_t bytes_to_be_sent = 0; + for (i = 0; i != inbufcnt; ++i) { + bytes_to_be_sent += inbufs[i].len; } + req->bytes_sent += bytes_to_be_sent; if (!self->sent_headers) { conn->req.timestamps.response_start_at = h2o_gettimeofday(conn->super.ctx->loop); if (conn->req.send_server_timing_header) h2o_add_server_timing_header(&conn->req); + + if (should_use_chunked_encoding(req)) { + self->chunked.enabled = 1; + + /* TODO should this be move to somewhere? */ + /* we cannot handle certain responses (like 101 switching protocols) */ + if (req->res.status != 200) { + req->http1_is_persistent = 0; + } + + /* set transfer-encoding header */ + h2o_add_header(&req->pool, &req->res.headers, H2O_TOKEN_TRANSFER_ENCODING, NULL, H2O_STRLIT("chunked")); + } else { + req->send_server_timing_trailer = 0; + } + /* build headers and send */ const char *connection = req->http1_is_persistent ? "keep-alive" : "close"; bufs[bufcnt].base = h2o_mem_alloc_pool( @@ -794,6 +863,13 @@ void finalostream_send(h2o_ostream_t *_self, h2o_req_t *req, h2o_iovec_t *inbufs ++bufcnt; self->sent_headers = 1; } + + if (self->chunked.enabled) { + h2o_iovec_t *encoded = alloca(sizeof(h2o_iovec_t) * (inbufcnt + 2)); + inbufcnt = encode_chunked(self, req, inbufs, inbufcnt, encoded, send_state, bytes_to_be_sent); + inbufs = encoded; + } + memcpy(bufs + bufcnt, inbufs, sizeof(h2o_iovec_t) * inbufcnt); bufcnt += inbufcnt; From e152ff4b951b32e7cd09baa50141469ee698523e Mon Sep 17 00:00:00 2001 From: Kazuho Oku Date: Wed, 1 Aug 2018 11:12:57 +0900 Subject: [PATCH 02/10] simplify, by omitting the creating of a temporary list, and by making "encode_chunked" concentrate on building the prefix and suffixes --- lib/http1.c | 45 ++++++++++++++++++++------------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/lib/http1.c b/lib/http1.c index 5d4102570e..e165dca633 100644 --- a/lib/http1.c +++ b/lib/http1.c @@ -36,7 +36,7 @@ struct st_h2o_http1_finalostream_t { int sent_headers; struct { unsigned char enabled : 1; - char buf[64]; + char buf[sizeof(size_t) * 2 + sizeof("\r\n")]; } chunked; struct { void *buf; @@ -776,44 +776,37 @@ static int should_use_chunked_encoding(h2o_req_t *req) return 1; } -static size_t encode_chunked(struct st_h2o_http1_finalostream_t *self, h2o_req_t *req, h2o_iovec_t *inbufs, size_t inbufcnt, h2o_iovec_t *outbufs, h2o_send_state_t state, size_t chunk_size) +static void encode_chunked(h2o_iovec_t *prefix, h2o_iovec_t *suffix, h2o_send_state_t state, size_t chunk_size, + int send_server_timing_trailer, char *buffer) { - size_t outbufcnt = 0; + *prefix = h2o_iovec_init(NULL, 0); + *suffix = h2o_iovec_init(NULL, 0); /* create chunk header and output data */ if (chunk_size != 0) { - outbufs[outbufcnt].base = self->chunked.buf; - outbufs[outbufcnt].len = sprintf(self->chunked.buf, "%zx\r\n", chunk_size); - assert(outbufs[outbufcnt].len < sizeof(self->chunked.buf)); - outbufcnt++; - memcpy(outbufs + outbufcnt, inbufs, sizeof(h2o_iovec_t) * inbufcnt); - outbufcnt += inbufcnt; + prefix->base = buffer; + prefix->len = sprintf(buffer, "%zx\r\n", chunk_size); if (state != H2O_SEND_STATE_ERROR) { - outbufs[outbufcnt].base = "\r\n0\r\n\r\n"; - outbufs[outbufcnt].len = state == H2O_SEND_STATE_FINAL ? (req->send_server_timing_trailer ? 5 : 7) : 2; /* FIXME */ - outbufcnt++; + suffix->base = "\r\n0\r\n\r\n"; + suffix->len = state == H2O_SEND_STATE_FINAL ? (send_server_timing_trailer ? 5 : 7) : 2; /* FIXME */ } } else if (state == H2O_SEND_STATE_FINAL) { - outbufs[outbufcnt].base = "0\r\n\r\n"; - outbufs[outbufcnt].len = req->send_server_timing_trailer ? 3 : 5; - outbufcnt++; + suffix->base = "0\r\n\r\n"; + suffix->len = send_server_timing_trailer ? 3 : 5; } /* if state is error, send a broken chunk to pass the error down to the browser */ if (state == H2O_SEND_STATE_ERROR) { - outbufs[outbufcnt].base = "\r\n1\r\n"; - outbufs[outbufcnt].len = 5; - outbufcnt++; + suffix->base = "\r\n1\r\n"; + suffix->len = 5; } - - return outbufcnt; } void finalostream_send(h2o_ostream_t *_self, h2o_req_t *req, h2o_iovec_t *inbufs, size_t inbufcnt, h2o_send_state_t send_state) { struct st_h2o_http1_finalostream_t *self = (void *)_self; struct st_h2o_http1_conn_t *conn = (struct st_h2o_http1_conn_t *)req->conn; - h2o_iovec_t *bufs = alloca(sizeof(h2o_iovec_t) * (inbufcnt + 1 + 2)); /* 1 for header, 2 for chunked encoding */ + h2o_iovec_t *bufs = alloca(sizeof(h2o_iovec_t) * (inbufcnt + 1 + 2)) /* 1 for header, 2 for chunked encoding */, chunked_suffix; int i; int bufcnt = 0; @@ -865,13 +858,15 @@ void finalostream_send(h2o_ostream_t *_self, h2o_req_t *req, h2o_iovec_t *inbufs } if (self->chunked.enabled) { - h2o_iovec_t *encoded = alloca(sizeof(h2o_iovec_t) * (inbufcnt + 2)); - inbufcnt = encode_chunked(self, req, inbufs, inbufcnt, encoded, send_state, bytes_to_be_sent); - inbufs = encoded; + encode_chunked(bufs + bufcnt, &chunked_suffix, send_state, bytes_to_be_sent, req->send_server_timing_trailer, + self->chunked.buf); + if (bufs[bufcnt].len != 0) + ++bufcnt; } - memcpy(bufs + bufcnt, inbufs, sizeof(h2o_iovec_t) * inbufcnt); bufcnt += inbufcnt; + if (self->chunked.enabled && chunked_suffix.len != 0) + bufs[bufcnt++] = chunked_suffix; if (send_state == H2O_SEND_STATE_ERROR) { conn->req.http1_is_persistent = 0; From f40d93bea1800158db31661f901211bc492ec981 Mon Sep 17 00:00:00 2001 From: Ichito Nagata Date: Thu, 2 Aug 2018 13:25:31 +0900 Subject: [PATCH 03/10] add an assertion: see https://github.com/h2o/h2o/pull/1819#pullrequestreview-142221890 --- lib/http1.c | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/http1.c b/lib/http1.c index e165dca633..ad50f09219 100644 --- a/lib/http1.c +++ b/lib/http1.c @@ -728,6 +728,7 @@ static void finalostream_start_pull(h2o_ostream_t *_self, h2o_ostream_pull_cb cb assert(conn->req._ostr_top == &conn->_ostr_final.super); assert(!conn->_ostr_final.sent_headers); + assert(conn->req.res.content_length != SIZE_MAX); conn->req.timestamps.response_start_at = h2o_gettimeofday(conn->super.ctx->loop); if (conn->req.send_server_timing_header) From d60f7b1e95978e778f24ad0f118c54092e02b3d6 Mon Sep 17 00:00:00 2001 From: Ichito Nagata Date: Thu, 2 Aug 2018 13:28:21 +0900 Subject: [PATCH 04/10] drop off the chunked flag explicitly --- lib/http1.c | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/http1.c b/lib/http1.c index ad50f09219..0446e51e2c 100644 --- a/lib/http1.c +++ b/lib/http1.c @@ -845,6 +845,7 @@ void finalostream_send(h2o_ostream_t *_self, h2o_req_t *req, h2o_iovec_t *inbufs /* set transfer-encoding header */ h2o_add_header(&req->pool, &req->res.headers, H2O_TOKEN_TRANSFER_ENCODING, NULL, H2O_STRLIT("chunked")); } else { + self->chunked.enabled = 0; req->send_server_timing_trailer = 0; } From e4ef3a8bd4659b6411d50e297c7c7fbe454fe1cf Mon Sep 17 00:00:00 2001 From: Ichito Nagata Date: Thu, 2 Aug 2018 16:11:54 +0900 Subject: [PATCH 05/10] apply chunked encoding to pull mode responses collectly --- lib/http1.c | 147 +++++++++++++++++++++++++++++----------------------- 1 file changed, 83 insertions(+), 64 deletions(-) diff --git a/lib/http1.c b/lib/http1.c index 0446e51e2c..e5a2d9daba 100644 --- a/lib/http1.c +++ b/lib/http1.c @@ -698,26 +698,100 @@ static size_t flatten_headers(char *buf, h2o_req_t *req, const char *connection) return dst - buf; } +static int should_use_chunked_encoding(h2o_req_t *req) +{ + if (req->version != 0x101) + return 0; + /* do nothing if content-length is known */ + if (req->res.content_length != SIZE_MAX) + return 0; + /* RFC 2616 4.4 states that the following status codes (and response to a HEAD method) should not include message body */ + if ((100 <= req->res.status && req->res.status <= 199) || req->res.status == 204 || req->res.status == 304) + return 0; + if (h2o_memis(req->input.method.base, req->input.method.len, H2O_STRLIT("HEAD"))) + return 0; + + return 1; +} + +static void setup_chunked(struct st_h2o_http1_finalostream_t *self, h2o_req_t *req) +{ + if (should_use_chunked_encoding(req)) { + self->chunked.enabled = 1; + + /* TODO should this be move to somewhere? */ + /* we cannot handle certain responses (like 101 switching protocols) */ + if (req->res.status != 200) { + req->http1_is_persistent = 0; + } + + /* set transfer-encoding header */ + h2o_add_header(&req->pool, &req->res.headers, H2O_TOKEN_TRANSFER_ENCODING, NULL, H2O_STRLIT("chunked")); + } else { + self->chunked.enabled = 0; + req->send_server_timing_trailer = 0; + } +} + +static void encode_chunked(h2o_iovec_t *prefix, h2o_iovec_t *suffix, h2o_send_state_t state, size_t chunk_size, + int send_server_timing_trailer, char *buffer) +{ + *prefix = h2o_iovec_init(NULL, 0); + *suffix = h2o_iovec_init(NULL, 0); + + /* create chunk header and output data */ + if (chunk_size != 0) { + prefix->base = buffer; + prefix->len = sprintf(buffer, "%zx\r\n", chunk_size); + if (state != H2O_SEND_STATE_ERROR) { + suffix->base = "\r\n0\r\n\r\n"; + suffix->len = state == H2O_SEND_STATE_FINAL ? (send_server_timing_trailer ? 5 : 7) : 2; /* FIXME */ + } + } else if (state == H2O_SEND_STATE_FINAL) { + suffix->base = "0\r\n\r\n"; + suffix->len = send_server_timing_trailer ? 3 : 5; + } + + /* if state is error, send a broken chunk to pass the error down to the browser */ + if (state == H2O_SEND_STATE_ERROR) { + suffix->base = "\r\n1\r\n"; + suffix->len = 5; + } +} + static void proceed_pull(struct st_h2o_http1_conn_t *conn, size_t nfilled) { - h2o_iovec_t buf = {conn->_ostr_final.pull.buf, nfilled}; + h2o_iovec_t bufs[4]; + size_t bufcnt = 0; h2o_send_state_t send_state; + h2o_iovec_t prefix = h2o_iovec_init(NULL, 0), suffix = h2o_iovec_init(NULL, 0); + + if (nfilled != 0) + bufs[bufcnt++] = h2o_iovec_init(conn->_ostr_final.pull.buf, nfilled); - if (buf.len < MAX_PULL_BUF_SZ) { - h2o_iovec_t cbuf = {buf.base + buf.len, MAX_PULL_BUF_SZ - buf.len}; + if (nfilled < MAX_PULL_BUF_SZ) { + h2o_iovec_t cbuf = {conn->_ostr_final.pull.buf + nfilled, MAX_PULL_BUF_SZ - nfilled}; send_state = h2o_pull(&conn->req, conn->_ostr_final.pull.cb, &cbuf); + conn->req.bytes_sent += cbuf.len; + + if (conn->_ostr_final.chunked.enabled) + encode_chunked(&prefix, &suffix, send_state, cbuf.len, conn->req.send_server_timing_trailer, conn->_ostr_final.chunked.buf); + if (prefix.len != 0) + bufs[bufcnt++] = prefix; + bufs[bufcnt++] = cbuf; + if (suffix.len != 0) + bufs[bufcnt++] = suffix; + if (send_state == H2O_SEND_STATE_ERROR) { conn->req.http1_is_persistent = 0; conn->req.send_server_timing_trailer = 0; } - buf.len += cbuf.len; - conn->req.bytes_sent += cbuf.len; } else { send_state = H2O_SEND_STATE_IN_PROGRESS; } /* write */ - h2o_socket_write(conn->sock, &buf, 1, h2o_send_state_is_in_progress(send_state) ? on_send_next_pull : on_send_complete); + h2o_socket_write(conn->sock, bufs, bufcnt, h2o_send_state_is_in_progress(send_state) ? on_send_next_pull : on_send_complete); } static void finalostream_start_pull(h2o_ostream_t *_self, h2o_ostream_pull_cb cb) @@ -728,12 +802,13 @@ static void finalostream_start_pull(h2o_ostream_t *_self, h2o_ostream_pull_cb cb assert(conn->req._ostr_top == &conn->_ostr_final.super); assert(!conn->_ostr_final.sent_headers); - assert(conn->req.res.content_length != SIZE_MAX); conn->req.timestamps.response_start_at = h2o_gettimeofday(conn->super.ctx->loop); if (conn->req.send_server_timing_header) h2o_add_server_timing_header(&conn->req); + setup_chunked(&conn->_ostr_final, &conn->req); + /* register the pull callback */ conn->_ostr_final.pull.cb = cb; @@ -761,48 +836,6 @@ static void on_delayed_send_complete(h2o_timeout_entry_t *entry) on_send_complete(conn->sock, 0); } -static int should_use_chunked_encoding(h2o_req_t *req) -{ - if (req->version != 0x101) - return 0; - /* do nothing if content-length is known */ - if (req->res.content_length != SIZE_MAX) - return 0; - /* RFC 2616 4.4 states that the following status codes (and response to a HEAD method) should not include message body */ - if ((100 <= req->res.status && req->res.status <= 199) || req->res.status == 204 || req->res.status == 304) - return 0; - if (h2o_memis(req->input.method.base, req->input.method.len, H2O_STRLIT("HEAD"))) - return 0; - - return 1; -} - -static void encode_chunked(h2o_iovec_t *prefix, h2o_iovec_t *suffix, h2o_send_state_t state, size_t chunk_size, - int send_server_timing_trailer, char *buffer) -{ - *prefix = h2o_iovec_init(NULL, 0); - *suffix = h2o_iovec_init(NULL, 0); - - /* create chunk header and output data */ - if (chunk_size != 0) { - prefix->base = buffer; - prefix->len = sprintf(buffer, "%zx\r\n", chunk_size); - if (state != H2O_SEND_STATE_ERROR) { - suffix->base = "\r\n0\r\n\r\n"; - suffix->len = state == H2O_SEND_STATE_FINAL ? (send_server_timing_trailer ? 5 : 7) : 2; /* FIXME */ - } - } else if (state == H2O_SEND_STATE_FINAL) { - suffix->base = "0\r\n\r\n"; - suffix->len = send_server_timing_trailer ? 3 : 5; - } - - /* if state is error, send a broken chunk to pass the error down to the browser */ - if (state == H2O_SEND_STATE_ERROR) { - suffix->base = "\r\n1\r\n"; - suffix->len = 5; - } -} - void finalostream_send(h2o_ostream_t *_self, h2o_req_t *req, h2o_iovec_t *inbufs, size_t inbufcnt, h2o_send_state_t send_state) { struct st_h2o_http1_finalostream_t *self = (void *)_self; @@ -833,21 +866,7 @@ void finalostream_send(h2o_ostream_t *_self, h2o_req_t *req, h2o_iovec_t *inbufs if (conn->req.send_server_timing_header) h2o_add_server_timing_header(&conn->req); - if (should_use_chunked_encoding(req)) { - self->chunked.enabled = 1; - - /* TODO should this be move to somewhere? */ - /* we cannot handle certain responses (like 101 switching protocols) */ - if (req->res.status != 200) { - req->http1_is_persistent = 0; - } - - /* set transfer-encoding header */ - h2o_add_header(&req->pool, &req->res.headers, H2O_TOKEN_TRANSFER_ENCODING, NULL, H2O_STRLIT("chunked")); - } else { - self->chunked.enabled = 0; - req->send_server_timing_trailer = 0; - } + setup_chunked(self, req); /* build headers and send */ const char *connection = req->http1_is_persistent ? "keep-alive" : "close"; From c151142a8a15e5113f088defaa21f5abf92b26a7 Mon Sep 17 00:00:00 2001 From: Ichito Nagata Date: Thu, 2 Aug 2018 16:12:15 +0900 Subject: [PATCH 06/10] remove needless comment --- lib/http1.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/http1.c b/lib/http1.c index e5a2d9daba..cc30fee9ff 100644 --- a/lib/http1.c +++ b/lib/http1.c @@ -745,7 +745,7 @@ static void encode_chunked(h2o_iovec_t *prefix, h2o_iovec_t *suffix, h2o_send_st prefix->len = sprintf(buffer, "%zx\r\n", chunk_size); if (state != H2O_SEND_STATE_ERROR) { suffix->base = "\r\n0\r\n\r\n"; - suffix->len = state == H2O_SEND_STATE_FINAL ? (send_server_timing_trailer ? 5 : 7) : 2; /* FIXME */ + suffix->len = state == H2O_SEND_STATE_FINAL ? (send_server_timing_trailer ? 5 : 7) : 2; } } else if (state == H2O_SEND_STATE_FINAL) { suffix->base = "0\r\n\r\n"; From 790c883040a20f2bbf70f9e70a7f40e3ba783b89 Mon Sep 17 00:00:00 2001 From: Ichito Nagata Date: Thu, 2 Aug 2018 16:29:50 +0900 Subject: [PATCH 07/10] only close connection if the request was broken --- lib/http1.c | 9 --------- 1 file changed, 9 deletions(-) diff --git a/lib/http1.c b/lib/http1.c index cc30fee9ff..28d18e287d 100644 --- a/lib/http1.c +++ b/lib/http1.c @@ -169,7 +169,6 @@ static void process_request(struct st_h2o_http1_conn_t *conn) conn->_req_entity_reader = NULL; \ set_timeout(conn, NULL, NULL); \ h2o_socket_read_stop(conn->sock); \ - conn->req.http1_is_persistent = 0; \ conn->super.ctx->emitted_error_status[H2O_STATUS_ERROR_##status_]++; \ h2o_send_error_generic(&conn->req, status_, reason, body, H2O_SEND_ERROR_HTTP1_CLOSE_CONNECTION); \ } @@ -718,14 +717,6 @@ static void setup_chunked(struct st_h2o_http1_finalostream_t *self, h2o_req_t *r { if (should_use_chunked_encoding(req)) { self->chunked.enabled = 1; - - /* TODO should this be move to somewhere? */ - /* we cannot handle certain responses (like 101 switching protocols) */ - if (req->res.status != 200) { - req->http1_is_persistent = 0; - } - - /* set transfer-encoding header */ h2o_add_header(&req->pool, &req->res.headers, H2O_TOKEN_TRANSFER_ENCODING, NULL, H2O_STRLIT("chunked")); } else { self->chunked.enabled = 0; From 982f6065859c749c28899d99a68cba894be7fb63 Mon Sep 17 00:00:00 2001 From: Ichito Nagata Date: Sat, 4 Aug 2018 04:57:48 +0900 Subject: [PATCH 08/10] add test for chunked encoding in pull mode --- t/40chunked.t | 64 ++++++++++++++++++++++++++++++++++++++++++ t/assets/upstream.psgi | 4 ++- 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 t/40chunked.t diff --git a/t/40chunked.t b/t/40chunked.t new file mode 100644 index 0000000000..88d5144836 --- /dev/null +++ b/t/40chunked.t @@ -0,0 +1,64 @@ +use strict; +use warnings; +use Net::EmptyPort qw(check_port empty_port); +use Test::More; +use t::Util; + +plan skip_all => 'nc not found' + unless prog_exists('nc'); + +sub fetch { + my ($server, $path) = @_; + my $resp = `echo 'GET $path HTTP/1.1\r\n\r\n' | nc 127.0.0.1 $server->{port}`; + my ($headers, $body) = split(/^\r\n/m, $resp, 2); + return ($headers, $body); +} + +subtest 'push mode' => sub { + plan skip_all => 'plackup not found' + unless prog_exists('plackup'); + plan skip_all => 'Starlet not found' + unless system('perl -MStarlet /dev/null > /dev/null 2>&1') == 0; + my $upstream_port = empty_port(); + my $upstream = spawn_server( + argv => [ qw(plackup -s Starlet --access-log /dev/null --listen), $upstream_port, ASSETS_DIR . "/upstream.psgi" ], + is_ready => sub { check_port($upstream_port) }, + ); + my $server = spawn_h2o(<< "EOT"); +hosts: + default: + paths: + "/": + proxy.reverse.url: http://127.0.0.1:$upstream_port +EOT + my ($headers, $body) = fetch($server, '/streaming-body?sleep=0'); + like $headers, qr/^transfer-encoding: chunked\r$/m; + my @list = split("\r\n", $body); + my $content = ''; + while (scalar(@list) >= 2) { + my ($len, $data) = splice(@list, 0, 2); + is length($data), $len; + $content .= $data; + } + is $content, join("", (1..30)); +}; + +subtest 'pull mode' => sub { + # server-timing forces chunked encoding in enforce mode + my $server = spawn_h2o(<< "EOT"); +hosts: + default: + paths: + "/": + file.dir: @{[ DOC_ROOT ]} + server-timing: enforce +EOT + my ($headers, $body) = fetch($server, '/'); + like $headers, qr/^transfer-encoding: chunked\r$/m; + my $content = "hello\n"; + like $body, qr/^6\r\n$content\r\n0\r\nserver-timing: .+?\r\n\r\n$/; + pass; +}; + +done_testing; + diff --git a/t/assets/upstream.psgi b/t/assets/upstream.psgi index 1343fa9af6..cc6d587118 100644 --- a/t/assets/upstream.psgi +++ b/t/assets/upstream.psgi @@ -159,11 +159,13 @@ builder { }; mount "/streaming-body" => sub { my $env = shift; + my $query = Plack::Request->new($env)->query_parameters; + my $sleep = $query->{sleep} // 0.1; return sub { my $responder = shift; my $writer = $responder->([ 200, [ 'content-type' => 'text/plain' ] ]); for my $i (1..30) { - sleep 0.1; + sleep $sleep; $writer->write($i); } $writer->close; From c5ab4150323426ebc266b6bc0adab1f6ec514a92 Mon Sep 17 00:00:00 2001 From: Kazuho Oku Date: Wed, 8 Aug 2018 16:16:16 +0900 Subject: [PATCH 09/10] optimize for pull+content-length case --- lib/http1.c | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/http1.c b/lib/http1.c index 28d18e287d..e25563c6f7 100644 --- a/lib/http1.c +++ b/lib/http1.c @@ -764,15 +764,18 @@ static void proceed_pull(struct st_h2o_http1_conn_t *conn, size_t nfilled) h2o_iovec_t cbuf = {conn->_ostr_final.pull.buf + nfilled, MAX_PULL_BUF_SZ - nfilled}; send_state = h2o_pull(&conn->req, conn->_ostr_final.pull.cb, &cbuf); conn->req.bytes_sent += cbuf.len; - - if (conn->_ostr_final.chunked.enabled) + if (conn->_ostr_final.chunked.enabled) { encode_chunked(&prefix, &suffix, send_state, cbuf.len, conn->req.send_server_timing_trailer, conn->_ostr_final.chunked.buf); - if (prefix.len != 0) - bufs[bufcnt++] = prefix; - bufs[bufcnt++] = cbuf; - if (suffix.len != 0) - bufs[bufcnt++] = suffix; - + if (prefix.len != 0) + bufs[bufcnt++] = prefix; + bufs[bufcnt++] = cbuf; + if (suffix.len != 0) + bufs[bufcnt++] = suffix; + } else if (nfilled != 0) { + bufs[bufcnt - 1].len += cbuf.len; + } else { + bufs[bufcnt++] = cbuf; + } if (send_state == H2O_SEND_STATE_ERROR) { conn->req.http1_is_persistent = 0; conn->req.send_server_timing_trailer = 0; From 61efbcb3d475ca370e23567817fb55704fe89bdf Mon Sep 17 00:00:00 2001 From: Kazuho Oku Date: Wed, 8 Aug 2018 16:23:36 +0900 Subject: [PATCH 10/10] reduce the size of ostr_final to speed up the initialization of the object --- lib/http1.c | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/lib/http1.c b/lib/http1.c index e25563c6f7..c74bd89f3e 100644 --- a/lib/http1.c +++ b/lib/http1.c @@ -34,10 +34,7 @@ struct st_h2o_http1_finalostream_t { h2o_ostream_t super; int sent_headers; - struct { - unsigned char enabled : 1; - char buf[sizeof(size_t) * 2 + sizeof("\r\n")]; - } chunked; + char *chunked_buf; /* buffer used for chunked-encoding (NULL unless chunked encoding is used) */ struct { void *buf; h2o_ostream_pull_cb cb; @@ -716,10 +713,9 @@ static int should_use_chunked_encoding(h2o_req_t *req) static void setup_chunked(struct st_h2o_http1_finalostream_t *self, h2o_req_t *req) { if (should_use_chunked_encoding(req)) { - self->chunked.enabled = 1; h2o_add_header(&req->pool, &req->res.headers, H2O_TOKEN_TRANSFER_ENCODING, NULL, H2O_STRLIT("chunked")); + self->chunked_buf = h2o_mem_alloc_pool_aligned(&req->pool, 1, sizeof(size_t) * 2 + sizeof("\r\n")); } else { - self->chunked.enabled = 0; req->send_server_timing_trailer = 0; } } @@ -764,8 +760,9 @@ static void proceed_pull(struct st_h2o_http1_conn_t *conn, size_t nfilled) h2o_iovec_t cbuf = {conn->_ostr_final.pull.buf + nfilled, MAX_PULL_BUF_SZ - nfilled}; send_state = h2o_pull(&conn->req, conn->_ostr_final.pull.cb, &cbuf); conn->req.bytes_sent += cbuf.len; - if (conn->_ostr_final.chunked.enabled) { - encode_chunked(&prefix, &suffix, send_state, cbuf.len, conn->req.send_server_timing_trailer, conn->_ostr_final.chunked.buf); + if (conn->_ostr_final.chunked_buf != NULL) { + encode_chunked(&prefix, &suffix, send_state, cbuf.len, conn->req.send_server_timing_trailer, + conn->_ostr_final.chunked_buf); if (prefix.len != 0) bufs[bufcnt++] = prefix; bufs[bufcnt++] = cbuf; @@ -872,15 +869,15 @@ void finalostream_send(h2o_ostream_t *_self, h2o_req_t *req, h2o_iovec_t *inbufs self->sent_headers = 1; } - if (self->chunked.enabled) { + if (self->chunked_buf != NULL) { encode_chunked(bufs + bufcnt, &chunked_suffix, send_state, bytes_to_be_sent, req->send_server_timing_trailer, - self->chunked.buf); + self->chunked_buf); if (bufs[bufcnt].len != 0) ++bufcnt; } memcpy(bufs + bufcnt, inbufs, sizeof(h2o_iovec_t) * inbufcnt); bufcnt += inbufcnt; - if (self->chunked.enabled && chunked_suffix.len != 0) + if (self->chunked_buf != NULL && chunked_suffix.len != 0) bufs[bufcnt++] = chunked_suffix; if (send_state == H2O_SEND_STATE_ERROR) {