/* Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 #include #include #include #include #include #include #include #include #include #include "tls_proto.h" #include "tls_conf.h" #include "tls_core.h" #include "tls_filter.h" #include "tls_util.h" extern module AP_MODULE_DECLARE_DATA tls_module; APLOG_USE_MODULE(tls); static rustls_io_result tls_read_callback( void *userdata, unsigned char *buf, size_t n, size_t *out_n) { tls_data_t *d = userdata; size_t len = d->len > n? n : d->len; memcpy(buf, d->data, len); *out_n = len; return 0; } /** * Provide TLS encrypted data to the rustls server_session in cc->rustls_connection>. * * If fin_tls_bb> holds data, take it from there. Otherwise perform a * read via the network filters below us into that brigade. * * fin_block> determines if we do a blocking read inititally or not. * If the first read did to not produce enough data, any secondary read is done * non-blocking. * * Had any data been added to cc->rustls_connection>, call its "processing" * function to handle the added data before leaving. */ static apr_status_t read_tls_to_rustls( tls_filter_ctx_t *fctx, apr_size_t len, apr_read_type_e block, int errors_expected) { tls_data_t d; apr_size_t rlen; apr_off_t passed = 0; rustls_result rr = RUSTLS_RESULT_OK; int os_err; apr_status_t rv = APR_SUCCESS; if (APR_BRIGADE_EMPTY(fctx->fin_tls_bb)) { ap_log_error(APLOG_MARK, APLOG_TRACE2, rv, fctx->cc->server, "read_tls_to_rustls, get data from network, block=%d", block); rv = ap_get_brigade(fctx->fin_ctx->next, fctx->fin_tls_bb, AP_MODE_READBYTES, block, (apr_off_t)len); if (APR_SUCCESS != rv) { goto cleanup; } } while (!APR_BRIGADE_EMPTY(fctx->fin_tls_bb) && passed < (apr_off_t)len) { apr_bucket *b = APR_BRIGADE_FIRST(fctx->fin_tls_bb); if (APR_BUCKET_IS_EOS(b)) { ap_log_error(APLOG_MARK, APLOG_TRACE2, rv, fctx->cc->server, "read_tls_to_rustls, EOS"); if (fctx->fin_tls_buffer_bb) { apr_brigade_cleanup(fctx->fin_tls_buffer_bb); } rv = APR_EOF; goto cleanup; } rv = apr_bucket_read(b, (const char**)&d.data, &d.len, block); if (APR_STATUS_IS_EOF(rv)) { apr_bucket_delete(b); continue; } else if (APR_SUCCESS != rv) { goto cleanup; } if (d.len > 0) { /* got something, do not block on getting more */ block = APR_NONBLOCK_READ; os_err = rustls_connection_read_tls(fctx->cc->rustls_connection, tls_read_callback, &d, &rlen); if (os_err) { rv = APR_FROM_OS_ERROR(os_err); goto cleanup; } if (fctx->fin_tls_buffer_bb) { /* we buffer for later replay on the 'real' rustls_connection */ apr_brigade_write(fctx->fin_tls_buffer_bb, NULL, NULL, (const char*)d.data, rlen); } if (rlen >= d.len) { apr_bucket_delete(b); } else { b->start += (apr_off_t)rlen; b->length -= rlen; } fctx->fin_bytes_in_rustls += (apr_off_t)d.len; passed += (apr_off_t)rlen; } else if (d.len == 0) { apr_bucket_delete(b); } } if (passed > 0) { rr = rustls_connection_process_new_packets(fctx->cc->rustls_connection); if (rr != RUSTLS_RESULT_OK) goto cleanup; } cleanup: if (rr != RUSTLS_RESULT_OK) { rv = APR_ECONNRESET; if (!errors_expected) { const char *err_descr = ""; rv = tls_core_error(fctx->c, rr, &err_descr); ap_log_cerror(APLOG_MARK, APLOG_WARNING, rv, fctx->c, APLOGNO(10353) "processing TLS data: [%d] %s", (int)rr, err_descr); } } else if (APR_STATUS_IS_EOF(rv) && passed > 0) { /* encountering EOF while actually having read sth is a success. */ rv = APR_SUCCESS; } else if (APR_SUCCESS == rv && passed == 0 && fctx->fin_block == APR_NONBLOCK_READ) { rv = APR_EAGAIN; } else { ap_log_error(APLOG_MARK, APLOG_TRACE2, rv, fctx->cc->server, "read_tls_to_rustls, passed %ld bytes to rustls", (long)passed); } return rv; } static apr_status_t fout_pass_tls_to_net(tls_filter_ctx_t *fctx) { apr_status_t rv = APR_SUCCESS; if (!APR_BRIGADE_EMPTY(fctx->fout_tls_bb)) { rv = ap_pass_brigade(fctx->fout_ctx->next, fctx->fout_tls_bb); if (APR_SUCCESS == rv && fctx->c->aborted) { rv = APR_ECONNRESET; } fctx->fout_bytes_in_tls_bb = 0; apr_brigade_cleanup(fctx->fout_tls_bb); } return rv; } static apr_status_t fout_pass_all_to_net( tls_filter_ctx_t *fctx, int flush); static apr_status_t filter_abort( tls_filter_ctx_t *fctx) { apr_status_t rv; if (fctx->cc->state != TLS_CONN_ST_DONE) { if (fctx->cc->state > TLS_CONN_ST_CLIENT_HELLO) { rustls_connection_send_close_notify(fctx->cc->rustls_connection); rv = fout_pass_all_to_net(fctx, 1); ap_log_cerror(APLOG_MARK, APLOG_TRACE2, rv, fctx->c, "filter_abort, flushed output"); } fctx->c->aborted = 1; fctx->cc->state = TLS_CONN_ST_DONE; } return APR_ECONNABORTED; } static apr_status_t filter_recv_client_hello(tls_filter_ctx_t *fctx) { apr_status_t rv = APR_SUCCESS; ap_log_error(APLOG_MARK, APLOG_TRACE2, 0, fctx->cc->server, "tls_filter, server=%s, recv client hello", fctx->cc->server->server_hostname); /* only for incoming connections */ ap_assert(!fctx->cc->outgoing); if (rustls_connection_is_handshaking(fctx->cc->rustls_connection)) { apr_bucket_brigade *bb_tmp; ap_log_cerror(APLOG_MARK, APLOG_TRACE2, rv, fctx->c, "filter_recv_client_hello: start"); fctx->fin_tls_buffer_bb = apr_brigade_create(fctx->c->pool, fctx->c->bucket_alloc); do { if (rustls_connection_wants_read(fctx->cc->rustls_connection)) { rv = read_tls_to_rustls(fctx, fctx->fin_max_in_rustls, APR_BLOCK_READ, 1); if (APR_SUCCESS != rv) { if (fctx->cc->client_hello_seen) { rv = APR_EAGAIN; /* we got what we needed */ break; } /* Something went wrong before we saw the client hello. * This is a real error on which we should not continue. */ goto cleanup; } } /* Notice: we never write here to the client. We just want to inspect * the client hello. */ } while (!fctx->cc->client_hello_seen); /* We have seen the client hello and selected the server (vhost) to use * on this connection. Set up the 'real' rustls_connection based on the * servers 'real' rustls_config. */ rv = tls_core_conn_seen_client_hello(fctx->c); if (APR_SUCCESS != rv) goto cleanup; bb_tmp = fctx->fin_tls_bb; /* data we have yet to feed to rustls */ fctx->fin_tls_bb = fctx->fin_tls_buffer_bb; /* data we already fed to the pre_session */ fctx->fin_tls_buffer_bb = NULL; APR_BRIGADE_CONCAT(fctx->fin_tls_bb, bb_tmp); /* all tls data from the client so far, reloaded */ apr_brigade_destroy(bb_tmp); rv = APR_SUCCESS; } cleanup: ap_log_cerror(APLOG_MARK, APLOG_TRACE2, rv, fctx->c, "filter_recv_client_hello: done"); return rv; } static apr_status_t filter_send_client_hello(tls_filter_ctx_t *fctx) { apr_status_t rv = APR_SUCCESS; ap_log_error(APLOG_MARK, APLOG_TRACE2, 0, fctx->cc->server, "tls_filter, server=%s, send client hello", fctx->cc->server->server_hostname); /* Only for outgoing connections */ ap_assert(fctx->cc->outgoing); if (rustls_connection_is_handshaking(fctx->cc->rustls_connection)) { while (rustls_connection_wants_write(fctx->cc->rustls_connection)) { /* write flushed, so it really gets out */ rv = fout_pass_all_to_net(fctx, 1); if (APR_SUCCESS != rv) goto cleanup; } } cleanup: return rv; } /** * While cc->rustls_connection> indicates that a handshake is ongoing, * write TLS data from and read network TLS data to the server session. * * @return APR_SUCCESS when the handshake is completed */ static apr_status_t filter_do_handshake( tls_filter_ctx_t *fctx) { apr_status_t rv = APR_SUCCESS; ap_log_error(APLOG_MARK, APLOG_TRACE2, 0, fctx->cc->server, "tls_filter, server=%s, do handshake", fctx->cc->server->server_hostname); if (rustls_connection_is_handshaking(fctx->cc->rustls_connection)) { do { if (rustls_connection_wants_write(fctx->cc->rustls_connection)) { rv = fout_pass_all_to_net(fctx, 1); if (APR_SUCCESS != rv) goto cleanup; } else if (rustls_connection_wants_read(fctx->cc->rustls_connection)) { rv = read_tls_to_rustls(fctx, fctx->fin_max_in_rustls, APR_BLOCK_READ, 0); if (APR_SUCCESS != rv) goto cleanup; } } while (rustls_connection_is_handshaking(fctx->cc->rustls_connection)); /* rustls reports the TLS handshake to be done, when it *internally* has * processed everything into its buffers. Not when the buffers have been * send to the other side. */ if (rustls_connection_wants_write(fctx->cc->rustls_connection)) { rv = fout_pass_all_to_net(fctx, 1); if (APR_SUCCESS != rv) goto cleanup; } } cleanup: ap_log_error(APLOG_MARK, APLOG_TRACE2, rv, fctx->cc->server, "tls_filter, server=%s, handshake done", fctx->cc->server->server_hostname); if (APR_SUCCESS != rv) { if (fctx->cc->last_error_descr) { ap_log_cerror(APLOG_MARK, APLOG_INFO, APR_ECONNABORTED, fctx->c, APLOGNO(10354) "handshake failed: %s", fctx->cc->last_error_descr); } } return rv; } static apr_status_t progress_tls_atleast_to(tls_filter_ctx_t *fctx, tls_conn_state_t state) { apr_status_t rv = APR_SUCCESS; /* handle termination immediately */ if (state == TLS_CONN_ST_DONE) { rv = APR_ECONNABORTED; goto cleanup; } if (state > TLS_CONN_ST_CLIENT_HELLO && TLS_CONN_ST_CLIENT_HELLO == fctx->cc->state) { rv = tls_core_conn_init(fctx->c); if (APR_SUCCESS != rv) goto cleanup; if (fctx->cc->outgoing) { rv = filter_send_client_hello(fctx); } else { rv = filter_recv_client_hello(fctx); } if (APR_SUCCESS != rv) goto cleanup; fctx->cc->state = TLS_CONN_ST_HANDSHAKE; } if (state > TLS_CONN_ST_HANDSHAKE && TLS_CONN_ST_HANDSHAKE== fctx->cc->state) { rv = filter_do_handshake(fctx); if (APR_SUCCESS != rv) goto cleanup; rv = tls_core_conn_post_handshake(fctx->c); if (APR_SUCCESS != rv) goto cleanup; fctx->cc->state = TLS_CONN_ST_TRAFFIC; } if (state < fctx->cc->state) { rv = APR_ECONNABORTED; } cleanup: if (APR_SUCCESS != rv) { filter_abort(fctx); /* does change the state itself */ } return rv; } /** * The connection filter converting TLS encrypted network data into plain, unencrpyted * traffic data to be processed by filters above it in the filter chain. * * Unfortunately, Apache's filter infrastructure places a heavy implementation * complexity on its input filters for the various use cases its HTTP/1.x parser * (mainly) finds convenient: * * the bucket brigade to place the data into. * one of * - AP_MODE_READBYTES: just add up to data into * - AP_MODE_GETLINE: make a best effort to get data up to and including a CRLF. * it can be less, but not more t than that. * - AP_MODE_EATCRLF: never used, we puke on it. * - AP_MODE_SPECULATIVE: read data without consuming it. * - AP_MODE_EXHAUSTIVE: never used, we puke on it. * - AP_MODE_INIT: called once on a connection. needs to pass down the filter * chain, giving every filter the change to "INIT". * do blocking or non-blocking reads * max amount of data to add to , seems to be 0 for GETLINE */ static apr_status_t filter_conn_input( ap_filter_t *f, apr_bucket_brigade *bb, ap_input_mode_t mode, apr_read_type_e block, apr_off_t readbytes) { tls_filter_ctx_t *fctx = f->ctx; apr_status_t rv = APR_SUCCESS; apr_off_t passed = 0, nlen; rustls_result rr = RUSTLS_RESULT_OK; apr_size_t in_buf_len; char *in_buf = NULL; fctx->fin_block = block; if (f->c->aborted) { rv = filter_abort(fctx); goto cleanup; } ap_log_error(APLOG_MARK, APLOG_TRACE2, 0, fctx->cc->server, "tls_filter_conn_input, server=%s, mode=%d, block=%d, readbytes=%ld", fctx->cc->server->server_hostname, mode, block, (long)readbytes); rv = progress_tls_atleast_to(fctx, TLS_CONN_ST_TRAFFIC); if (APR_SUCCESS != rv) goto cleanup; /* this also leaves on APR_EAGAIN */ if (!fctx->cc->rustls_connection) { return ap_get_brigade(f->next, bb, mode, block, readbytes); } #if AP_MODULE_MAGIC_AT_LEAST(20200420, 1) ap_filter_reinstate_brigade(f, fctx->fin_plain_bb, NULL); #endif if (AP_MODE_INIT == mode) { /* INIT is used to trigger the handshake, it does not return any traffic data. */ goto cleanup; } /* If we have nothing buffered, try getting more input. * a) ask rustls_connection for decrypted data, if it has any. * Note that only full records can be decrypted. We might have * written TLS data to the session, but that does not mean it * can give unencryted data out again. * b) read TLS bytes from the network and feed them to the rustls session. * c) go back to a) if b) added data. */ while (APR_BRIGADE_EMPTY(fctx->fin_plain_bb)) { apr_size_t rlen = 0; apr_bucket *b; if (fctx->fin_bytes_in_rustls > 0) { in_buf_len = APR_BUCKET_BUFF_SIZE; in_buf = ap_calloc(in_buf_len, sizeof(char)); rr = rustls_connection_read(fctx->cc->rustls_connection, (unsigned char*)in_buf, in_buf_len, &rlen); if (rr == RUSTLS_RESULT_PLAINTEXT_EMPTY) { rr = RUSTLS_RESULT_OK; rlen = 0; } if (rr != RUSTLS_RESULT_OK) goto cleanup; ap_log_cerror(APLOG_MARK, APLOG_TRACE2, rv, fctx->c, "tls_filter_conn_input: got %ld plain bytes from rustls", (long)rlen); if (rlen > 0) { b = apr_bucket_heap_create(in_buf, rlen, free, fctx->c->bucket_alloc); APR_BRIGADE_INSERT_TAIL(fctx->fin_plain_bb, b); } else { free(in_buf); } in_buf = NULL; } if (rlen == 0) { /* that did not produce anything either. try getting more * TLS data from the network into the rustls session. */ fctx->fin_bytes_in_rustls = 0; rv = read_tls_to_rustls(fctx, fctx->fin_max_in_rustls, block, 0); if (APR_SUCCESS != rv) goto cleanup; /* this also leave on APR_EAGAIN */ } } if (AP_MODE_GETLINE == mode) { if (readbytes <= 0) readbytes = HUGE_STRING_LEN; rv = tls_util_brigade_split_line(bb, fctx->fin_plain_bb, block, readbytes, &nlen); if (APR_SUCCESS != rv) goto cleanup; passed += nlen; } else if (AP_MODE_READBYTES == mode) { ap_assert(readbytes > 0); rv = tls_util_brigade_transfer(bb, fctx->fin_plain_bb, readbytes, &nlen); if (APR_SUCCESS != rv) goto cleanup; passed += nlen; } else if (AP_MODE_SPECULATIVE == mode) { ap_assert(readbytes > 0); rv = tls_util_brigade_copy(bb, fctx->fin_plain_bb, readbytes, &nlen); if (APR_SUCCESS != rv) goto cleanup; passed += nlen; } else if (AP_MODE_EXHAUSTIVE == mode) { /* return all we have */ APR_BRIGADE_CONCAT(bb, fctx->fin_plain_bb); } else { /* We do support any other mode */ rv = APR_ENOTIMPL; goto cleanup; } fout_pass_all_to_net(fctx, 0); cleanup: if (NULL != in_buf) free(in_buf); if (APLOGctrace3(fctx->c)) { tls_util_bb_log(fctx->c, APLOG_TRACE3, "tls_input, fctx->fin_plain_bb", fctx->fin_plain_bb); tls_util_bb_log(fctx->c, APLOG_TRACE3, "tls_input, bb", bb); } if (rr != RUSTLS_RESULT_OK) { const char *err_descr = ""; rv = tls_core_error(fctx->c, rr, &err_descr); ap_log_cerror(APLOG_MARK, APLOG_DEBUG, rv, fctx->c, APLOGNO(10355) "tls_filter_conn_input: [%d] %s", (int)rr, err_descr); } else if (APR_STATUS_IS_EAGAIN(rv)) { ap_log_cerror(APLOG_MARK, APLOG_TRACE4, rv, fctx->c, "tls_filter_conn_input: no data available"); } else if (APR_SUCCESS != rv) { ap_log_cerror(APLOG_MARK, APLOG_DEBUG, rv, fctx->c, APLOGNO(10356) "tls_filter_conn_input"); } else { ap_log_cerror(APLOG_MARK, APLOG_TRACE2, rv, fctx->c, "tls_filter_conn_input: passed %ld bytes", (long)passed); } #if AP_MODULE_MAGIC_AT_LEAST(20200420, 1) if (APR_SUCCESS == rv || APR_STATUS_IS_EAGAIN(rv)) { ap_filter_setaside_brigade(f, fctx->fin_plain_bb); } #endif return rv; } static rustls_io_result tls_write_callback( void *userdata, const unsigned char *buf, size_t n, size_t *out_n) { tls_filter_ctx_t *fctx = userdata; apr_status_t rv; if ((apr_off_t)n + fctx->fout_bytes_in_tls_bb >= (apr_off_t)fctx->fout_auto_flush_size) { apr_bucket *b = apr_bucket_transient_create((const char*)buf, n, fctx->fout_tls_bb->bucket_alloc); APR_BRIGADE_INSERT_TAIL(fctx->fout_tls_bb, b); fctx->fout_bytes_in_tls_bb += (apr_off_t)n; rv = fout_pass_tls_to_net(fctx); *out_n = n; } else { rv = apr_brigade_write(fctx->fout_tls_bb, NULL, NULL, (const char*)buf, n); if (APR_SUCCESS != rv) goto cleanup; fctx->fout_bytes_in_tls_bb += (apr_off_t)n; *out_n = n; } cleanup: ap_log_error(APLOG_MARK, APLOG_TRACE5, rv, fctx->cc->server, "tls_write_callback: %ld bytes", (long)n); return APR_TO_OS_ERROR(rv); } static rustls_io_result tls_write_vectored_callback( void *userdata, const rustls_iovec *riov, size_t count, size_t *out_n) { tls_filter_ctx_t *fctx = userdata; const struct iovec *iov = (const struct iovec*)riov; apr_status_t rv; size_t i, n = 0; apr_bucket *b; for (i = 0; i < count; ++i, ++iov) { b = apr_bucket_transient_create((const char*)iov->iov_base, iov->iov_len, fctx->fout_tls_bb->bucket_alloc); APR_BRIGADE_INSERT_TAIL(fctx->fout_tls_bb, b); n += iov->iov_len; } fctx->fout_bytes_in_tls_bb += (apr_off_t)n; rv = fout_pass_tls_to_net(fctx); *out_n = n; ap_log_error(APLOG_MARK, APLOG_TRACE5, rv, fctx->cc->server, "tls_write_vectored_callback: %ld bytes in %d slices", (long)n, (int)count); return APR_TO_OS_ERROR(rv); } #define TLS_WRITE_VECTORED 1 /** * Read TLS encrypted data from cc->rustls_connection> and pass it down * Apache's filter chain to the network. * * For now, we always FLUSH the data, since that is what we need during handshakes. */ static apr_status_t fout_pass_rustls_to_tls(tls_filter_ctx_t *fctx) { apr_status_t rv = APR_SUCCESS; if (rustls_connection_wants_write(fctx->cc->rustls_connection)) { size_t dlen; int os_err; if (TLS_WRITE_VECTORED) { do { os_err = rustls_connection_write_tls_vectored( fctx->cc->rustls_connection, tls_write_vectored_callback, fctx, &dlen); if (os_err) { rv = APR_FROM_OS_ERROR(os_err); goto cleanup; } } while (rustls_connection_wants_write(fctx->cc->rustls_connection)); } else { do { os_err = rustls_connection_write_tls( fctx->cc->rustls_connection, tls_write_callback, fctx, &dlen); if (os_err) { rv = APR_FROM_OS_ERROR(os_err); goto cleanup; } } while (rustls_connection_wants_write(fctx->cc->rustls_connection)); ap_log_cerror(APLOG_MARK, APLOG_TRACE3, rv, fctx->c, "fout_pass_rustls_to_tls, %ld bytes ready for network", (long)fctx->fout_bytes_in_tls_bb); fctx->fout_bytes_in_rustls = 0; } } cleanup: return rv; } static apr_status_t fout_pass_buf_to_rustls( tls_filter_ctx_t *fctx, const char *buf, apr_size_t len) { apr_status_t rv = APR_SUCCESS; rustls_result rr = RUSTLS_RESULT_OK; apr_size_t written; while (len) { /* check if we will exceed the limit of data in rustls. * rustls does not guarantuee that it will accept all data, so we * iterate and flush when needed. */ if (fctx->fout_bytes_in_rustls + (apr_off_t)len > (apr_off_t)fctx->fout_max_in_rustls) { rv = fout_pass_rustls_to_tls(fctx); if (APR_SUCCESS != rv) goto cleanup; } rr = rustls_connection_write(fctx->cc->rustls_connection, (const unsigned char*)buf, len, &written); if (rr != RUSTLS_RESULT_OK) goto cleanup; ap_assert(written <= len); fctx->fout_bytes_in_rustls += (apr_off_t)written; buf += written; len -= written; if (written == 0) { rv = APR_EAGAIN; ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, fctx->c, APLOGNO(10357) "fout_pass_buf_to_rustls: not read by rustls at all"); goto cleanup; } } cleanup: if (rr != RUSTLS_RESULT_OK) { const char *err_descr = ""; rv = tls_core_error(fctx->c, rr, &err_descr); ap_log_cerror(APLOG_MARK, APLOG_DEBUG, rv, fctx->c, APLOGNO(10358) "fout_pass_buf_to_tls to rustls: [%d] %s", (int)rr, err_descr); } return rv; } static apr_status_t fout_pass_all_to_tls(tls_filter_ctx_t *fctx) { apr_status_t rv = APR_SUCCESS; if (fctx->fout_buf_plain_len) { rv = fout_pass_buf_to_rustls(fctx, fctx->fout_buf_plain, fctx->fout_buf_plain_len); ap_log_cerror(APLOG_MARK, APLOG_TRACE2, rv, fctx->c, "fout_pass_all_to_tls: %ld plain bytes written to rustls", (long)fctx->fout_buf_plain_len); if (APR_SUCCESS != rv) goto cleanup; fctx->fout_buf_plain_len = 0; } rv = fout_pass_rustls_to_tls(fctx); cleanup: return rv; } static apr_status_t fout_pass_all_to_net(tls_filter_ctx_t *fctx, int flush) { apr_status_t rv; rv = fout_pass_all_to_tls(fctx); if (APR_SUCCESS != rv) goto cleanup; if (flush) { apr_bucket *b = apr_bucket_flush_create(fctx->fout_tls_bb->bucket_alloc); APR_BRIGADE_INSERT_TAIL(fctx->fout_tls_bb, b); } rv = fout_pass_tls_to_net(fctx); cleanup: return rv; } static apr_status_t fout_add_bucket_to_plain(tls_filter_ctx_t *fctx, apr_bucket *b) { const char *data; apr_size_t dlen, buf_remain; apr_status_t rv = APR_SUCCESS; ap_assert((apr_size_t)-1 != b->length); if (b->length == 0) { apr_bucket_delete(b); goto cleanup; } buf_remain = fctx->fout_buf_plain_size - fctx->fout_buf_plain_len; if (buf_remain == 0) { rv = fout_pass_all_to_tls(fctx); if (APR_SUCCESS != rv) goto cleanup; buf_remain = fctx->fout_buf_plain_size - fctx->fout_buf_plain_len; ap_assert(buf_remain > 0); } if (b->length > buf_remain) { apr_bucket_split(b, buf_remain); } rv = apr_bucket_read(b, &data, &dlen, APR_BLOCK_READ); if (APR_SUCCESS != rv) goto cleanup; /*if (dlen > TLS_PREF_PLAIN_CHUNK_SIZE)*/ ap_assert(dlen <= buf_remain); memcpy(fctx->fout_buf_plain + fctx->fout_buf_plain_len, data, dlen); fctx->fout_buf_plain_len += dlen; apr_bucket_delete(b); cleanup: return rv; } static apr_status_t fout_add_bucket_to_tls(tls_filter_ctx_t *fctx, apr_bucket *b) { apr_status_t rv; rv = fout_pass_all_to_tls(fctx); if (APR_SUCCESS != rv) goto cleanup; APR_BUCKET_REMOVE(b); APR_BRIGADE_INSERT_TAIL(fctx->fout_tls_bb, b); if (AP_BUCKET_IS_EOC(b)) { rustls_connection_send_close_notify(fctx->cc->rustls_connection); fctx->cc->state = TLS_CONN_ST_NOTIFIED; rv = fout_pass_rustls_to_tls(fctx); if (APR_SUCCESS != rv) goto cleanup; } cleanup: return rv; } static apr_status_t fout_append_plain(tls_filter_ctx_t *fctx, apr_bucket *b) { const char *data; apr_size_t dlen, buf_remain; rustls_result rr = RUSTLS_RESULT_OK; apr_status_t rv = APR_SUCCESS; const char *lbuf = NULL; int flush = 0; if (b) { /* if our plain buffer is full, now is a good time to flush it. */ buf_remain = fctx->fout_buf_plain_size - fctx->fout_buf_plain_len; if (buf_remain == 0) { rv = fout_pass_all_to_tls(fctx); if (APR_SUCCESS != rv) goto cleanup; buf_remain = fctx->fout_buf_plain_size - fctx->fout_buf_plain_len; ap_assert(buf_remain > 0); } /* Resolve any indeterminate bucket to a "real" one by reading it. */ if ((apr_size_t)-1 == b->length) { rv = apr_bucket_read(b, &data, &dlen, APR_BLOCK_READ); if (APR_STATUS_IS_EOF(rv)) { apr_bucket_delete(b); goto maybe_flush; } else if (APR_SUCCESS != rv) goto cleanup; } /* Now `b` is the bucket that we need to append and consume */ if (APR_BUCKET_IS_METADATA(b)) { /* outgoing buckets: * [PLAINDATA META PLAINDATA META META] * need to become: * [TLSDATA META TLSDATA META META] * because we need to send the meta buckets down the * network filters. */ rv = fout_add_bucket_to_tls(fctx, b); flush = 1; } else if (b->length == 0) { apr_bucket_delete(b); } else if (b->length < 1024 || fctx->fout_buf_plain_len > 0) { /* we want to buffer small chunks to create larger TLS records and * not leak security relevant information. So, we buffer small * chunks and add (parts of) later, larger chunks if the plain * buffer contains data. */ rv = fout_add_bucket_to_plain(fctx, b); if (APR_SUCCESS != rv) goto cleanup; } else { /* we have a large chunk and our plain buffer is empty, write it * directly into rustls. */ #define TLS_FILE_CHUNK_SIZE 4 * TLS_PREF_PLAIN_CHUNK_SIZE if (b->length > TLS_FILE_CHUNK_SIZE) { apr_bucket_split(b, TLS_FILE_CHUNK_SIZE); } if (APR_BUCKET_IS_FILE(b) && (lbuf = malloc(b->length))) { /* A file bucket is a most wonderous thing. Since the dawn of time, * it has been subject to many optimizations for efficient handling * of large data in the server: * - unless one reads from it, it will just consist of a file handle * and the offset+length information. * - a apr_bucket_read() will transform itself to a bucket holding * some 8000 bytes of data (APR_BUCKET_BUFF_SIZE), plus a following * bucket that continues to hold the file handle and updated offsets/length * information. * Using standard bucket brigade handling, one would send 8000 bytes * chunks to the network and that is fine for many occasions. * - to have improved performance, the http: network handler takes * the file handle directly and uses sendfile() when the OS supports it. * - But there is not sendfile() for TLS (netflix did some experiments). * So. * rustls will try to collect max length traffic data into ont TLS * message, but it can only work with what we gave it. If we give it buffers * that fit what it wants to assemble already, its work is much easier. * * We can read file buckets in large chunks than APR_BUCKET_BUFF_SIZE, * with a bit of knowledge about how they work. */ apr_bucket_file *f = (apr_bucket_file *)b->data; apr_file_t *fd = f->fd; apr_off_t offset = b->start; dlen = b->length; rv = apr_file_seek(fd, APR_SET, &offset); if (APR_SUCCESS != rv) goto cleanup; rv = apr_file_read(fd, (void*)lbuf, &dlen); if (APR_SUCCESS != rv && !APR_STATUS_IS_EOF(rv)) goto cleanup; rv = fout_pass_buf_to_rustls(fctx, lbuf, dlen); if (APR_SUCCESS != rv) goto cleanup; apr_bucket_delete(b); } else { rv = apr_bucket_read(b, &data, &dlen, APR_BLOCK_READ); if (APR_SUCCESS != rv) goto cleanup; rv = fout_pass_buf_to_rustls(fctx, data, dlen); if (APR_SUCCESS != rv) goto cleanup; apr_bucket_delete(b); } } } maybe_flush: if (flush) { rv = fout_pass_all_to_net(fctx, 1); if (APR_SUCCESS != rv) goto cleanup; } cleanup: if (lbuf) free((void*)lbuf); if (rr != RUSTLS_RESULT_OK) { const char *err_descr = ""; rv = tls_core_error(fctx->c, rr, &err_descr); ap_log_cerror(APLOG_MARK, APLOG_DEBUG, rv, fctx->c, APLOGNO(10359) "write_bucket_to_rustls: [%d] %s", (int)rr, err_descr); } return rv; } /** * The connection filter converting plain, unencrypted traffic data into TLS * encrypted bytes and send the down the Apache filter chain out to the network. * * the data to send, including "meta data" such as FLUSH indicators * to force filters to write any data set aside (an apache term for * 'buffering'). * The buckets in need to be completely consumed, e.g. will be * empty on a successful return. but unless FLUSHed, filters may hold * buckets back internally, for various reasons. However they always * need to be processed in the order they arrive. */ static apr_status_t filter_conn_output( ap_filter_t *f, apr_bucket_brigade *bb) { tls_filter_ctx_t *fctx = f->ctx; apr_status_t rv = APR_SUCCESS; rustls_result rr = RUSTLS_RESULT_OK; if (f->c->aborted) { ap_log_cerror(APLOG_MARK, APLOG_TRACE4, 0, fctx->c, "tls_filter_conn_output: aborted conn"); apr_brigade_cleanup(bb); rv = APR_ECONNABORTED; goto cleanup; } rv = progress_tls_atleast_to(fctx, TLS_CONN_ST_TRAFFIC); if (APR_SUCCESS != rv) goto cleanup; /* this also leaves on APR_EAGAIN */ if (fctx->cc->state == TLS_CONN_ST_DONE) { /* have done everything, just pass through */ ap_log_cerror(APLOG_MARK, APLOG_TRACE4, 0, fctx->c, "tls_filter_conn_output: tls session is already done"); rv = ap_pass_brigade(f->next, bb); goto cleanup; } ap_log_error(APLOG_MARK, APLOG_TRACE2, 0, fctx->cc->server, "tls_filter_conn_output, server=%s", fctx->cc->server->server_hostname); if (APLOGctrace5(fctx->c)) { tls_util_bb_log(fctx->c, APLOG_TRACE5, "filter_conn_output", bb); } while (!APR_BRIGADE_EMPTY(bb)) { rv = fout_append_plain(fctx, APR_BRIGADE_FIRST(bb)); if (APR_SUCCESS != rv) goto cleanup; } if (APLOGctrace5(fctx->c)) { tls_util_bb_log(fctx->c, APLOG_TRACE5, "filter_conn_output, processed plain", bb); tls_util_bb_log(fctx->c, APLOG_TRACE5, "filter_conn_output, tls", fctx->fout_tls_bb); } cleanup: if (rr != RUSTLS_RESULT_OK) { const char *err_descr = ""; rv = tls_core_error(fctx->c, rr, &err_descr); ap_log_cerror(APLOG_MARK, APLOG_DEBUG, rv, fctx->c, APLOGNO(10360) "tls_filter_conn_output: [%d] %s", (int)rr, err_descr); } else { ap_log_cerror(APLOG_MARK, APLOG_TRACE2, rv, fctx->c, "tls_filter_conn_output: done"); } return rv; } int tls_filter_pre_conn_init(conn_rec *c) { tls_conf_conn_t *cc; tls_filter_ctx_t *fctx; if (OK != tls_core_pre_conn_init(c)) { return DECLINED; } ap_log_error(APLOG_MARK, APLOG_TRACE2, 0, c->base_server, "tls_filter_pre_conn_init on %s", c->base_server->server_hostname); cc = tls_conf_conn_get(c); ap_assert(cc); fctx = apr_pcalloc(c->pool, sizeof(*fctx)); fctx->c = c; fctx->cc = cc; cc->filter_ctx = fctx; /* a bit tricky: registering out filters returns the ap_filter_t* * that it created for it. The ->next field points always * to the filter "below" our filter. That will be other registered * filters and last, but not least, the network filter on the socket. * * Therefore, wenn we need to read/write TLS data during handshake, we can * pass the data to/call on ->next- Since ->next can change during the setup of * a connections (other modules register also sth.), we keep the ap_filter_t* * returned here, since httpd core will update the ->next whenever someone * adds a filter or removes one. This can potentially happen all the time. */ fctx->fin_ctx = ap_add_input_filter(TLS_FILTER_RAW, fctx, NULL, c); fctx->fin_tls_bb = apr_brigade_create(c->pool, c->bucket_alloc); fctx->fin_tls_buffer_bb = NULL; fctx->fin_plain_bb = apr_brigade_create(c->pool, c->bucket_alloc); fctx->fout_ctx = ap_add_output_filter(TLS_FILTER_RAW, fctx, NULL, c); fctx->fout_tls_bb = apr_brigade_create(c->pool, c->bucket_alloc); fctx->fout_buf_plain_size = APR_BUCKET_BUFF_SIZE; fctx->fout_buf_plain = apr_pcalloc(c->pool, fctx->fout_buf_plain_size); fctx->fout_buf_plain_len = 0; /* Let the filters have 2 max-length TLS Messages in the rustls buffers. * The effects we would like to achieve here are: * 1. pass data out, so that every bucket becomes its own TLS message. * This hides, if possible, the length of response parts. * If we give rustls enough plain data, it will use the max TLS message * size and things are more hidden. But we can only write what the application * or protocol gives us. * 2. max length records result in less overhead for all layers involved. * 3. a TLS message from the client can only be decrypted when it has * completely arrived. If we provide rustls with enough data (if the * network has it for us), it should always be able to decrypt at least * one TLS message and we have plain bytes to forward to the protocol * handler. */ fctx->fin_max_in_rustls = 4 * TLS_REC_MAX_SIZE; fctx->fout_max_in_rustls = 4 * TLS_PREF_PLAIN_CHUNK_SIZE; fctx->fout_auto_flush_size = 2 * TLS_REC_MAX_SIZE; return OK; } void tls_filter_conn_init(conn_rec *c) { tls_conf_conn_t *cc = tls_conf_conn_get(c); if (cc && cc->filter_ctx && !cc->outgoing) { /* We are one in a row of hooks that - possibly - want to process this * connection, the (HTTP) protocol handlers among them. * * For incoming connections, we need to select the protocol to use NOW, * so that the later protocol handlers do the right thing. * Send an INIT down the input filter chain to trigger the TLS handshake, * which will select a protocol via ALPN. */ apr_bucket_brigade* temp; ap_log_error(APLOG_MARK, APLOG_TRACE2, 0, c->base_server, "tls_filter_conn_init on %s, triggering handshake", c->base_server->server_hostname); temp = apr_brigade_create(c->pool, c->bucket_alloc); ap_get_brigade(c->input_filters, temp, AP_MODE_INIT, APR_BLOCK_READ, 0); apr_brigade_destroy(temp); } } void tls_filter_register( apr_pool_t *pool) { (void)pool; ap_register_input_filter(TLS_FILTER_RAW, filter_conn_input, NULL, AP_FTYPE_CONNECTION + 5); ap_register_output_filter(TLS_FILTER_RAW, filter_conn_output, NULL, AP_FTYPE_CONNECTION + 5); }