static void f_abort_gnutls(liGnuTLSFilter *f) { if (f->aborted) return; f->aborted = TRUE; f_acquire(f); f_close_gnutls(f); li_stream_disconnect(&f->crypt_source); /* plain in -> crypt out */ li_stream_disconnect(&f->crypt_drain); /* io -> crypt in */ li_stream_disconnect_dest(&f->crypt_source); /* crypt out -> io */ f_release(f); }
/* app -> ssl (plain) */ static void stream_plain_drain_cb(liStream *stream, liStreamEvent event) { liGnuTLSFilter *f = LI_CONTAINER_OF(stream, liGnuTLSFilter, plain_drain); switch (event) { case LI_STREAM_NEW_DATA: if (!stream->out->is_closed && NULL != stream->source) { li_chunkqueue_steal_all(stream->out, stream->source->out); stream->out->is_closed = stream->out->is_closed || stream->source->out->is_closed; } do_gnutls_write(f); if (stream->out->is_closed) { li_stream_disconnect(stream); stream->out->is_closed = FALSE; } break; case LI_STREAM_NEW_CQLIMIT: break; case LI_STREAM_CONNECTED_DEST: /* crypt_source */ break; case LI_STREAM_CONNECTED_SOURCE: /* app */ break; case LI_STREAM_DISCONNECTED_DEST: if (!stream->out->is_closed || 0 != stream->out->length) { f_abort_gnutls(f); /* didn't read everything */ } break; case LI_STREAM_DISCONNECTED_SOURCE: if (!stream->out->is_closed) { f_abort_gnutls(f); /* didn't get everything */ } break; case LI_STREAM_DESTROY: f_release(f); break; } }
/* io -> ssl crypted in */ static void stream_crypt_drain_cb(liStream *stream, liStreamEvent event) { liGnuTLSFilter *f = LI_CONTAINER_OF(stream, liGnuTLSFilter, crypt_drain); switch (event) { case LI_STREAM_NEW_DATA: if (!stream->out->is_closed && NULL != stream->source) { li_chunkqueue_steal_all(stream->out, stream->source->out); stream->out->is_closed = stream->out->is_closed || stream->source->out->is_closed; li_stream_notify(stream); /* tell plain_source to do SSL_read */ } if (stream->out->is_closed) { li_stream_disconnect(stream); } break; case LI_STREAM_NEW_CQLIMIT: break; case LI_STREAM_CONNECTED_DEST: /* plain_source */ break; case LI_STREAM_CONNECTED_SOURCE: /* io in */ break; case LI_STREAM_DISCONNECTED_DEST: /* plain_source */ if (!stream->out->is_closed || 0 != stream->out->length) { f_abort_gnutls(f); /* didn't read everything */ } break; case LI_STREAM_DISCONNECTED_SOURCE: /* io in disconnect */ if (!stream->out->is_closed) { f_abort_gnutls(f); /* conn aborted */ } break; case LI_STREAM_DESTROY: f_release(f); break; } }
static void stream_http_response_cb(liStream *stream, liStreamEvent event) { liStreamHttpResponse* shr = LI_CONTAINER_OF(stream, liStreamHttpResponse, stream); switch (event) { case LI_STREAM_NEW_DATA: stream_http_response_data(shr); break; case LI_STREAM_DISCONNECTED_DEST: shr->vr = NULL; li_stream_disconnect(stream); break; case LI_STREAM_DISCONNECTED_SOURCE: shr->vr = NULL; if (!stream->out->is_closed) { /* "abort" */ li_stream_disconnect_dest(stream); } break; case LI_STREAM_DESTROY: li_http_response_parser_clear(&shr->parse_response_ctx); g_slice_free(liStreamHttpResponse, shr); break; default: break; } }
static void bod_cb(liStream *stream, liStreamEvent event) { bod_state *state = LI_CONTAINER_OF(stream, bod_state, stream); switch (event) { case LI_STREAM_NEW_DATA: bod_handle_data(state); break; case LI_STREAM_NEW_CQLIMIT: break; case LI_STREAM_CONNECTED_DEST: break; case LI_STREAM_CONNECTED_SOURCE: break; case LI_STREAM_DISCONNECTED_DEST: if (!state->stream.out->is_closed || 0 != state->stream.out->length) { li_stream_disconnect(stream); bod_close(state); } break; case LI_STREAM_DISCONNECTED_SOURCE: if (!state->stream.out->is_closed) { li_stream_disconnect_dest(stream); bod_close(state); } break; case LI_STREAM_DESTROY: bod_close(state); g_slice_free(bod_state, state); break; } }
static void stream_http_response_data(liStreamHttpResponse* shr) { if (NULL == shr->stream.source) return; if (!shr->response_headers_finished) { switch (li_http_response_parse(shr->vr, &shr->parse_response_ctx)) { case LI_HANDLER_GO_ON: check_response_header(shr); if (NULL == shr->stream.source) return; break; case LI_HANDLER_ERROR: VR_ERROR(shr->vr, "%s", "Parsing response header failed"); li_vrequest_error(shr->vr); return; case LI_HANDLER_WAIT_FOR_EVENT: if (shr->stream.source->out->is_closed) { VR_ERROR(shr->vr, "%s", "Parsing response header failed (eos)"); li_vrequest_error(shr->vr); } return; default: return; } } if (shr->transfer_encoding_chunked) { if (!li_filter_chunked_decode(shr->vr, shr->stream.out, shr->stream.source->out, &shr->chunked_decode_state)) { if (NULL != shr->vr) { VR_ERROR(shr->vr, "%s", "Decoding chunks failed"); li_vrequest_error(shr->vr); } else { li_stream_reset(&shr->stream); } } if (shr->stream.source->out->is_closed) { li_stream_disconnect(&shr->stream); } } else { li_chunkqueue_steal_all(shr->stream.out, shr->stream.source->out); if (shr->stream.source->out->is_closed) { shr->stream.out->is_closed = TRUE; li_stream_disconnect(&shr->stream); } } li_stream_notify(&shr->stream); }
static liHandlerResult cache_etag_filter_hit(liVRequest *vr, liFilter *f) { UNUSED(vr); if (NULL != f->in) { li_chunkqueue_skip_all(f->in); li_stream_disconnect(&f->stream); return LI_HANDLER_GO_ON; } return LI_HANDLER_GO_ON; }
/* stop buffering, forward everyting */ static void bod_stop(bod_state *state) { bod_close(state); if (NULL != state->stream.source && !state->stream.out->is_closed) { li_chunkqueue_steal_all(state->stream.out, state->stream.source->out); if (state->stream.source->out->is_closed) { state->stream.out->is_closed = TRUE; li_stream_disconnect(&state->stream); } li_stream_notify(&state->stream); } state->vr = NULL; }
static void f_close_gnutls(liGnuTLSFilter *f) { if (NULL != f->session && !f->closing) { liCQLimit *limit; f->closing = TRUE; f->session = NULL; LI_FORCE_ASSERT(NULL != f->crypt_source.out); LI_FORCE_ASSERT(NULL != f->crypt_source.out->limit); limit = f->crypt_source.out->limit; limit->notify = NULL; limit->context = NULL; li_stream_disconnect(&f->plain_source); /* crypt in -> plain out */ li_stream_disconnect(&f->plain_drain); /* app -> plain in */ li_stream_disconnect_dest(&f->plain_source); /* plain out -> app */ f->log_context = NULL; if (NULL != f->callbacks && NULL != f->callbacks->closed_cb) { f->callbacks->closed_cb(f, f->callback_data); } } }
static void li_connection_reset_keep_alive(liConnection *con) { liVRequest *vr = con->mainvr; if (NULL == con->con_sock.raw_in || NULL == con->con_sock.raw_out || con->in.source != con->con_sock.raw_in) { li_connection_reset(con); return; } /* only start keep alive watcher if there isn't more input data already */ if (con->con_sock.raw_in->out->length == 0) { li_event_stop(&con->keep_alive_data.watcher); { con->keep_alive_data.max_idle = CORE_OPTION(LI_CORE_OPTION_MAX_KEEP_ALIVE_IDLE).number; if (con->keep_alive_data.max_idle == 0) { con->state = LI_CON_STATE_CLOSE; con_iostream_shutdown(con); li_connection_reset(con); return; } con->keep_alive_data.timeout = li_cur_ts(con->wrk) + con->keep_alive_data.max_idle; if (con->keep_alive_data.max_idle == con->srv->keep_alive_queue_timeout) { /* queue is sorted by con->keep_alive_data.timeout */ gboolean need_start = (0 == con->wrk->keep_alive_queue.length); con->keep_alive_data.timeout = li_cur_ts(con->wrk) + con->srv->keep_alive_queue_timeout; g_queue_push_tail(&con->wrk->keep_alive_queue, con); con->keep_alive_data.link = g_queue_peek_tail_link(&con->wrk->keep_alive_queue); if (need_start) li_worker_check_keepalive(con->wrk); } else { li_event_timer_once(&con->keep_alive_data.watcher, con->keep_alive_data.max_idle); } } } else { li_stream_again_later(&con->in); } con->state = LI_CON_STATE_KEEP_ALIVE; con->response_headers_sent = FALSE; con->expect_100_cont = FALSE; con->out_has_all_data = FALSE; con->info.keep_alive = TRUE; li_connection_update_io_wait(con); li_vrequest_reset(con->mainvr, TRUE); li_http_request_parser_reset(&con->req_parser_ctx); li_stream_disconnect(&con->out); li_stream_disconnect_dest(&con->in); con->out.out->is_closed = FALSE; memset(&con->in_chunked_decode_state, 0, sizeof(con->in_chunked_decode_state)); /* restore chunkqueue limits */ li_chunkqueue_use_limit(con->con_sock.raw_in->out, LI_CONNECTION_DEFAULT_CHUNKQUEUE_LIMIT); li_chunkqueue_use_limit(con->con_sock.raw_out->out, LI_CONNECTION_DEFAULT_CHUNKQUEUE_LIMIT); /* reset stats */ con->info.stats.bytes_in = G_GUINT64_CONSTANT(0); con->info.stats.bytes_in_5s = G_GUINT64_CONSTANT(0); con->info.stats.bytes_in_5s_diff = G_GUINT64_CONSTANT(0); con->info.stats.bytes_out = G_GUINT64_CONSTANT(0); con->info.stats.bytes_out_5s = G_GUINT64_CONSTANT(0); con->info.stats.bytes_out_5s_diff = G_GUINT64_CONSTANT(0); con->info.stats.last_avg = 0; }
/* http response header/data -> tcp/ssl */ static void _connection_http_out_cb(liStream *stream, liStreamEvent event) { liConnection *con = LI_CONTAINER_OF(stream, liConnection, out); liChunkQueue *raw_out = stream->out, *out; liVRequest *vr = con->mainvr; switch (event) { case LI_STREAM_NEW_DATA: /* handle below */ break; case LI_STREAM_CONNECTED_SOURCE: /* also handle data immediately */ break; case LI_STREAM_DISCONNECTED_SOURCE: if (!con->out_has_all_data) li_connection_error(con); return; case LI_STREAM_DISCONNECTED_DEST: if (!raw_out->is_closed || 0 != raw_out->length || NULL == con->con_sock.raw_out) { li_connection_error(con); } else { connection_close(con); } return; case LI_STREAM_DESTROY: con->info.resp = NULL; li_job_later(&con->wrk->loop.jobqueue, &con->job_reset); return; default: return; } out = (NULL != stream->source) ? stream->source->out : NULL; /* keep raw_out->is_closed = FALSE for keep-alive requests; instead set con->out_has_all_data = TRUE */ if (LI_CON_STATE_HANDLE_MAINVR <= con->state) { if (NULL == stream->source) { if (LI_CON_STATE_HANDLE_MAINVR == con->state) { /* wait for vrequest to connect the stream as signal that the headers are ready */ return; } } if (!con->response_headers_sent) { if (CORE_OPTION(LI_CORE_OPTION_DEBUG_REQUEST_HANDLING).boolean) { VR_DEBUG(vr, "%s", "write response headers"); } con->response_headers_sent = TRUE; if (0 == CORE_OPTION(LI_CORE_OPTION_MAX_KEEP_ALIVE_IDLE).number) { con->info.keep_alive = FALSE; } li_response_send_headers(vr, raw_out, out, FALSE); } if (!con->out_has_all_data && !raw_out->is_closed && NULL != out) { if (vr->response.transfer_encoding & LI_HTTP_TRANSFER_ENCODING_CHUNKED) { li_filter_chunked_encode(vr, raw_out, out); } else { li_chunkqueue_steal_all(raw_out, out); } } if (raw_out->is_closed || NULL == out || out->is_closed) { con->out_has_all_data = TRUE; raw_out->is_closed = FALSE; } if (con->out_has_all_data) { if (con->state < LI_CON_STATE_WRITE) { con->state = LI_CON_STATE_WRITE; li_connection_update_io_wait(con); } if (NULL != out) { out = NULL; li_stream_disconnect(stream); } } con->info.out_queue_length = raw_out->length; } li_stream_notify(stream); }
static liHandlerResult memcache_store_filter(liVRequest *vr, liFilter *f) { memcache_filter *mf = (memcache_filter*) f->param; if (NULL == f->in) { memcache_store_filter_free(vr, f); /* didn't handle f->in->is_closed? abort forwarding */ if (!f->out->is_closed) li_stream_reset(&f->stream); return LI_HANDLER_GO_ON; } if (NULL == mf) goto forward; if (f->in->is_closed && 0 == f->in->length && f->out->is_closed) { /* nothing to do anymore */ return LI_HANDLER_GO_ON; } /* check if size still fits into buffer */ if ((gssize) (f->in->length + mf->buf->used) > (gssize) mf->ctx->maxsize) { /* response too big, switch to "forward" mode */ memcache_store_filter_free(vr, f); goto forward; } while (0 < f->in->length) { char *data; off_t len; liChunkIter ci; liHandlerResult res; GError *err = NULL; ci = li_chunkqueue_iter(f->in); if (LI_HANDLER_GO_ON != (res = li_chunkiter_read(ci, 0, 16*1024, &data, &len, &err))) { if (NULL != err) { VR_ERROR(vr, "Couldn't read data from chunkqueue: %s", err->message); g_error_free(err); } return res; } if ((gssize) (len + mf->buf->used) > (gssize) mf->ctx->maxsize) { /* response too big, switch to "forward" mode */ memcache_store_filter_free(vr, f); goto forward; } memcpy(mf->buf->addr + mf->buf->used, data, len); mf->buf->used += len; if (!f->out->is_closed) { li_chunkqueue_steal_len(f->out, f->in, len); } else { li_chunkqueue_skip(f->in, len); } } if (f->in->is_closed) { /* finally: store response in memcached */ liMemcachedCon *con; GError *err = NULL; liMemcachedRequest *req; memcached_ctx *ctx = mf->ctx; assert(0 == f->in->length); f->out->is_closed = TRUE; con = mc_ctx_prepare(ctx, vr->wrk); mc_ctx_build_key(vr->wrk->tmp_str, ctx, vr); if (NULL != vr && CORE_OPTION(LI_CORE_OPTION_DEBUG_REQUEST_HANDLING).boolean) { VR_DEBUG(vr, "memcached.store: storing response for key '%s'", vr->wrk->tmp_str->str); } req = li_memcached_set(con, vr->wrk->tmp_str, ctx->flags, ctx->ttl, mf->buf, NULL, NULL, &err); memcache_store_filter_free(vr, f); if (NULL == req) { if (NULL != err) { if (NULL != vr && LI_MEMCACHED_DISABLED != err->code) { VR_ERROR(vr, "memcached.store: set failed: %s", err->message); } g_clear_error(&err); } else if (NULL != vr) { VR_ERROR(vr, "memcached.store: set failed: %s", "Unkown error"); } } } return LI_HANDLER_GO_ON; forward: if (f->out->is_closed) { li_chunkqueue_skip_all(f->in); li_stream_disconnect(&f->stream); } else { li_chunkqueue_steal_all(f->out, f->in); if (f->in->is_closed) f->out->is_closed = f->in->is_closed; } return LI_HANDLER_GO_ON; }
static void do_gnutls_write(liGnuTLSFilter *f) { const ssize_t blocksize = 16*1024; /* 16k */ char *block_data; off_t block_len; ssize_t r; off_t write_max; #ifdef USE_CORK gboolean corked = FALSE; #endif liChunkQueue *cq = f->plain_drain.out; f_acquire(f); f->write_wants_read = FALSE; /* use space in (encrypted) outgoing buffer as amounts of bytes we try to write from (plain) output * don't care if we write a little bit more than the limit allowed */ write_max = li_chunkqueue_limit_available(f->crypt_source.out); LI_FORCE_ASSERT(write_max >= 0); /* we set a limit! */ if (0 == write_max) goto out; /* if we start writing, try to write at least blocksize bytes */ if (write_max < blocksize) write_max = blocksize; if (NULL != f->session && !f->initial_handshaked_finished && !do_gnutls_handshake(f, TRUE)) goto out; if (NULL == f->session) { f_abort_gnutls(f); goto out; } #ifdef USE_CORK if (0 != cq->length && cq->queue.length > 1) { corked = TRUE; gnutls_record_cork(f->session); } #endif do { GError *err = NULL; liChunkIter ci; if (0 == cq->length) break; ci = li_chunkqueue_iter(cq); switch (li_chunkiter_read(ci, 0, blocksize, &block_data, &block_len, &err)) { case LI_HANDLER_GO_ON: break; case LI_HANDLER_ERROR: if (NULL != err) { _ERROR(f->srv, f->wrk, f->log_context, "Couldn't read data from chunkqueue: %s", err->message); g_error_free(err); } /* fall through */ default: f_abort_gnutls(f); goto out; } r = gnutls_record_send(f->session, block_data, block_len); if (r <= 0) { do_handle_error(f, "gnutls_record_send", r, TRUE); goto out; } li_chunkqueue_skip(cq, r); write_max -= r; } while (r == block_len && write_max > 0); if (cq->is_closed && 0 == cq->length) { r = gnutls_bye(f->session, GNUTLS_SHUT_RDWR); switch (r) { case GNUTLS_E_SUCCESS: case GNUTLS_E_AGAIN: case GNUTLS_E_INTERRUPTED: f->plain_source.out->is_closed = TRUE; f->crypt_source.out->is_closed = TRUE; f->crypt_drain.out->is_closed = TRUE; li_stream_disconnect(&f->crypt_source); /* plain in -> crypt out */ f_close_gnutls(f); break; default: do_handle_error(f, "gnutls_bye", r, TRUE); f_abort_gnutls(f); break; } } else if (0 < cq->length && 0 != li_chunkqueue_limit_available(f->crypt_source.out)) { li_stream_again_later(&f->plain_drain); } out: #ifdef USE_CORK if (NULL != f->session && corked) { corked = TRUE; gnutls_record_uncork(f->session, 0); } #endif f_release(f); }
static void do_gnutls_read(liGnuTLSFilter *f) { const ssize_t blocksize = 16*1024; /* 16k */ off_t max_read = 4 * blocksize; /* 64k */ ssize_t r; off_t len = 0; liChunkQueue *cq = f->plain_source.out; f_acquire(f); if (NULL != f->session && !f->initial_handshaked_finished && !do_gnutls_handshake(f, FALSE)) goto out; if (NULL == f->session) { f_abort_gnutls(f); goto out; } do { liBuffer *buf; gboolean cq_buf_append; buf = li_chunkqueue_get_last_buffer(cq, 1024); cq_buf_append = (buf != NULL); if (buf != NULL) { /* use last buffer as raw_in_buffer; they should be the same anyway */ if (G_UNLIKELY(buf != f->raw_in_buffer)) { li_buffer_acquire(buf); li_buffer_release(f->raw_in_buffer); f->raw_in_buffer = buf; } } else { buf = f->raw_in_buffer; if (buf != NULL && buf->alloc_size - buf->used < 1024) { /* release *buffer */ li_buffer_release(buf); f->raw_in_buffer = buf = NULL; } if (buf == NULL) { f->raw_in_buffer = buf = li_buffer_new(blocksize); } } LI_FORCE_ASSERT(f->raw_in_buffer == buf); r = gnutls_record_recv(f->session, buf->addr + buf->used, buf->alloc_size - buf->used); if (r < 0) { do_handle_error(f, "gnutls_record_recv", r, FALSE); goto out; } else if (r == 0) { /* clean shutdown? */ f->plain_source.out->is_closed = TRUE; f->plain_drain.out->is_closed = TRUE; f->crypt_source.out->is_closed = TRUE; f->crypt_drain.out->is_closed = TRUE; li_stream_disconnect(&f->crypt_drain); /* io -> crypt in */ li_stream_disconnect_dest(&f->crypt_source); /* crypt out -> io */ li_stream_disconnect(&f->crypt_source); /* plain in -> crypt out */ f_close_gnutls(f); goto out; } if (cq_buf_append) { li_chunkqueue_update_last_buffer_size(cq, r); } else { gsize offset; li_buffer_acquire(buf); offset = buf->used; buf->used += r; li_chunkqueue_append_buffer2(cq, buf, offset, r); } if (buf->alloc_size - buf->used < 1024) { /* release *buffer */ li_buffer_release(buf); f->raw_in_buffer = buf = NULL; } len += r; } while (len < max_read); out: f_release(f); }
static void bod_handle_data(bod_state *state) { liChunkQueue *out = state->stream.out; liChunkQueue *in; if (out->is_closed) { li_stream_disconnect(&state->stream); bod_close(state); return; } in = (state->stream.source != NULL) ? state->stream.source->out : NULL; if (NULL == in) goto out; if (NULL == state->vr) { li_chunkqueue_steal_all(out, in); goto out; } while (in->length > 0) { liChunk *c = li_chunkqueue_first_chunk(in); liChunkIter ci; off_t length, data_len; char *data = NULL; GError *err; assert(UNUSED_CHUNK != c->type); switch (c->type) { case UNUSED_CHUNK: /* shouldn't happen anyway, but stealing it is ok here too */ case FILE_CHUNK: if (state->split_on_file_chunks) { bod_close(state); } else { bod_flush(state); } li_chunkqueue_steal_chunk(out, in); break; case STRING_CHUNK: case MEM_CHUNK: case BUFFER_CHUNK: if (!bod_open(state)) return; length = li_chunk_length(c); ci = li_chunkqueue_iter(in); err = NULL; if (LI_HANDLER_GO_ON != li_chunkiter_read(ci, 0, length, &data, &data_len, &err)) { if (NULL != err) { VR_ERROR(state->vr, "%s", err->message); g_error_free(err); } bod_error(state); return; } while ( data_len > 0 ) { ssize_t r; r = pwrite(state->tempfile->fd, data, data_len, state->write_pos); if (r < 0) { switch (errno) { case EINTR: continue; default: break; } VR_ERROR(state->vr, "pwrite failed: %s", g_strerror(errno)); bod_stop(state); /* write failures are not critical */ return; } data += r; data_len -= r; state->write_pos += r; } li_chunkqueue_skip(in, length); break; } } bod_autoflush(state); out: if (NULL == in || in->is_closed) { out->is_closed = TRUE; bod_close(state); /* close/flush ignores out->is_closed */ li_stream_notify(&state->stream); /* if no flush happened we still notify */ } }
static liHandlerResult cache_etag_filter_miss(liVRequest *vr, liFilter *f) { cache_etag_file *cfile = (cache_etag_file*) f->param; ssize_t res; gchar *buf; off_t buflen; liChunkIter citer; GError *err = NULL; if (NULL == f->in) { cache_etag_filter_free(vr, f); /* didn't handle f->in->is_closed? abort forwarding */ if (!f->out->is_closed) li_stream_reset(&f->stream); return LI_HANDLER_GO_ON; } if (NULL == cfile) goto forward; if (f->in->length > 0) { citer = li_chunkqueue_iter(f->in); if (LI_HANDLER_GO_ON != li_chunkiter_read(citer, 0, 64*1024, &buf, &buflen, &err)) { if (NULL != err) { if (NULL != vr) VR_ERROR(vr, "Couldn't read data from chunkqueue: %s", err->message); g_error_free(err); } else { if (NULL != vr) VR_ERROR(vr, "%s", "Couldn't read data from chunkqueue"); } cache_etag_filter_free(vr, f); goto forward; } res = write(cfile->fd, buf, buflen); if (res < 0) { switch (errno) { case EINTR: case EAGAIN: return LI_HANDLER_COMEBACK; default: if (NULL != vr) VR_ERROR(vr, "Couldn't write to temporary cache file '%s': %s", cfile->tmpfilename->str, g_strerror(errno)); cache_etag_filter_free(vr, f); goto forward; } } else { if (!f->out->is_closed) { li_chunkqueue_steal_len(f->out, f->in, res); } else { li_chunkqueue_skip(f->in, res); } } } if (0 == f->in->length && f->in->is_closed) { f->out->is_closed = TRUE; f->param = NULL; cache_etag_file_finish(vr, cfile); return LI_HANDLER_GO_ON; } return LI_HANDLER_GO_ON; forward: if (f->out->is_closed) { li_chunkqueue_skip_all(f->in); li_stream_disconnect(&f->stream); } else { li_chunkqueue_steal_all(f->out, f->in); if (f->in->is_closed) f->out->is_closed = f->in->is_closed; } return LI_HANDLER_GO_ON; }