blob: a67d025fa883c299f3afd8ee08e583ff01de5847 [file] [log] [blame]
/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <assert.h>
#include <ap_mpm.h>
#include <httpd.h>
#include <http_core.h>
#include <http_log.h>
#include <http_connection.h>
#include "h2_private.h"
#include "h2_bucket_eoc.h"
#include "h2_config.h"
#include "h2_conn_io.h"
#include "h2_h2.h"
#include "h2_util.h"
#define TLS_DATA_MAX (16*1024)
/* Calculated like this: assuming MTU 1500 bytes
* 1500 - 40 (IP) - 20 (TCP) - 40 (TCP options)
* - TLS overhead (60-100)
* ~= 1300 bytes */
#define WRITE_SIZE_INITIAL 1300
/* Calculated like this: max TLS record size 16*1024
* - 40 (IP) - 20 (TCP) - 40 (TCP options)
* - TLS overhead (60-100)
* which seems to create less TCP packets overall
*/
#define WRITE_SIZE_MAX (TLS_DATA_MAX - 100)
#define WRITE_BUFFER_SIZE (8*WRITE_SIZE_MAX)
apr_status_t h2_conn_io_init(h2_conn_io *io, conn_rec *c,
const h2_config *cfg,
apr_pool_t *pool)
{
io->connection = c;
io->input = apr_brigade_create(pool, c->bucket_alloc);
io->output = apr_brigade_create(pool, c->bucket_alloc);
io->buflen = 0;
io->is_tls = h2_h2_is_tls(c);
io->buffer_output = io->is_tls;
if (io->buffer_output) {
io->bufsize = WRITE_BUFFER_SIZE;
io->buffer = apr_pcalloc(pool, io->bufsize);
}
else {
io->bufsize = 0;
}
if (io->is_tls) {
/* That is where we start with,
* see https://issues.apache.org/jira/browse/TS-2503 */
io->warmup_size = h2_config_geti64(cfg, H2_CONF_TLS_WARMUP_SIZE);
io->cooldown_usecs = (h2_config_geti(cfg, H2_CONF_TLS_COOLDOWN_SECS)
* APR_USEC_PER_SEC);
io->write_size = WRITE_SIZE_INITIAL;
}
else {
io->warmup_size = 0;
io->cooldown_usecs = 0;
io->write_size = io->bufsize;
}
if (APLOGctrace1(c)) {
ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, io->connection,
"h2_conn_io(%ld): init, buffering=%d, warmup_size=%ld, cd_secs=%f",
io->connection->id, io->buffer_output, (long)io->warmup_size,
((float)io->cooldown_usecs/APR_USEC_PER_SEC));
}
return APR_SUCCESS;
}
int h2_conn_io_is_buffered(h2_conn_io *io)
{
return io->bufsize > 0;
}
static apr_status_t h2_conn_io_bucket_read(h2_conn_io *io,
apr_read_type_e block,
h2_conn_io_on_read_cb on_read_cb,
void *puser, int *pdone)
{
apr_status_t status = APR_SUCCESS;
apr_size_t readlen = 0;
*pdone = 0;
while (status == APR_SUCCESS && !*pdone
&& !APR_BRIGADE_EMPTY(io->input)) {
apr_bucket* bucket = APR_BRIGADE_FIRST(io->input);
if (APR_BUCKET_IS_METADATA(bucket)) {
/* we do nothing regarding any meta here */
}
else {
const char *bucket_data = NULL;
apr_size_t bucket_length = 0;
status = apr_bucket_read(bucket, &bucket_data,
&bucket_length, block);
if (status == APR_SUCCESS && bucket_length > 0) {
apr_size_t consumed = 0;
if (APLOGctrace2(io->connection)) {
char buffer[32];
h2_util_hex_dump(buffer, sizeof(buffer)/sizeof(buffer[0]),
bucket_data, bucket_length);
ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, io->connection,
"h2_conn_io(%ld): read %d bytes: %s",
io->connection->id, (int)bucket_length, buffer);
}
status = on_read_cb(bucket_data, bucket_length, &consumed,
pdone, puser);
if (status == APR_SUCCESS && bucket_length > consumed) {
/* We have data left in the bucket. Split it. */
status = apr_bucket_split(bucket, consumed);
}
readlen += consumed;
}
}
apr_bucket_delete(bucket);
}
if (readlen == 0 && status == APR_SUCCESS && block == APR_NONBLOCK_READ) {
return APR_EAGAIN;
}
return status;
}
apr_status_t h2_conn_io_read(h2_conn_io *io,
apr_read_type_e block,
h2_conn_io_on_read_cb on_read_cb,
void *puser)
{
apr_status_t status;
int done = 0;
ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, io->connection,
"h2_conn_io: try read, block=%d", block);
if (!APR_BRIGADE_EMPTY(io->input)) {
/* Seems something is left from a previous read, lets
* satisfy our caller with the data we already have. */
status = h2_conn_io_bucket_read(io, block, on_read_cb, puser, &done);
apr_brigade_cleanup(io->input);
if (status != APR_SUCCESS || done) {
return status;
}
}
/* We only do a blocking read when we have no streams to process. So,
* in httpd scoreboard lingo, we are in a KEEPALIVE connection state.
* When reading non-blocking, we do have streams to process and update
* child with NULL request. That way, any current request information
* in the scoreboard is preserved.
*/
if (block == APR_BLOCK_READ) {
ap_update_child_status_from_conn(io->connection->sbh,
SERVER_BUSY_KEEPALIVE,
io->connection);
}
else {
ap_update_child_status(io->connection->sbh, SERVER_BUSY_READ, NULL);
}
/* TODO: replace this with a connection filter itself, so that we
* no longer need to transfer incoming buckets to our own brigade.
*/
status = ap_get_brigade(io->connection->input_filters,
io->input, AP_MODE_READBYTES,
block, 64 * 4096);
switch (status) {
case APR_SUCCESS:
return h2_conn_io_bucket_read(io, block, on_read_cb, puser, &done);
case APR_EOF:
case APR_EAGAIN:
break;
default:
ap_log_cerror(APLOG_MARK, APLOG_DEBUG, status, io->connection,
"h2_conn_io: error reading");
break;
}
return status;
}
static apr_status_t pass_out(apr_bucket_brigade *bb, void *ctx)
{
h2_conn_io *io = (h2_conn_io*)ctx;
apr_status_t status;
apr_off_t bblen;
if (APR_BRIGADE_EMPTY(bb)) {
return APR_SUCCESS;
}
ap_update_child_status(io->connection->sbh, SERVER_BUSY_WRITE, NULL);
status = apr_brigade_length(bb, 0, &bblen);
if (status == APR_SUCCESS) {
ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, io->connection,
"h2_conn_io(%ld): pass_out brigade %ld bytes",
io->connection->id, (long)bblen);
status = ap_pass_brigade(io->connection->output_filters, bb);
if (status == APR_SUCCESS) {
io->bytes_written += (apr_size_t)bblen;
io->last_write = apr_time_now();
}
apr_brigade_cleanup(bb);
}
return status;
}
/* Bring the current buffer content into the output brigade, appropriately
* chunked.
*/
static apr_status_t bucketeer_buffer(h2_conn_io *io) {
const char *data = io->buffer;
apr_size_t remaining = io->buflen;
apr_bucket *b;
int bcount, i;
if (io->write_size > WRITE_SIZE_INITIAL
&& (io->cooldown_usecs > 0)
&& (apr_time_now() - io->last_write) >= io->cooldown_usecs) {
/* long time not written, reset write size */
io->write_size = WRITE_SIZE_INITIAL;
io->bytes_written = 0;
ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, io->connection,
"h2_conn_io(%ld): timeout write size reset to %ld",
(long)io->connection->id, (long)io->write_size);
}
else if (io->write_size < WRITE_SIZE_MAX
&& io->bytes_written >= io->warmup_size) {
/* connection is hot, use max size */
io->write_size = WRITE_SIZE_MAX;
ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, io->connection,
"h2_conn_io(%ld): threshold reached, write size now %ld",
(long)io->connection->id, (long)io->write_size);
}
bcount = (int)(remaining / io->write_size);
for (i = 0; i < bcount; ++i) {
b = apr_bucket_transient_create(data, io->write_size,
io->output->bucket_alloc);
APR_BRIGADE_INSERT_TAIL(io->output, b);
data += io->write_size;
remaining -= io->write_size;
}
if (remaining > 0) {
b = apr_bucket_transient_create(data, remaining,
io->output->bucket_alloc);
APR_BRIGADE_INSERT_TAIL(io->output, b);
}
return APR_SUCCESS;
}
apr_status_t h2_conn_io_write(h2_conn_io *io,
const char *buf, size_t length)
{
apr_status_t status = APR_SUCCESS;
io->unflushed = 1;
if (io->bufsize > 0) {
ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, io->connection,
"h2_conn_io: buffering %ld bytes", (long)length);
if (!APR_BRIGADE_EMPTY(io->output)) {
status = h2_conn_io_pass(io);
io->unflushed = 1;
}
while (length > 0 && (status == APR_SUCCESS)) {
apr_size_t avail = io->bufsize - io->buflen;
if (avail <= 0) {
bucketeer_buffer(io);
status = pass_out(io->output, io);
io->buflen = 0;
}
else if (length > avail) {
memcpy(io->buffer + io->buflen, buf, avail);
io->buflen += avail;
length -= avail;
buf += avail;
}
else {
memcpy(io->buffer + io->buflen, buf, length);
io->buflen += length;
length = 0;
break;
}
}
}
else {
ap_log_cerror(APLOG_MARK, APLOG_TRACE2, status, io->connection,
"h2_conn_io: writing %ld bytes to brigade", (long)length);
status = apr_brigade_write(io->output, pass_out, io, buf, length);
}
return status;
}
apr_status_t h2_conn_io_writeb(h2_conn_io *io, apr_bucket *b)
{
APR_BRIGADE_INSERT_TAIL(io->output, b);
io->unflushed = 1;
return APR_SUCCESS;
}
apr_status_t h2_conn_io_consider_flush(h2_conn_io *io)
{
apr_status_t status = APR_SUCCESS;
/* The HTTP/1.1 network output buffer/flush behaviour does not
* give optimal performance in the HTTP/2 case, as the pattern of
* buckets (data/eor/eos) is different.
* As long as we have not found out the "best" way to deal with
* this, force a flush at least every WRITE_BUFFER_SIZE amount
* of data.
*/
if (io->unflushed) {
apr_off_t len = 0;
if (!APR_BRIGADE_EMPTY(io->output)) {
apr_brigade_length(io->output, 0, &len);
}
len += io->buflen;
if (len >= WRITE_BUFFER_SIZE) {
return h2_conn_io_pass(io);
}
}
return status;
}
static apr_status_t h2_conn_io_flush_int(h2_conn_io *io, int force)
{
if (io->unflushed || force) {
if (io->buflen > 0) {
/* something in the buffer, put it in the output brigade */
ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, io->connection,
"h2_conn_io: flush, flushing %ld bytes", (long)io->buflen);
bucketeer_buffer(io);
io->buflen = 0;
}
if (force) {
APR_BRIGADE_INSERT_TAIL(io->output,
apr_bucket_flush_create(io->output->bucket_alloc));
}
ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, io->connection,
"h2_conn_io: flush");
/* Send it out */
io->unflushed = 0;
return pass_out(io->output, io);
/* no more access after this, as we might have flushed an EOC bucket
* that de-allocated us all. */
}
return APR_SUCCESS;
}
apr_status_t h2_conn_io_flush(h2_conn_io *io)
{
return h2_conn_io_flush_int(io, 1);
}
apr_status_t h2_conn_io_pass(h2_conn_io *io)
{
return h2_conn_io_flush_int(io, 0);
}
apr_status_t h2_conn_io_close(h2_conn_io *io, void *session)
{
apr_bucket *b;
/* Send out anything in our buffers */
h2_conn_io_flush_int(io, 0);
b = h2_bucket_eoc_create(io->connection->bucket_alloc, session);
APR_BRIGADE_INSERT_TAIL(io->output, b);
b = apr_bucket_flush_create(io->connection->bucket_alloc);
APR_BRIGADE_INSERT_TAIL(io->output, b);
return ap_pass_brigade(io->connection->output_filters, io->output);
/* and all is gone */
}