| /** @file |
| |
| Http2Stream.cc |
| |
| @section license License |
| |
| 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 "proxy/http2/Http2Stream.h" |
| |
| #include "proxy/http2/HTTP2.h" |
| #include "proxy/http2/Http2ClientSession.h" |
| #include "proxy/http2/Http2ServerSession.h" |
| #include "proxy/http/HttpDebugNames.h" |
| #include "proxy/http/HttpSM.h" |
| #include "tscore/Diags.h" |
| #include "tscore/HTTPVersion.h" |
| #include "tscore/ink_assert.h" |
| #include "tsutil/DbgCtl.h" |
| |
| #include <numeric> |
| |
| #define REMEMBER(e, r) \ |
| { \ |
| this->_history.push_back(MakeSourceLocation(), e, r); \ |
| } |
| |
| namespace |
| { |
| DbgCtl dbg_ctl_http2_stream{"http2_stream"}; |
| |
| } // end anonymous namespace |
| |
| #define Http2StreamDebug(fmt, ...) \ |
| SsnDbg(_proxy_ssn, dbg_ctl_http2_stream, "[%" PRId64 "] [%u] " fmt, _proxy_ssn->connection_id(), this->get_id(), ##__VA_ARGS__); |
| |
| ClassAllocator<Http2Stream> http2StreamAllocator("http2StreamAllocator"); |
| |
| Http2Stream::Http2Stream(ProxySession *session, Http2StreamId sid, ssize_t initial_peer_rwnd, ssize_t initial_local_rwnd, |
| bool registered_stream) |
| : super(session), _id(sid), _registered_stream(registered_stream), _peer_rwnd(initial_peer_rwnd), _local_rwnd(initial_local_rwnd) |
| { |
| SET_HANDLER(&Http2Stream::main_event_handler); |
| |
| this->mark_milestone(Http2StreamMilestone::OPEN); |
| |
| this->_sm = nullptr; |
| this->_thread = this_ethread(); |
| this->_state = Http2StreamState::HTTP2_STREAM_STATE_IDLE; |
| |
| auto const *proxy_session = get_proxy_ssn(); |
| ink_assert(proxy_session != nullptr); |
| auto const *h2_session = dynamic_cast<Http2CommonSession const *>(proxy_session); |
| ink_assert(h2_session != nullptr); |
| this->_is_outbound = h2_session->is_outbound(); |
| |
| this->_reader = this->_receive_buffer.alloc_reader(); |
| |
| if (this->is_outbound_connection()) { // Flip the sense of the expected headers. Fix naming later |
| _receive_header.create(HTTPType::RESPONSE); |
| _send_header.create(HTTPType::REQUEST, HTTP_2_0); |
| } else { |
| this->upstream_outbound_options = *(session->accept_options); |
| _receive_header.create(HTTPType::REQUEST); |
| _send_header.create(HTTPType::RESPONSE, HTTP_2_0); |
| } |
| |
| http_parser_init(&http_parser); |
| } |
| |
| Http2Stream::~Http2Stream() |
| { |
| REMEMBER(NO_EVENT, this->reentrancy_count); |
| Http2StreamDebug("Destroy stream, sent %" PRIu64 " bytes, registered: %s", this->bytes_sent, |
| (_registered_stream ? "true" : "false")); |
| |
| // In the case of a temporary stream used to parse the header to keep the HPACK |
| // up to date, there may not be a mutex. Nothing was set up, so nothing to |
| // clean up in the destructor |
| if (this->mutex == nullptr) { |
| return; |
| } |
| |
| SCOPED_MUTEX_LOCK(lock, this->mutex, this_ethread()); |
| // Clean up after yourself if this was an EOS |
| ink_release_assert(this->closed); |
| ink_release_assert(reentrancy_count == 0); |
| |
| uint64_t cid = 0; |
| |
| if (_registered_stream) { |
| // Safe to initiate SSN_CLOSE if this is the last stream |
| if (_proxy_ssn) { |
| cid = _proxy_ssn->connection_id(); |
| |
| SCOPED_MUTEX_LOCK(lock, _proxy_ssn->mutex, this_ethread()); |
| Http2ConnectionState &connection_state = this->get_connection_state(); |
| |
| // Make sure the stream is removed from the stream list and priority tree |
| // In many cases, this has been called earlier, so this call is a no-op |
| connection_state.delete_stream(this); |
| |
| connection_state.decrement_peer_stream_count(); |
| |
| // Update session's stream counts, so it accurately goes into keep-alive state |
| connection_state.release_stream(); |
| |
| // Do not access `_proxy_ssn` in below. It might be freed by `release_stream`. |
| } |
| } // Otherwise, not registered with the connection_state (i.e. a temporary stream used for HPACK header processing) |
| |
| // Clean up the write VIO in case of inactivity timeout |
| this->do_io_write(nullptr, 0, nullptr); |
| |
| this->_milestones.mark(Http2StreamMilestone::CLOSE); |
| |
| ink_hrtime total_time = this->_milestones.elapsed(Http2StreamMilestone::OPEN, Http2StreamMilestone::CLOSE); |
| Metrics::Counter::increment(http2_rsb.total_transactions_time, total_time); |
| |
| // Slow Log |
| if (Http2::stream_slow_log_threshold != 0 && ink_hrtime_from_msec(Http2::stream_slow_log_threshold) < total_time) { |
| Error("[%" PRIu64 "] [%" PRIu32 "] [%" PRId64 "] Slow H2 Stream: " |
| "open: %" PRIu64 " " |
| "dec_hdrs: %.3f " |
| "txn: %.3f " |
| "enc_hdrs: %.3f " |
| "tx_hdrs: %.3f " |
| "tx_data: %.3f " |
| "close: %.3f", |
| cid, static_cast<uint32_t>(this->_id), this->_http_sm_id, |
| ink_hrtime_to_msec(this->_milestones[Http2StreamMilestone::OPEN]), |
| this->_milestones.difference_sec(Http2StreamMilestone::OPEN, Http2StreamMilestone::START_DECODE_HEADERS), |
| this->_milestones.difference_sec(Http2StreamMilestone::OPEN, Http2StreamMilestone::START_TXN), |
| this->_milestones.difference_sec(Http2StreamMilestone::OPEN, Http2StreamMilestone::START_ENCODE_HEADERS), |
| this->_milestones.difference_sec(Http2StreamMilestone::OPEN, Http2StreamMilestone::START_TX_HEADERS_FRAMES), |
| this->_milestones.difference_sec(Http2StreamMilestone::OPEN, Http2StreamMilestone::START_TX_DATA_FRAMES), |
| this->_milestones.difference_sec(Http2StreamMilestone::OPEN, Http2StreamMilestone::CLOSE)); |
| } |
| |
| _receive_header.destroy(); |
| _send_header.destroy(); |
| |
| // Drop references to all buffer data |
| this->_receive_buffer.clear(); |
| |
| // Free the mutexes in the VIO |
| read_vio.mutex.clear(); |
| write_vio.mutex.clear(); |
| |
| ats_free(header_blocks); |
| |
| _clear_timers(); |
| clear_io_events(); |
| http_parser_clear(&http_parser); |
| } |
| |
| int |
| Http2Stream::main_event_handler(int event, void *edata) |
| { |
| SCOPED_MUTEX_LOCK(lock, this->mutex, this_ethread()); |
| REMEMBER(event, this->reentrancy_count); |
| |
| if (!this->_switch_thread_if_not_on_right_thread(event, edata)) { |
| // Not on the right thread |
| return 0; |
| } |
| ink_release_assert(this->_thread == this_ethread()); |
| |
| Event *e = static_cast<Event *>(edata); |
| reentrancy_count++; |
| if (e == _read_vio_event) { |
| _read_vio_event = nullptr; |
| this->signal_read_event(e->callback_event); |
| reentrancy_count--; |
| return 0; |
| } else if (e == _write_vio_event) { |
| _write_vio_event = nullptr; |
| this->signal_write_event(e->callback_event); |
| reentrancy_count--; |
| return 0; |
| } else if (e == cross_thread_event) { |
| cross_thread_event = nullptr; |
| } else if (e == read_event) { |
| read_event = nullptr; |
| } else if (e == write_event) { |
| write_event = nullptr; |
| } |
| |
| switch (event) { |
| case VC_EVENT_ACTIVE_TIMEOUT: |
| case VC_EVENT_INACTIVITY_TIMEOUT: |
| if (_sm == nullptr && closed != true) { |
| // TIMEOUT without HttpSM - assuming incomplete header timeout |
| Http2StreamDebug("timeout event=%d", event); |
| |
| ip_port_text_buffer ipb; |
| const char *remote_ip = ats_ip_ntop(this->_proxy_ssn->get_remote_addr(), ipb, sizeof(ipb)); |
| |
| Error("HTTP/2 stream error timeout remote_ip=%s session_id=%" PRId64 " stream_id=%u event=%d", remote_ip, |
| this->_proxy_ssn->connection_id(), this->_id, event); |
| |
| // Close stream |
| do_io_close(); |
| terminate_stream = true; |
| |
| // Close connection because this stream doesn't read HEADERS/CONTINUATION frames anymore that makes HPACK dynamic table |
| // out of sync. |
| Http2ConnectionState &connection_state = get_connection_state(); |
| { |
| SCOPED_MUTEX_LOCK(lock, connection_state.mutex, this_ethread()); |
| Http2Error error(Http2ErrorClass::HTTP2_ERROR_CLASS_CONNECTION, Http2ErrorCode::HTTP2_ERROR_COMPRESSION_ERROR, |
| "stream timeout"); |
| connection_state.handleEvent(HTTP2_SESSION_EVENT_ERROR, &error); |
| } |
| } else if (_sm && read_vio.ntodo() > 0) { |
| this->signal_read_event(event); |
| } else if (_sm && write_vio.ntodo() > 0) { |
| this->signal_write_event(event); |
| } else { |
| Warning("HTTP/2 unknown case of %d event - session_id=%" PRId64 " stream_id=%u", event, this->_proxy_ssn->connection_id(), |
| this->_id); |
| } |
| break; |
| case VC_EVENT_WRITE_READY: |
| case VC_EVENT_WRITE_COMPLETE: |
| _timeout.update_inactivity(); |
| if (e->cookie == &write_vio) { |
| if (write_vio.mutex && write_vio.cont && this->_sm) { |
| this->signal_write_event(event); |
| } |
| } else { |
| this->update_write_request(true); |
| } |
| break; |
| case VC_EVENT_READ_COMPLETE: |
| read_vio.nbytes = read_vio.ndone; |
| /* fall through */ |
| case VC_EVENT_READ_READY: |
| _timeout.update_inactivity(); |
| if (e->cookie == &read_vio) { |
| if (read_vio.mutex && read_vio.cont && this->_sm) { |
| this->signal_read_event(event); |
| } |
| } else { |
| this->update_read_request(true); |
| } |
| break; |
| case VC_EVENT_EOS: |
| if (e->cookie == &read_vio) { |
| SCOPED_MUTEX_LOCK(lock, read_vio.mutex, this_ethread()); |
| if (read_vio.cont) { |
| read_vio.cont->handleEvent(VC_EVENT_EOS, &read_vio); |
| } |
| } else if (e->cookie == &write_vio) { |
| SCOPED_MUTEX_LOCK(lock, write_vio.mutex, this_ethread()); |
| if (write_vio.cont) { |
| write_vio.cont->handleEvent(VC_EVENT_EOS, &write_vio); |
| } |
| } |
| break; |
| } |
| reentrancy_count--; |
| // Clean stream up if the terminate flag is set and we are at the bottom of the handler stack |
| terminate_if_possible(); |
| |
| return 0; |
| } |
| |
| Http2ErrorCode |
| Http2Stream::decode_header_blocks(HpackHandle &hpack_handle, uint32_t maximum_table_size) |
| { |
| Http2ErrorCode error = |
| http2_decode_header_blocks(&_receive_header, (const uint8_t *)header_blocks, header_blocks_length, nullptr, hpack_handle, |
| _trailing_header_is_possible, maximum_table_size, this->is_outbound_connection()); |
| if (error != Http2ErrorCode::HTTP2_ERROR_NO_ERROR) { |
| Http2StreamDebug("Error decoding header blocks: %u", static_cast<uint32_t>(error)); |
| } |
| return error; |
| } |
| |
| void |
| Http2Stream::send_headers(Http2ConnectionState & /* cstate ATS_UNUSED */) |
| { |
| if (closed) { |
| return; |
| } |
| REMEMBER(NO_EVENT, this->reentrancy_count); |
| |
| // Convert header to HTTP/1.1 format. Trailing headers need no conversion |
| // because they, by definition, do not contain pseudo headers. |
| if (this->trailing_header_is_possible()) { |
| Http2StreamDebug("trailing header: Skipping send_headers initialization."); |
| } else { |
| if (http2_convert_header_from_2_to_1_1(&_receive_header) == ParseResult::ERROR) { |
| Http2StreamDebug("Error converting HTTP/2 headers to HTTP/1.1."); |
| if (_receive_header.type_get() == HTTPType::REQUEST) { |
| // There's no way to cause Bad Request directly at this time. |
| // Set an invalid method so it causes an error later. |
| _receive_header.method_set(std::string_view{"\xffVOID", 1}); |
| } |
| } |
| |
| if (_receive_header.type_get() == HTTPType::REQUEST) { |
| // Check whether the request uses CONNECT method |
| auto method{_receive_header.method_get()}; |
| if (method == static_cast<std::string_view>(HTTP_METHOD_CONNECT)) { |
| this->_is_tunneling = true; |
| } |
| } |
| ink_release_assert(this->_sm != nullptr); |
| this->_http_sm_id = this->_sm->sm_id; |
| } |
| |
| // Write header to a buffer. Borrowing logic from HttpSM::write_header_into_buffer. |
| // Seems like a function like this ought to be in HTTPHdr directly |
| int bufindex; |
| int dumpoffset = 0; |
| // The name dumpoffset is used here for parity with |
| // HttpSM::write_header_into_buffer, but create an alias for clarity in the |
| // use of this variable below this loop. |
| int &num_header_bytes = dumpoffset; |
| int done, tmp; |
| do { |
| bufindex = 0; |
| tmp = dumpoffset; |
| IOBufferBlock *block = this->_receive_buffer.get_current_block(); |
| if (!block) { |
| this->_receive_buffer.add_block(); |
| block = this->_receive_buffer.get_current_block(); |
| } |
| done = _receive_header.print(block->end(), block->write_avail(), &bufindex, &tmp); |
| dumpoffset += bufindex; |
| this->_receive_buffer.fill(bufindex); |
| if (!done) { |
| this->_receive_buffer.add_block(); |
| } |
| } while (!done); |
| |
| if (num_header_bytes == 0) { |
| // No data to signal read event |
| return; |
| } |
| |
| // Is the _sm ready to process the header? |
| if (this->read_vio.nbytes > 0) { |
| if (this->receive_end_stream) { |
| // These headers may be standard or trailer headers: |
| // |
| // * If they are standard, then there is no body (note again that the |
| // END_STREAM flag was sent with them), data_length will be 0, and |
| // num_header_bytes will simply be the length of the headers. |
| // |
| // * If they are trailers, then the tunnel behind the SM was set up after |
| // the original headers were sent, and thus nbytes should not include the |
| // size of the original standard headers. Rather, for trailers, nbytes |
| // only needs to include the body length (i.e., DATA frame payload |
| // length), and the length of these current trailer headers calculated in |
| // num_header_bytes. |
| this->read_vio.nbytes = this->data_length + num_header_bytes; |
| Http2StreamDebug("nbytes: %" PRId64 ", ndone: %" PRId64 ", num_header_bytes: %d, data_length: %" PRId64, |
| this->read_vio.nbytes, this->read_vio.ndone, num_header_bytes, this->data_length); |
| if (this->is_outbound_connection()) { |
| // This is a response header. |
| // We don't set ndone because the VC_EVENT_EOS will |
| // first flush the remaining content to consumers, |
| // after which the TUNNEL_EVENT_DONE will be fired |
| // and the header handler will be set up. |
| // The header handler will read the buffer, and not |
| // get its content from the VIO |
| // This can break if the implementation |
| // changes. |
| this->signal_read_event(VC_EVENT_EOS); |
| } else { |
| // Request headers. |
| this->read_vio.ndone = this->read_vio.nbytes; |
| this->signal_read_event(VC_EVENT_READ_COMPLETE); |
| } |
| } else { |
| // End of header but not end of stream, must have some body frames coming |
| this->has_body = true; |
| this->signal_read_event(VC_EVENT_READ_READY); |
| } |
| } |
| } |
| |
| bool |
| Http2Stream::change_state(uint8_t type, uint8_t flags) |
| { |
| auto const initial_state = _state; |
| switch (_state) { |
| case Http2StreamState::HTTP2_STREAM_STATE_IDLE: |
| if (type == HTTP2_FRAME_TYPE_HEADERS) { |
| if (receive_end_stream) { |
| _state = Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_REMOTE; |
| } else if (send_end_stream) { |
| _state = Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_LOCAL; |
| } else { |
| _state = Http2StreamState::HTTP2_STREAM_STATE_OPEN; |
| } |
| } else if (type == HTTP2_FRAME_TYPE_CONTINUATION) { |
| if (receive_end_stream) { |
| _state = Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_REMOTE; |
| } else if (send_end_stream) { |
| _state = Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_LOCAL; |
| } else { |
| _state = Http2StreamState::HTTP2_STREAM_STATE_OPEN; |
| } |
| } else if (type == HTTP2_FRAME_TYPE_PUSH_PROMISE) { |
| _state = Http2StreamState::HTTP2_STREAM_STATE_RESERVED_LOCAL; |
| } else if (type == HTTP2_FRAME_TYPE_RST_STREAM) { |
| _state = Http2StreamState::HTTP2_STREAM_STATE_CLOSED; |
| } else { |
| return false; |
| } |
| break; |
| |
| case Http2StreamState::HTTP2_STREAM_STATE_OPEN: |
| if (type == HTTP2_FRAME_TYPE_RST_STREAM) { |
| _state = Http2StreamState::HTTP2_STREAM_STATE_CLOSED; |
| } else if (type == HTTP2_FRAME_TYPE_HEADERS || type == HTTP2_FRAME_TYPE_DATA) { |
| if (receive_end_stream) { |
| if (send_end_stream) { |
| _state = Http2StreamState::HTTP2_STREAM_STATE_CLOSED; |
| } else { |
| _state = Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_REMOTE; |
| } |
| } else if (send_end_stream) { |
| if (receive_end_stream) { |
| _state = Http2StreamState::HTTP2_STREAM_STATE_CLOSED; |
| } else { |
| _state = Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_LOCAL; |
| } |
| } else { |
| // Do not change state |
| } |
| } else { |
| // A stream in the "open" state may be used by both peers to send frames of any type. |
| return true; |
| } |
| break; |
| |
| case Http2StreamState::HTTP2_STREAM_STATE_RESERVED_LOCAL: |
| if (type == HTTP2_FRAME_TYPE_HEADERS) { |
| if (flags & HTTP2_FLAGS_HEADERS_END_HEADERS) { |
| _state = Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_REMOTE; |
| } |
| } else if (type == HTTP2_FRAME_TYPE_CONTINUATION) { |
| if (flags & HTTP2_FLAGS_CONTINUATION_END_HEADERS) { |
| _state = Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_REMOTE; |
| } |
| } else { |
| return false; |
| } |
| break; |
| |
| case Http2StreamState::HTTP2_STREAM_STATE_RESERVED_REMOTE: |
| // Currently ATS supports only HTTP/2 server features |
| return false; |
| |
| case Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_LOCAL: |
| if (type == HTTP2_FRAME_TYPE_RST_STREAM || receive_end_stream) { |
| _state = Http2StreamState::HTTP2_STREAM_STATE_CLOSED; |
| } |
| break; |
| |
| case Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_REMOTE: |
| if (type == HTTP2_FRAME_TYPE_RST_STREAM || send_end_stream) { |
| _state = Http2StreamState::HTTP2_STREAM_STATE_CLOSED; |
| } else if (type == HTTP2_FRAME_TYPE_HEADERS) { // w/o END_STREAM flag |
| // No state change here. Expect a following DATA frame with END_STREAM flag. |
| return true; |
| } else if (type == HTTP2_FRAME_TYPE_CONTINUATION) { // w/o END_STREAM flag |
| // No state change here. Expect a following DATA frame with END_STREAM flag. |
| return true; |
| } |
| break; |
| |
| case Http2StreamState::HTTP2_STREAM_STATE_CLOSED: |
| // No state changing |
| return true; |
| |
| default: |
| return false; |
| } |
| |
| Http2StreamDebug("%s -> %s", Http2DebugNames::get_state_name(initial_state), Http2DebugNames::get_state_name(_state)); |
| |
| return true; |
| } |
| |
| VIO * |
| Http2Stream::do_io_read(Continuation *c, int64_t nbytes, MIOBuffer *buf) |
| { |
| if (buf) { |
| read_vio.set_writer(buf); |
| } else { |
| read_vio.buffer.clear(); |
| } |
| |
| read_vio.mutex = c ? c->mutex : this->mutex; |
| read_vio.cont = c; |
| read_vio.nbytes = nbytes; |
| read_vio.ndone = 0; |
| read_vio.vc_server = this; |
| read_vio.op = VIO::READ; |
| |
| // TODO: re-enable read_vio |
| |
| return &read_vio; |
| } |
| |
| VIO * |
| Http2Stream::do_io_write(Continuation *c, int64_t nbytes, IOBufferReader *abuffer, bool /* owner ATS_UNUSED */) |
| { |
| if (abuffer) { |
| write_vio.set_reader(abuffer); |
| } else { |
| write_vio.buffer.clear(); |
| } |
| write_vio.mutex = c ? c->mutex : this->mutex; |
| write_vio.cont = c; |
| write_vio.nbytes = nbytes; |
| write_vio.ndone = 0; |
| write_vio.vc_server = this; |
| write_vio.op = VIO::WRITE; |
| _send_reader = abuffer; |
| |
| if (c != nullptr && nbytes > 0 && this->is_state_writeable()) { |
| update_write_request(false); |
| } else if (!this->is_state_writeable()) { |
| // Cannot start a write on a closed stream |
| return nullptr; |
| } |
| return &write_vio; |
| } |
| |
| // Initiated from SM |
| void |
| Http2Stream::do_io_close(int /* flags */) |
| { |
| SCOPED_MUTEX_LOCK(lock, this->mutex, this_ethread()); |
| |
| if (!closed) { |
| REMEMBER(NO_EVENT, this->reentrancy_count); |
| Http2StreamDebug("do_io_close"); |
| |
| // Let the other end know we are going away. |
| // We only need to do this for the client side since we only need to pass through RST_STREAM |
| // from the server. If a client sends a RST_STREAM, we need to keep the server side alive so |
| // the background fill can function as intended. |
| // |
| // In the half-closed(local) state, server can close stream actively by sending RST_STREAM frame |
| if (!this->is_outbound_connection() && |
| (this->is_state_writeable() || _state == Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_LOCAL)) { |
| this->get_connection_state().send_rst_stream_frame(_id, Http2ErrorCode::HTTP2_ERROR_NO_ERROR); |
| } |
| |
| // When we get here, the SM has initiated the shutdown. Either it received a WRITE_COMPLETE, or it is shutting down. Any |
| // remaining IO operations back to client should be abandoned. The SM-side buffers backing these operations will be deleted |
| // by the time this is called from transaction_done. |
| closed = true; |
| |
| // Adjust state, so we don't process any more data |
| _state = Http2StreamState::HTTP2_STREAM_STATE_CLOSED; |
| |
| _clear_timers(); |
| clear_io_events(); |
| |
| // Otherwise, Wait until transaction_done is called from HttpSM to signal that the TXN_CLOSE hook has been executed |
| } |
| } |
| |
| /* |
| * HttpSM has called TXN_close hooks. |
| */ |
| void |
| Http2Stream::transaction_done() |
| { |
| SCOPED_MUTEX_LOCK(lock, this->mutex, this_ethread()); |
| super::transaction_done(); |
| |
| if (!closed && _state == Http2StreamState::HTTP2_STREAM_STATE_CLOSED) { |
| do_io_close(); // Make sure we've been closed. If we didn't close the _proxy_ssn session better still be open |
| } |
| Http2ConnectionState &state = this->get_connection_state(); |
| ink_release_assert(closed || !state.is_state_closed()); |
| |
| // HttpSM is gone, no more reading or writing |
| _sm = nullptr; |
| read_vio.disable(); |
| write_vio.disable(); |
| |
| if (closed) { |
| // Safe to initiate SSN_CLOSE if this is the last stream |
| ink_assert(cross_thread_event == nullptr); |
| // Schedule the destroy to occur after we unwind here. IF we call directly, may delete with reference on the stack. |
| terminate_stream = true; |
| terminate_if_possible(); |
| } |
| } |
| |
| void |
| Http2Stream::terminate_if_possible() |
| { |
| // if (terminate_stream && reentrancy_count == 0) { |
| if (reentrancy_count == 0 && closed && terminate_stream) { |
| REMEMBER(NO_EVENT, this->reentrancy_count); |
| |
| SCOPED_MUTEX_LOCK(lock, _proxy_ssn->mutex, this_ethread()); |
| THREAD_FREE(this, http2StreamAllocator, this_ethread()); |
| } |
| } |
| |
| // Initiated from the Http2 side |
| void |
| Http2Stream::initiating_close() |
| { |
| if (!closed) { |
| SCOPED_MUTEX_LOCK(lock, this->mutex, this_ethread()); |
| REMEMBER(NO_EVENT, this->reentrancy_count); |
| Http2StreamDebug("initiating_close client_window=%zd session_window=%zd", _peer_rwnd, |
| this->get_connection_state().get_peer_rwnd()); |
| |
| if (!this->is_outbound_connection() && this->is_state_writeable()) { // Let the other end know we are going away |
| this->get_connection_state().send_rst_stream_frame(_id, Http2ErrorCode::HTTP2_ERROR_NO_ERROR); |
| } |
| |
| // Set the state of the connection to closed |
| // TODO - these states should be combined |
| closed = true; |
| _state = Http2StreamState::HTTP2_STREAM_STATE_CLOSED; |
| |
| // leaving the reference to the SM, so we can detach from the SM when we actually destroy |
| // _sm = NULL; |
| // Leaving reference to client session as well, so we can signal once the |
| // TXN_CLOSE has been sent |
| // _proxy_ssn = NULL; |
| |
| _clear_timers(); |
| clear_io_events(); |
| |
| // This should result in do_io_close or release being called. That will schedule the final |
| // kill yourself signal |
| // We are sending signals rather than calling the handlers directly to avoid the case where |
| // the HttpTunnel handler causes the HttpSM to be deleted on the stack. |
| bool sent_write_complete = false; |
| if (_sm) { |
| // Push out any last IO events |
| // First look for active write or read |
| if (write_vio.cont && write_vio.nbytes > 0 && write_vio.ndone == write_vio.nbytes && |
| (!is_outbound_connection() || get_state() == Http2StreamState::HTTP2_STREAM_STATE_OPEN)) { |
| SCOPED_MUTEX_LOCK(lock, write_vio.mutex, this_ethread()); |
| Http2StreamDebug("Send tracked event VC_EVENT_WRITE_COMPLETE on write_vio. sm_id: %" PRId64, _sm->sm_id); |
| write_event = send_tracked_event(write_event, VC_EVENT_WRITE_COMPLETE, &write_vio); |
| sent_write_complete = true; |
| } |
| |
| if (!sent_write_complete) { |
| if (write_vio.cont && write_vio.get_writer() && |
| (!is_outbound_connection() || get_state() == Http2StreamState::HTTP2_STREAM_STATE_OPEN || |
| get_state() == Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_LOCAL)) { |
| SCOPED_MUTEX_LOCK(lock, write_vio.mutex, this_ethread()); |
| Http2StreamDebug("Send tracked event VC_EVENT_EOS on write_vio. sm_id: %" PRId64, _sm->sm_id); |
| write_event = send_tracked_event(write_event, VC_EVENT_EOS, &write_vio); |
| } else if (read_vio.cont && read_vio.get_writer()) { |
| SCOPED_MUTEX_LOCK(lock, read_vio.mutex, this_ethread()); |
| Http2StreamDebug("Send tracked event VC_EVENT_EOS on read_vio. sm_id: %" PRId64, _sm->sm_id); |
| read_event = send_tracked_event(read_event, VC_EVENT_EOS, &read_vio); |
| } else { |
| Http2StreamDebug("send EOS to SM"); |
| // Just send EOS to the _sm |
| _sm->handleEvent(VC_EVENT_EOS, nullptr); |
| } |
| } |
| } else { |
| Http2StreamDebug("No SM to signal"); |
| // Transaction is already gone or not started. Kill yourself |
| terminate_stream = true; |
| terminate_if_possible(); |
| } |
| } |
| } |
| |
| bool |
| Http2Stream::is_outbound_connection() const |
| { |
| return _is_outbound; |
| } |
| |
| bool |
| Http2Stream::is_tunneling() const |
| { |
| return _is_tunneling; |
| } |
| |
| /* Replace existing event only if the new event is different than the inprogress event */ |
| Event * |
| Http2Stream::send_tracked_event(Event *event, int send_event, VIO *vio) |
| { |
| if (event != nullptr) { |
| if (event->callback_event != send_event) { |
| event->cancel(); |
| event = nullptr; |
| } |
| } |
| |
| if (event == nullptr) { |
| REMEMBER(send_event, this->reentrancy_count); |
| event = this_ethread()->schedule_imm(this, send_event, vio); |
| } |
| |
| return event; |
| } |
| |
| void |
| Http2Stream::update_read_request(bool call_update) |
| { |
| if (closed || _proxy_ssn == nullptr || _sm == nullptr || read_vio.mutex == nullptr) { |
| return; |
| } |
| |
| if (!this->_switch_thread_if_not_on_right_thread(VC_EVENT_READ_READY, nullptr)) { |
| // Not on the right thread |
| return; |
| } |
| ink_release_assert(this->_thread == this_ethread()); |
| |
| SCOPED_MUTEX_LOCK(lock, read_vio.mutex, this_ethread()); |
| if (read_vio.nbytes == 0 || read_vio.is_disabled()) { |
| return; |
| } |
| |
| // Try to be smart and only signal if there was additional data |
| int send_event = VC_EVENT_READ_READY; |
| if (read_vio.ntodo() == 0 || (this->receive_end_stream && this->read_vio.nbytes != INT64_MAX)) { |
| send_event = VC_EVENT_READ_COMPLETE; |
| } |
| |
| int64_t read_avail = this->read_vio.get_writer()->max_read_avail(); |
| if (read_avail > 0 || send_event == VC_EVENT_READ_COMPLETE) { |
| if (call_update) { // Safe to call vio handler directly |
| _timeout.update_inactivity(); |
| if (read_vio.cont && this->_sm) { |
| read_vio.cont->handleEvent(send_event, &read_vio); |
| } |
| } else { // Called from do_io_read. Still setting things up. Send event |
| // to handle this after the dust settles |
| read_event = send_tracked_event(read_event, send_event, &read_vio); |
| } |
| } |
| } |
| |
| void |
| Http2Stream::restart_sending() |
| { |
| // Make sure the stream is in a good state to be sending |
| if (this->is_closed()) { |
| return; |
| } |
| if (!this->parsing_header_done) { |
| this->update_write_request(true); |
| return; |
| } |
| if (this->is_outbound_connection()) { |
| if (this->get_state() != Http2StreamState::HTTP2_STREAM_STATE_OPEN || write_vio.ntodo() == 0) { |
| return; |
| } |
| } else { |
| if (this->get_state() != Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_REMOTE) { |
| return; |
| } |
| } |
| |
| IOBufferReader *reader = this->get_data_reader_for_send(); |
| if (reader && !reader->is_read_avail_more_than(0)) { |
| return; |
| } |
| |
| if (this->write_vio.mutex && this->write_vio.ntodo() == 0) { |
| return; |
| } |
| |
| this->send_body(true); |
| } |
| |
| void |
| Http2Stream::update_write_request(bool call_update) |
| { |
| if (!this->is_state_writeable() || closed || _proxy_ssn == nullptr || write_vio.mutex == nullptr || |
| write_vio.get_reader() == nullptr || this->_send_reader == nullptr) { |
| return; |
| } |
| |
| if (!this->_switch_thread_if_not_on_right_thread(VC_EVENT_WRITE_READY, nullptr)) { |
| // Not on the right thread |
| return; |
| } |
| ink_release_assert(this->_thread == this_ethread()); |
| |
| Http2StreamDebug("update_write_request parse_done=%d", parsing_header_done); |
| |
| Http2ConnectionState &connection_state = this->get_connection_state(); |
| |
| SCOPED_MUTEX_LOCK(lock, write_vio.mutex, this_ethread()); |
| |
| IOBufferReader *vio_reader = write_vio.get_reader(); |
| |
| if (write_vio.ntodo() > 0 && (!vio_reader->is_read_avail_more_than(0))) { |
| Http2StreamDebug("update_write_request give up without doing anything ntodo=%" PRId64 " is_read_avail=%d client_window=%zd" |
| " session_window=%zd", |
| write_vio.ntodo(), vio_reader->is_read_avail_more_than(0), _peer_rwnd, |
| this->get_connection_state().get_peer_rwnd()); |
| return; |
| } |
| |
| // Process the new data |
| if (!this->parsing_header_done) { |
| // Still parsing the request or response header |
| int bytes_used = 0; |
| ParseResult state; |
| if (this->is_outbound_connection()) { |
| state = this->_send_header.parse_req(&http_parser, this->_send_reader, &bytes_used, false); |
| } else { |
| state = this->_send_header.parse_resp(&http_parser, this->_send_reader, &bytes_used, false); |
| } |
| // HTTPHdr::parse_resp() consumed the send_reader in above |
| write_vio.ndone += bytes_used; |
| |
| switch (state) { |
| case ParseResult::DONE: { |
| this->parsing_header_done = true; |
| Http2StreamDebug("update_write_request parsing done, read %d bytes", bytes_used); |
| |
| // Schedule session shutdown if response header has "Connection: close" |
| MIMEField *field = this->_send_header.field_find(static_cast<std::string_view>(MIME_FIELD_CONNECTION)); |
| if (field) { |
| auto value{field->value_get()}; |
| if (value == static_cast<std::string_view>(HTTP_VALUE_CLOSE)) { |
| SCOPED_MUTEX_LOCK(lock, _proxy_ssn->mutex, this_ethread()); |
| if (connection_state.get_shutdown_state() == HTTP2_SHUTDOWN_NONE) { |
| connection_state.set_shutdown_state(HTTP2_SHUTDOWN_NOT_INITIATED, Http2ErrorCode::HTTP2_ERROR_NO_ERROR); |
| } |
| } |
| } |
| |
| { |
| SCOPED_MUTEX_LOCK(lock, _proxy_ssn->mutex, this_ethread()); |
| // Send the response header back |
| connection_state.send_headers_frame(this); |
| } |
| |
| // Roll back states of response header to read final response |
| if (!this->is_outbound_connection() && this->_send_header.expect_final_response()) { |
| this->parsing_header_done = false; |
| } |
| if (this->is_outbound_connection() || this->_send_header.expect_final_response()) { |
| _send_header.destroy(); |
| _send_header.create(this->is_outbound_connection() ? HTTPType::REQUEST : HTTPType::RESPONSE, HTTP_2_0); |
| http_parser_clear(&http_parser); |
| http_parser_init(&http_parser); |
| } |
| bool final_write = this->write_vio.ntodo() == 0; |
| if (final_write) { |
| this->signal_write_event(VC_EVENT_WRITE_COMPLETE, !CALL_UPDATE); |
| } |
| |
| if (!final_write && this->_send_reader->is_read_avail_more_than(0)) { |
| Http2StreamDebug("update_write_request done parsing, still more to send"); |
| this->_milestones.mark(Http2StreamMilestone::START_TX_DATA_FRAMES); |
| this->send_body(call_update); |
| } |
| break; |
| } |
| case ParseResult::CONT: |
| // Let it ride for next time |
| Http2StreamDebug("update_write_request still parsing, read %d bytes", bytes_used); |
| break; |
| default: |
| Http2StreamDebug("update_write_request state %d, read %d bytes", static_cast<int>(state), bytes_used); |
| break; |
| } |
| } else { |
| this->_milestones.mark(Http2StreamMilestone::START_TX_DATA_FRAMES); |
| this->send_body(call_update); |
| } |
| |
| return; |
| } |
| |
| void |
| Http2Stream::signal_read_event(int event) |
| { |
| if (this->_sm == nullptr || this->read_vio.cont == nullptr || this->read_vio.cont->mutex == nullptr || |
| this->read_vio.op == VIO::NONE || this->terminate_stream) { |
| return; |
| } |
| |
| reentrancy_count++; |
| MUTEX_TRY_LOCK(lock, read_vio.cont->mutex, this_ethread()); |
| if (lock.is_locked()) { |
| if (read_event) { |
| read_event->cancel(); |
| read_event = nullptr; |
| } |
| _timeout.update_inactivity(); |
| this->read_vio.cont->handleEvent(event, &this->read_vio); |
| } else { |
| if (this->_read_vio_event) { |
| this->_read_vio_event->cancel(); |
| } |
| this->_read_vio_event = this_ethread()->schedule_in(this, retry_delay, event, &read_vio); |
| } |
| reentrancy_count--; |
| // Clean stream up if the terminate flag is set and we are at the bottom of the handler stack |
| terminate_if_possible(); |
| } |
| |
| void |
| Http2Stream::signal_write_event(int event, bool call_update) |
| { |
| // Don't signal a write event if in fact nothing was written |
| if (this->_sm == nullptr || this->write_vio.cont == nullptr || this->write_vio.cont->mutex == nullptr || |
| this->write_vio.op == VIO::NONE || this->terminate_stream) { |
| return; |
| } |
| |
| reentrancy_count++; |
| if (call_update) { |
| MUTEX_TRY_LOCK(lock, write_vio.cont->mutex, this_ethread()); |
| if (lock.is_locked()) { |
| if (write_event) { |
| write_event->cancel(); |
| write_event = nullptr; |
| } |
| _timeout.update_inactivity(); |
| this->write_vio.cont->handleEvent(event, &this->write_vio); |
| } else { |
| if (this->_write_vio_event) { |
| this->_write_vio_event->cancel(); |
| } |
| this->_write_vio_event = this_ethread()->schedule_in(this, retry_delay, event, &write_vio); |
| } |
| } else { |
| // Called from do_io_write. Might still be setting up state. Send an event to let the dust settle |
| write_event = send_tracked_event(write_event, event, &write_vio); |
| } |
| reentrancy_count--; |
| // Clean stream up if the terminate flag is set and we are at the bottom of the handler stack |
| terminate_if_possible(); |
| } |
| |
| bool |
| Http2Stream::push_promise(URL &url, const MIMEField *accept_encoding) |
| { |
| SCOPED_MUTEX_LOCK(lock, _proxy_ssn->mutex, this_ethread()); |
| return this->get_connection_state().send_push_promise_frame(this, url, accept_encoding); |
| } |
| |
| void |
| Http2Stream::send_body(bool /* call_update ATS_UNUSED */) |
| { |
| Http2ConnectionState &connection_state = this->get_connection_state(); |
| _timeout.update_inactivity(); |
| |
| reentrancy_count++; |
| |
| SCOPED_MUTEX_LOCK(lock, _proxy_ssn->mutex, this_ethread()); |
| if (Http2::stream_priority_enabled) { |
| connection_state.schedule_stream_to_send_priority_frames(this); |
| // signal_write_event() will be called from `Http2ConnectionState::send_data_frames_depends_on_priority()` |
| // when write_vio is consumed |
| } else { |
| connection_state.send_data_frames(this); |
| // XXX The call to signal_write_event can destroy/free the Http2Stream. |
| // Don't modify the Http2Stream after calling this method. |
| } |
| |
| reentrancy_count--; |
| terminate_if_possible(); |
| } |
| |
| void |
| Http2Stream::reenable_write() |
| { |
| if (this->_proxy_ssn) { |
| SCOPED_MUTEX_LOCK(lock, this->mutex, this_ethread()); |
| update_write_request(true); |
| } |
| } |
| |
| void |
| Http2Stream::reenable(VIO *vio) |
| { |
| if (this->_proxy_ssn) { |
| if (vio->op == VIO::WRITE) { |
| SCOPED_MUTEX_LOCK(lock, this->mutex, this_ethread()); |
| update_write_request(true); |
| } else if (vio->op == VIO::READ) { |
| SCOPED_MUTEX_LOCK(ssn_lock, _proxy_ssn->mutex, this_ethread()); |
| Http2ConnectionState &connection_state = this->get_connection_state(); |
| connection_state.restart_receiving(this); |
| } |
| } |
| } |
| |
| IOBufferReader * |
| Http2Stream::get_data_reader_for_send() const |
| { |
| return this->_send_reader; |
| } |
| |
| void |
| Http2Stream::set_active_timeout(ink_hrtime timeout_in) |
| { |
| _timeout.set_active_timeout(timeout_in); |
| } |
| |
| void |
| Http2Stream::set_inactivity_timeout(ink_hrtime timeout_in) |
| { |
| _timeout.set_inactive_timeout(timeout_in); |
| } |
| |
| void |
| Http2Stream::cancel_active_timeout() |
| { |
| _timeout.cancel_active_timeout(); |
| } |
| |
| void |
| Http2Stream::cancel_inactivity_timeout() |
| { |
| _timeout.cancel_inactive_timeout(); |
| } |
| |
| bool |
| Http2Stream::is_active_timeout_expired(ink_hrtime now) |
| { |
| return _timeout.is_active_timeout_expired(now); |
| } |
| |
| bool |
| Http2Stream::is_inactive_timeout_expired(ink_hrtime now) |
| { |
| return _timeout.is_inactive_timeout_expired(now); |
| } |
| |
| void |
| Http2Stream::clear_io_events() |
| { |
| if (cross_thread_event) { |
| cross_thread_event->cancel(); |
| cross_thread_event = nullptr; |
| } |
| |
| if (read_event) { |
| read_event->cancel(); |
| read_event = nullptr; |
| } |
| |
| if (write_event) { |
| write_event->cancel(); |
| write_event = nullptr; |
| } |
| |
| if (this->_read_vio_event) { |
| this->_read_vio_event->cancel(); |
| this->_read_vio_event = nullptr; |
| } |
| |
| if (this->_write_vio_event) { |
| this->_write_vio_event->cancel(); |
| this->_write_vio_event = nullptr; |
| } |
| } |
| |
| /** |
| Callback from HttpSM |
| |
| release and do_io_close are the same for the HTTP/2 protocol |
| */ |
| void |
| Http2Stream::release() |
| { |
| if (_state == Http2StreamState::HTTP2_STREAM_STATE_CLOSED) { |
| this->do_io_close(); |
| return; |
| } |
| |
| Http2StreamDebug("Delaying do_io_close() until stream is in the closed state"); |
| } |
| |
| void |
| Http2Stream::increment_transactions_stat() |
| { |
| if (this->is_outbound_connection()) { |
| Metrics::Gauge::increment(http2_rsb.current_server_stream_count); |
| Metrics::Counter::increment(http2_rsb.total_server_stream_count); |
| } else { |
| Metrics::Gauge::increment(http2_rsb.current_client_stream_count); |
| Metrics::Counter::increment(http2_rsb.total_client_stream_count); |
| } |
| } |
| |
| void |
| Http2Stream::decrement_transactions_stat() |
| { |
| if (this->is_outbound_connection()) { |
| Metrics::Gauge::decrement(http2_rsb.current_server_stream_count); |
| } else { |
| Metrics::Gauge::decrement(http2_rsb.current_client_stream_count); |
| } |
| } |
| |
| ssize_t |
| Http2Stream::get_peer_rwnd() const |
| { |
| return this->_peer_rwnd; |
| } |
| |
| Http2ErrorCode |
| Http2Stream::increment_peer_rwnd(size_t amount) |
| { |
| this->_peer_rwnd += amount; |
| |
| this->_recent_rwnd_increment[this->_recent_rwnd_increment_index] = amount; |
| ++this->_recent_rwnd_increment_index; |
| this->_recent_rwnd_increment_index %= this->_recent_rwnd_increment.size(); |
| double sum = std::accumulate(this->_recent_rwnd_increment.begin(), this->_recent_rwnd_increment.end(), 0.0); |
| double avg = sum / this->_recent_rwnd_increment.size(); |
| if (avg < Http2::min_avg_window_update) { |
| return Http2ErrorCode::HTTP2_ERROR_ENHANCE_YOUR_CALM; |
| } |
| return Http2ErrorCode::HTTP2_ERROR_NO_ERROR; |
| } |
| |
| Http2ErrorCode |
| Http2Stream::decrement_peer_rwnd(size_t amount) |
| { |
| this->_peer_rwnd -= amount; |
| if (this->_peer_rwnd < 0) { |
| return Http2ErrorCode::HTTP2_ERROR_PROTOCOL_ERROR; |
| } else { |
| return Http2ErrorCode::HTTP2_ERROR_NO_ERROR; |
| } |
| } |
| |
| ssize_t |
| Http2Stream::get_local_rwnd() const |
| { |
| return this->_local_rwnd; |
| } |
| |
| Http2ErrorCode |
| Http2Stream::increment_local_rwnd(size_t amount) |
| { |
| this->_local_rwnd += amount; |
| return Http2ErrorCode::HTTP2_ERROR_NO_ERROR; |
| } |
| |
| Http2ErrorCode |
| Http2Stream::decrement_local_rwnd(size_t amount) |
| { |
| this->_local_rwnd -= amount; |
| if (this->_local_rwnd < 0) { |
| return Http2ErrorCode::HTTP2_ERROR_PROTOCOL_ERROR; |
| } else { |
| return Http2ErrorCode::HTTP2_ERROR_NO_ERROR; |
| } |
| } |
| |
| bool |
| Http2Stream::_switch_thread_if_not_on_right_thread(int event, void *edata) |
| { |
| if (this->_thread != this_ethread()) { |
| SCOPED_MUTEX_LOCK(stream_lock, this->mutex, this_ethread()); |
| if (cross_thread_event == nullptr) { |
| // Send to the right thread |
| cross_thread_event = this->_thread->schedule_imm(this, event, edata); |
| } |
| return false; |
| } |
| return true; |
| } |
| |
| int |
| Http2Stream::get_transaction_priority_weight() const |
| { |
| return priority_node ? priority_node->weight : 0; |
| } |
| |
| int |
| Http2Stream::get_transaction_priority_dependence() const |
| { |
| if (!priority_node) { |
| return -1; |
| } else { |
| return priority_node->parent ? priority_node->parent->id : 0; |
| } |
| } |
| |
| int64_t |
| Http2Stream::read_vio_read_avail() |
| { |
| MIOBuffer *writer = this->read_vio.get_writer(); |
| if (writer) { |
| return writer->max_read_avail(); |
| } |
| |
| return 0; |
| } |
| |
| bool |
| Http2Stream::has_request_body(int64_t /* content_length ATS_UNUSED */, bool /* is_chunked_set ATS_UNUSED */) const |
| { |
| return has_body; |
| } |
| |
| Http2ConnectionState & |
| Http2Stream::get_connection_state() |
| { |
| if (this->is_outbound_connection()) { |
| Http2ServerSession *session = static_cast<Http2ServerSession *>(_proxy_ssn); |
| return session->connection_state; |
| } else { |
| Http2ClientSession *session = static_cast<Http2ClientSession *>(_proxy_ssn); |
| return session->connection_state; |
| } |
| } |
| |
| bool |
| Http2Stream::is_read_closed() const |
| { |
| return this->receive_end_stream; |
| } |
| |
| bool |
| Http2Stream::expect_send_trailer() const |
| { |
| return this->_expect_send_trailer; |
| } |
| |
| void |
| Http2Stream::set_expect_send_trailer() |
| { |
| _expect_send_trailer = true; |
| parsing_header_done = false; |
| reset_send_headers(); |
| } |
| bool |
| Http2Stream::expect_receive_trailer() const |
| { |
| return this->_expect_receive_trailer; |
| } |
| |
| void |
| Http2Stream::set_expect_receive_trailer() |
| { |
| _expect_receive_trailer = true; |
| } |
| |
| void |
| Http2Stream::set_rx_error_code(ProxyError e) |
| { |
| if (!this->is_outbound_connection() && this->_sm) { |
| this->_sm->t_state.client_info.rx_error_code = e; |
| } |
| } |
| |
| void |
| Http2Stream::set_tx_error_code(ProxyError e) |
| { |
| if (!this->is_outbound_connection() && this->_sm) { |
| if (!(this->_sm->t_state.client_info.tx_error_code.cls == ProxyErrorClass::SSN && e.cls == ProxyErrorClass::TXN)) { |
| // if there is not an error already set for the session set the transaction level error |
| this->_sm->t_state.client_info.tx_error_code = e; |
| } |
| } |
| } |
| |
| HTTPVersion |
| Http2Stream::get_version(HTTPHdr & /* hdr ATS_UNUSED */) const |
| { |
| return HTTP_2_0; |
| } |