| /** @file | |
| HTTP state machine | |
| @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/http/HttpConfig.h" | |
| #include "proxy/hdrs/HdrUtils.h" | |
| #include "tscore/ink_hrtime.h" | |
| #include "tscore/ink_time.h" | |
| #include "tsutil/Metrics.h" | |
| #include "tsutil/StringCompare.h" | |
| #include "tsutil/ts_bw_format.h" | |
| #include "proxy/ProxyTransaction.h" | |
| #include "proxy/http/HttpSM.h" | |
| #include "proxy/http/ConnectingEntry.h" | |
| #include "proxy/http/HttpTransact.h" | |
| #include "proxy/http/HttpBodyFactory.h" | |
| #include "proxy/http/HttpTransactHeaders.h" | |
| #include "iocore/eventsystem/ConfigProcessor.h" | |
| #include "proxy/http/Http1ServerSession.h" | |
| #include "proxy/http2/Http2ServerSession.h" | |
| #include "proxy/http/HttpDebugNames.h" | |
| #include "proxy/http/HttpSessionManager.h" | |
| #include "proxy/http/HttpVCTable.h" | |
| #include "../../iocore/cache/P_CacheInternal.h" | |
| #include "../../iocore/net/P_Net.h" | |
| #include "../../iocore/net/P_UnixNet.h" | |
| #include "../../iocore/net/P_UnixNetVConnection.h" | |
| #include "proxy/http/PreWarmConfig.h" | |
| #include "proxy/logging/Log.h" | |
| #include "proxy/logging/LogAccess.h" | |
| #include "proxy/logging/TransactionLogData.h" | |
| #include "proxy/PluginVC.h" | |
| #include "proxy/ReverseProxy.h" | |
| #include "proxy/http/remap/RemapProcessor.h" | |
| #include "proxy/Transform.h" | |
| #include "../../iocore/net/P_SSLConfig.h" | |
| #include "iocore/net/ConnectionTracker.h" | |
| #include "iocore/net/TLSALPNSupport.h" | |
| #include "iocore/net/TLSBasicSupport.h" | |
| #include "iocore/net/TLSSNISupport.h" | |
| #include "iocore/net/TLSSessionResumptionSupport.h" | |
| #include "iocore/net/TLSTunnelSupport.h" | |
| #include "proxy/IPAllow.h" | |
| #include "iocore/net/ProxyProtocol.h" | |
| #include "tscore/Layout.h" | |
| #include "ts/ats_probe.h" | |
| #include <openssl/ossl_typ.h> | |
| #include <openssl/ssl.h> | |
| #include <algorithm> | |
| #include <atomic> | |
| using namespace std::literals; | |
| static constexpr int DEFAULT_RESPONSE_BUFFER_SIZE_INDEX = 6; // 8K | |
| static constexpr int DEFAULT_REQUEST_BUFFER_SIZE_INDEX = 6; // 8K | |
| static constexpr int MIN_CONFIG_BUFFER_SIZE_INDEX = 5; // 4K | |
| #define hsm_release_assert(EX) \ | |
| { \ | |
| if (!(EX)) { \ | |
| this->dump_state_on_assert(); \ | |
| _ink_assert(#EX, __FILE__, __LINE__); \ | |
| } \ | |
| } | |
| /* | |
| * Comment this off if you don't | |
| * want httpSM to use new_empty_MIOBuffer(..) call | |
| */ | |
| #define USE_NEW_EMPTY_MIOBUFFER | |
| extern int cache_config_read_while_writer; | |
| HttpBodyFactory *body_factory = nullptr; | |
| // We have a debugging list that can use to find stuck | |
| // state machines | |
| DList(HttpSM, debug_link) debug_sm_list; | |
| ink_mutex debug_sm_list_mutex; | |
| static DbgCtl dbg_ctl_dns{"dns"}; | |
| static DbgCtl dbg_ctl_dns_srv{"dns_srv"}; | |
| static DbgCtl dbg_ctl_http{"http"}; | |
| static DbgCtl dbg_ctl_http_cache_write{"http_cache_write"}; | |
| static DbgCtl dbg_ctl_http_connect{"http_connect"}; | |
| static DbgCtl dbg_ctl_http_hdrs{"http_hdrs"}; | |
| static DbgCtl dbg_ctl_http_parse{"http_parse"}; | |
| static DbgCtl dbg_ctl_http_range{"http_range"}; | |
| static DbgCtl dbg_ctl_http_redirect{"http_redirect"}; | |
| static DbgCtl dbg_ctl_http_redir_error{"http_redir_error"}; | |
| static DbgCtl dbg_ctl_http_seq{"http_seq"}; | |
| static DbgCtl dbg_ctl_http_ss{"http_ss"}; | |
| static DbgCtl dbg_ctl_http_ss_auth{"http_ss_auth"}; | |
| static DbgCtl dbg_ctl_http_timeout{"http_timeout"}; | |
| static DbgCtl dbg_ctl_http_track{"http_track"}; | |
| static DbgCtl dbg_ctl_http_trans{"http_trans"}; | |
| static DbgCtl dbg_ctl_http_tproxy{"http_tproxy"}; | |
| static DbgCtl dbg_ctl_http_tunnel{"http_tunnel"}; | |
| static DbgCtl dbg_ctl_http_websocket{"http_websocket"}; | |
| static DbgCtl dbg_ctl_ip_allow{"ip_allow"}; | |
| static DbgCtl dbg_ctl_ssl_alpn{"ssl_alpn"}; | |
| static DbgCtl dbg_ctl_ssl_early_data{"ssl_early_data"}; | |
| static DbgCtl dbg_ctl_ssl_sni{"ssl_sni"}; | |
| static DbgCtl dbg_ctl_url_rewrite{"url_rewrite"}; | |
| static constexpr int sub_header_size = sizeof("Content-type: ") - 1 + 2 + sizeof("Content-range: bytes ") - 1 + 4; | |
| static constexpr int boundary_size = 2 + sizeof("RANGE_SEPARATOR") - 1 + 2; | |
| static const char *str_100_continue_response = "HTTP/1.1 100 Continue\r\n\r\n"; | |
| static constexpr int len_100_continue_response = sizeof("HTTP/1.1 100 Continue\r\n\r\n") - 1; | |
| // Handy alias for short (single line) message generation. | |
| using lbw = swoc::LocalBufferWriter<256>; | |
| namespace | |
| { | |
| DbgCtl dbg_ctl_proxyprotocol{"proxyprotocol"}; | |
| // Unique state machine identifier | |
| std::atomic<int64_t> next_sm_id(0); | |
| /// Buffer for some error logs. | |
| thread_local std::string error_bw_buffer; | |
| constexpr size_t ORIGIN_LOG_URL_LEN = 512; | |
| void | |
| log_server_close_with_origin(HttpTransact::State &state, const char *message) | |
| { | |
| if (message == nullptr) { | |
| return; | |
| } | |
| if (state.hdr_info.server_request.valid()) { | |
| char origin_url[ORIGIN_LOG_URL_LEN] = {0}; | |
| int offset = 0; | |
| int skip = 0; | |
| state.hdr_info.server_request.url_print(origin_url, static_cast<int>(ORIGIN_LOG_URL_LEN) - 1, &offset, &skip); | |
| if (offset > 0) { | |
| origin_url[offset] = '\0'; | |
| Log::error("%s (origin %s)", message, origin_url); | |
| return; | |
| } | |
| } | |
| Log::error("%s", message); | |
| } | |
| } // namespace | |
| int64_t | |
| do_outbound_proxy_protocol(MIOBuffer *miob, NetVConnection *vc_out, NetVConnection *vc_in, int conf) | |
| { | |
| ink_release_assert(conf >= 0); | |
| ProxyProtocol info = vc_in->get_proxy_protocol_info(); | |
| ProxyProtocolVersion pp_version = proxy_protocol_version_cast(conf); | |
| if (info.version == ProxyProtocolVersion::UNDEFINED) { | |
| if (conf == 0) { | |
| // nothing to forward | |
| return 0; | |
| } else { | |
| Dbg(dbg_ctl_proxyprotocol, "vc_in had no Proxy Protocol. Manufacturing from the vc_in socket."); | |
| // set info from incoming NetVConnection | |
| IpEndpoint local = vc_in->get_local_endpoint(); | |
| info = ProxyProtocol{pp_version, local.family(), vc_in->get_remote_endpoint(), local}; | |
| } | |
| } | |
| vc_out->set_proxy_protocol_info(info); | |
| IOBufferBlock *block = miob->first_write_block(); | |
| size_t len = proxy_protocol_build(reinterpret_cast<uint8_t *>(block->buf()), block->write_avail(), info, pp_version); | |
| if (len > 0) { | |
| miob->fill(len); | |
| } | |
| return len; | |
| } | |
| ClassAllocator<HttpSM> httpSMAllocator("httpSMAllocator"); | |
| void | |
| initialize_thread_for_connecting_pools(EThread *thread) | |
| { | |
| if (thread->connecting_pool == nullptr) { | |
| thread->connecting_pool = new ConnectingPool(); | |
| } | |
| } | |
| #define SMDbg(tag, fmt, ...) SpecificDbg(debug_on, tag, "[%" PRId64 "] " fmt, sm_id, ##__VA_ARGS__) | |
| #define REMEMBER(e, r) \ | |
| { \ | |
| history.push_back(MakeSourceLocation(), e, r); \ | |
| } | |
| #ifdef STATE_ENTER | |
| #undef STATE_ENTER | |
| #endif | |
| #define STATE_ENTER(state_name, event) \ | |
| { \ | |
| /*ink_assert (magic == HttpSmMagic_t::ALIVE); */ REMEMBER(event, reentrancy_count); \ | |
| SMDbg(dbg_ctl_http, "[%s, %s]", #state_name, HttpDebugNames::get_event_name(event)); \ | |
| ATS_PROBE1(state_enter_##state_name, sm_id); \ | |
| } | |
| #define HTTP_SM_SET_DEFAULT_HANDLER(_h) \ | |
| { \ | |
| REMEMBER(NO_EVENT, reentrancy_count); \ | |
| default_handler = _h; \ | |
| } | |
| /* | |
| * Helper functions to ensure that the parallel | |
| * API set timeouts are set consistently with the records.yaml settings | |
| */ | |
| ink_hrtime | |
| HttpSM::get_server_inactivity_timeout() | |
| { | |
| ink_hrtime retval = 0; | |
| if (t_state.api_txn_no_activity_timeout_value != -1) { | |
| retval = HRTIME_MSECONDS(t_state.api_txn_no_activity_timeout_value); | |
| } else { | |
| retval = HRTIME_SECONDS(t_state.txn_conf->transaction_no_activity_timeout_out); | |
| } | |
| return retval; | |
| } | |
| ink_hrtime | |
| HttpSM::get_server_active_timeout() | |
| { | |
| ink_hrtime retval = 0; | |
| if (t_state.api_txn_active_timeout_value != -1) { | |
| retval = HRTIME_MSECONDS(t_state.api_txn_active_timeout_value); | |
| } else { | |
| retval = HRTIME_SECONDS(t_state.txn_conf->transaction_active_timeout_out); | |
| } | |
| return retval; | |
| } | |
| ink_hrtime | |
| HttpSM::get_server_connect_timeout() | |
| { | |
| ink_hrtime retval = 0; | |
| if (t_state.api_txn_connect_timeout_value != -1) { | |
| retval = HRTIME_MSECONDS(t_state.api_txn_connect_timeout_value); | |
| } else { | |
| retval = HRTIME_SECONDS(t_state.txn_conf->connect_attempts_timeout); | |
| } | |
| return retval; | |
| } | |
| HttpSM::HttpSM() : Continuation(nullptr), vc_table(this) {} | |
| HttpSM::~HttpSM() | |
| { | |
| http_parser_clear(&http_parser); | |
| // coverity[exn_spec_violation] - release() only does ref counting and delete on POD types | |
| HttpConfig::release(t_state.http_config_param); | |
| // coverity[exn_spec_violation] - cancel_pending_action() cancels pending cache work and clears tracked pointers | |
| cache_sm.cancel_pending_action(); | |
| mutex.clear(); | |
| tunnel.mutex.clear(); | |
| cache_sm.mutex.clear(); | |
| transform_cache_sm.mutex.clear(); | |
| magic = HttpSmMagic_t::DEAD; | |
| debug_on = false; | |
| if (_prewarm_sm) { | |
| // coverity[exn_spec_violation] - destroy() only frees memory and resets shared_ptrs | |
| _prewarm_sm->destroy(); | |
| THREAD_FREE(_prewarm_sm, preWarmSMAllocator, this_ethread()); | |
| _prewarm_sm = nullptr; | |
| } | |
| } | |
| void | |
| HttpSM::destroy() | |
| { | |
| THREAD_FREE(this, httpSMAllocator, this_thread()); | |
| } | |
| void | |
| HttpSM::init(bool from_early_data) | |
| { | |
| milestones[TS_MILESTONE_SM_START] = ink_get_hrtime(); | |
| _from_early_data = from_early_data; | |
| magic = HttpSmMagic_t::ALIVE; | |
| server_txn = nullptr; | |
| // Unique state machine identifier | |
| sm_id = next_sm_id++; | |
| ATS_PROBE1(milestone_sm_start, sm_id); | |
| t_state.state_machine = this; | |
| t_state.http_config_param = HttpConfig::acquire(); | |
| // Acquire a lease on the global remap / rewrite table (stupid global name ...) | |
| m_remap = rewrite_table.load(std::memory_order_acquire); | |
| // Simply point to the global config for the time being, no need to copy this | |
| // entire struct if nothing is going to change it. | |
| t_state.txn_conf = &t_state.http_config_param->oride; | |
| t_state.init(); | |
| http_parser_init(&http_parser); | |
| // Added to skip dns if the document is in cache. DNS will be forced if there is a ip based ACL in | |
| // cache control or parent.config or if the doc_in_cache_skip_dns is disabled or if http caching is disabled | |
| // TODO: This probably doesn't honor this as a per-transaction overridable config. | |
| t_state.force_dns = | |
| (ip_rule_in_CacheControlTable() || (nullptr != t_state.parent_params && t_state.parent_params->parent_table->ipMatch) || | |
| !(t_state.txn_conf->doc_in_cache_skip_dns) || !(t_state.txn_conf->cache_http)); | |
| SET_HANDLER(&HttpSM::main_handler); | |
| // Remember where this SM is running so it gets returned correctly | |
| this->setThreadAffinity(this_ethread()); | |
| #ifdef USE_HTTP_DEBUG_LISTS | |
| ink_mutex_acquire(&debug_sm_list_mutex); | |
| debug_sm_list.push(this); | |
| ink_mutex_release(&debug_sm_list_mutex); | |
| #endif | |
| } | |
| void | |
| HttpSM::set_ua_half_close_flag() | |
| { | |
| _ua.get_txn()->set_half_close_flag(true); | |
| } | |
| inline int | |
| HttpSM::do_api_callout() | |
| { | |
| if (hooks_set) { | |
| return do_api_callout_internal(); | |
| } else { | |
| handle_api_return(); | |
| return 0; | |
| } | |
| } | |
| int | |
| HttpSM::state_add_to_list(int /* event ATS_UNUSED */, void * /* data ATS_UNUSED */) | |
| { | |
| t_state.api_next_action = HttpTransact::StateMachineAction_t::API_SM_START; | |
| if (do_api_callout() < 0) { | |
| // Didn't get the hook continuation lock. Clear the read and wait for next event | |
| if (_ua.get_entry()->read_vio) { | |
| // Seems like _ua.get_entry()->read_vio->disable(); should work, but that was | |
| // not sufficient to stop the state machine from processing IO events until the | |
| // TXN_START hooks had completed. Just set the number of bytes to read to 0 | |
| _ua.get_entry()->read_vio = _ua.get_entry()->vc->do_io_read(this, 0, nullptr); | |
| } | |
| return EVENT_CONT; | |
| } | |
| return EVENT_DONE; | |
| } | |
| int | |
| HttpSM::state_remove_from_list(int /* event ATS_UNUSED */, void * /* data ATS_UNUSED */) | |
| { | |
| // We're now ready to finish off the state machine | |
| terminate_sm = true; | |
| kill_this_async_done = true; | |
| return EVENT_DONE; | |
| } | |
| void | |
| HttpSM::start_sub_sm() | |
| { | |
| tunnel.init(this, mutex); | |
| cache_sm.init(this, mutex); | |
| transform_cache_sm.init(this, mutex); | |
| } | |
| void | |
| HttpSM::attach_client_session(ProxyTransaction *txn) | |
| { | |
| ATS_PROBE1(milestone_ua_begin, sm_id); | |
| milestones[TS_MILESTONE_UA_BEGIN] = ink_get_hrtime(); | |
| ink_assert(txn != nullptr); | |
| NetVConnection *netvc = txn->get_netvc(); | |
| if (!netvc) { | |
| return; | |
| } | |
| ATS_PROBE4(http_attach_client_session, sm_id, netvc->get_socket(), txn->get_transaction_id(), | |
| dynamic_cast<UnixNetVConnection *>(netvc) ? dynamic_cast<UnixNetVConnection *>(netvc)->id : 0); | |
| _ua.set_txn(txn, milestones); | |
| // Collect log & stats information. We've already verified that the netvc is !nullptr above, | |
| // and netvc == _ua.get_txn()->get_netvc(). | |
| is_internal = netvc->get_is_internal_request(); | |
| mptcp_state = netvc->get_mptcp_state(); | |
| ink_release_assert(_ua.get_txn()->get_half_close_flag() == false); | |
| mutex = txn->mutex; | |
| if (_ua.get_txn()->debug()) { | |
| debug_on = true; | |
| } | |
| t_state.setup_per_txn_configs(); | |
| t_state.api_skip_all_remapping = netvc->get_is_unmanaged_request(); | |
| ink_assert(_ua.get_txn()->get_proxy_ssn()); | |
| ink_assert(_ua.get_txn()->get_proxy_ssn()->acceptor); | |
| // default the upstream IP style host resolution order from inbound | |
| t_state.my_txn_conf().host_res_data.order = _ua.get_txn()->get_proxy_ssn()->acceptor->options().host_res_preference; | |
| start_sub_sm(); | |
| // Allocate a user agent entry in the state machine's | |
| // vc table | |
| _ua.set_entry(vc_table.new_entry()); | |
| _ua.get_entry()->vc = txn; | |
| _ua.get_entry()->vc_type = HttpVC_t::UA_VC; | |
| ats_ip_copy(&t_state.client_info.src_addr, netvc->get_remote_addr()); | |
| ats_ip_copy(&t_state.client_info.dst_addr, netvc->get_local_addr()); | |
| ats_ip_copy(&t_state.effective_client_addr, netvc->get_effective_remote_addr()); | |
| t_state.client_info.is_transparent = netvc->get_is_transparent(); | |
| t_state.client_info.port_attribute = static_cast<HttpProxyPort::TransportType>(netvc->attributes); | |
| Metrics::Counter::increment(http_rsb.incoming_requests); | |
| if (t_state.client_info.port_attribute == HttpProxyPort::TRANSPORT_SSL) { | |
| Metrics::Counter::increment(http_rsb.https_incoming_requests); | |
| } | |
| // Record api hook set state | |
| hooks_set = txn->has_hooks(); | |
| // Setup for parsing the header | |
| _ua.get_entry()->vc_read_handler = &HttpSM::state_read_client_request_header; | |
| t_state.hdr_info.client_request.destroy(); | |
| t_state.hdr_info.client_request.create(HTTPType::REQUEST); | |
| // Prepare raw reader which will live until we are sure this is HTTP indeed | |
| auto tts = netvc->get_service<TLSTunnelSupport>(); | |
| if (is_transparent_passthrough_allowed() || (tts && tts->is_decryption_needed())) { | |
| _ua.set_raw_buffer_reader(_ua.get_txn()->get_remote_reader()->clone()); | |
| } | |
| // We first need to run the transaction start hook. Since | |
| // this hook maybe asynchronous, we need to disable IO on | |
| // client but set the continuation to be the state machine | |
| // so if we get an timeout events the sm handles them | |
| _ua.get_entry()->read_vio = txn->do_io_read(this, 0, _ua.get_txn()->get_remote_reader()->mbuf); | |
| _ua.get_entry()->write_vio = txn->do_io_write(this, 0, nullptr); | |
| ///////////////////////// | |
| // set up timeouts // | |
| ///////////////////////// | |
| txn->set_inactivity_timeout(HRTIME_SECONDS(t_state.txn_conf->transaction_no_activity_timeout_in)); | |
| txn->set_active_timeout(HRTIME_SECONDS(t_state.txn_conf->transaction_active_timeout_in)); | |
| ++reentrancy_count; | |
| // Add our state sm to the sm list | |
| state_add_to_list(EVENT_NONE, nullptr); | |
| // This is another external entry point and it is possible for the state machine to get terminated | |
| // while down the call chain from @c state_add_to_list. So we need to use the reentrancy_count to | |
| // prevent cleanup there and do it here as we return to the external caller. | |
| if (terminate_sm == true && reentrancy_count == 1) { | |
| kill_this(); | |
| } else { | |
| --reentrancy_count; | |
| ink_assert(reentrancy_count >= 0); | |
| } | |
| } | |
| void | |
| HttpSM::setup_client_read_request_header() | |
| { | |
| ink_assert(_ua.get_entry()->vc_read_handler == &HttpSM::state_read_client_request_header); | |
| _ua.get_entry()->read_vio = _ua.get_txn()->do_io_read(this, INT64_MAX, _ua.get_txn()->get_remote_reader()->mbuf); | |
| // The header may already be in the buffer if this | |
| // a request from a keep-alive connection | |
| handleEvent(VC_EVENT_READ_READY, _ua.get_entry()->read_vio); | |
| } | |
| void | |
| HttpSM::setup_blind_tunnel_port() | |
| { | |
| NetVConnection *netvc = _ua.get_txn()->get_netvc(); | |
| ink_release_assert(netvc); | |
| // This applies to both the TLS and non TLS cases | |
| if (t_state.hdr_info.client_request.url_get()->host_get().empty()) { | |
| // the URL object has not been created in the start of the transaction. Hence, we need to create the URL here | |
| URL u; | |
| t_state.hdr_info.client_request.create(HTTPType::REQUEST); | |
| t_state.hdr_info.client_request.method_set(static_cast<std::string_view>(HTTP_METHOD_CONNECT)); | |
| t_state.hdr_info.client_request.url_create(&u); | |
| u.scheme_set(std::string_view{URL_SCHEME_TUNNEL}); | |
| t_state.hdr_info.client_request.url_set(&u); | |
| } | |
| TLSTunnelSupport *tts = nullptr; | |
| if (!_ua.get_txn()->is_outbound_transparent() && (tts = netvc->get_service<TLSTunnelSupport>())) { | |
| if (t_state.hdr_info.client_request.url_get()->host_get().empty()) { | |
| if (tts->has_tunnel_destination()) { | |
| auto tunnel_host = tts->get_tunnel_host(); | |
| t_state.hdr_info.client_request.url_get()->host_set(tunnel_host); | |
| if (tts->get_tunnel_port() > 0) { | |
| t_state.tunnel_port_is_dynamic = tts->tunnel_port_is_dynamic(); | |
| t_state.hdr_info.client_request.url_get()->port_set(tts->get_tunnel_port()); | |
| } else { | |
| t_state.hdr_info.client_request.url_get()->port_set(netvc->get_local_port()); | |
| } | |
| } else { | |
| const char *server_name = ""; | |
| if (auto *snis = netvc->get_service<TLSSNISupport>(); snis) { | |
| server_name = snis->get_sni_server_name(); | |
| } | |
| t_state.hdr_info.client_request.url_get()->host_set({server_name}); | |
| t_state.hdr_info.client_request.url_get()->port_set(netvc->get_local_port()); | |
| } | |
| } | |
| } else { // If outbound transparent or not TLS, just use the local IP as the origin | |
| char new_host[INET6_ADDRSTRLEN]; | |
| ats_ip_ntop(netvc->get_local_addr(), new_host, sizeof(new_host)); | |
| t_state.hdr_info.client_request.url_get()->host_set({new_host}); | |
| t_state.hdr_info.client_request.url_get()->port_set(netvc->get_local_port()); | |
| } | |
| t_state.api_next_action = HttpTransact::StateMachineAction_t::API_TUNNEL_START; | |
| do_api_callout(); | |
| } | |
| int | |
| HttpSM::state_read_client_request_header(int event, void *data) | |
| { | |
| STATE_ENTER(state_read_client_request_header, event); | |
| ink_assert(_ua.get_entry()->read_vio == (VIO *)data); | |
| ink_assert(server_entry == nullptr); | |
| ink_assert(server_txn == nullptr); | |
| int bytes_used = 0; | |
| ink_assert(_ua.get_entry()->eos == false); | |
| NetVConnection *netvc = _ua.get_txn()->get_netvc(); | |
| if (!netvc && event != VC_EVENT_EOS) { | |
| return 0; | |
| } | |
| switch (event) { | |
| case VC_EVENT_READ_READY: | |
| case VC_EVENT_READ_COMPLETE: | |
| // More data to parse | |
| break; | |
| case VC_EVENT_EOS: | |
| _ua.get_entry()->eos = true; | |
| if ((client_request_hdr_bytes > 0) && is_transparent_passthrough_allowed() && (_ua.get_raw_buffer_reader() != nullptr)) { | |
| break; | |
| } | |
| // Fall through | |
| case VC_EVENT_ERROR: | |
| case VC_EVENT_INACTIVITY_TIMEOUT: | |
| case VC_EVENT_ACTIVE_TIMEOUT: | |
| // The user agent is hosed. Close it & | |
| // bail on the state machine | |
| vc_table.cleanup_entry(_ua.get_entry()); | |
| _ua.set_entry(nullptr); | |
| set_ua_abort(HttpTransact::ABORTED, event); | |
| terminate_sm = true; | |
| return 0; | |
| } | |
| // Reset the inactivity timeout if this is the first | |
| // time we've been called. The timeout had been set to | |
| // the accept timeout by the ProxyTransaction | |
| // | |
| if ((_ua.get_txn()->get_remote_reader()->read_avail() > 0) && (client_request_hdr_bytes == 0)) { | |
| ATS_PROBE1(milestone_ua_first_read, sm_id); | |
| milestones[TS_MILESTONE_UA_FIRST_READ] = ink_get_hrtime(); | |
| _ua.get_txn()->set_inactivity_timeout(HRTIME_SECONDS(t_state.txn_conf->transaction_no_activity_timeout_in)); | |
| } | |
| ///////////////////// | |
| // tokenize header // | |
| ///////////////////// | |
| ParseResult state = t_state.hdr_info.client_request.parse_req(&http_parser, _ua.get_txn()->get_remote_reader(), &bytes_used, | |
| _ua.get_entry()->eos, t_state.http_config_param->strict_uri_parsing, | |
| t_state.http_config_param->http_request_line_max_size, | |
| t_state.http_config_param->http_hdr_field_max_size); | |
| client_request_hdr_bytes += bytes_used; | |
| // Check to see if we are over the hdr size limit | |
| if (client_request_hdr_bytes > t_state.txn_conf->request_hdr_max_size) { | |
| SMDbg(dbg_ctl_http, "client header bytes were over max header size; treating as a bad request"); | |
| state = ParseResult::ERROR; | |
| } | |
| // We need to handle EOS as well as READ_READY because the client | |
| // may have sent all of the data already followed by a FIN and that | |
| // should be OK. | |
| if (_ua.get_raw_buffer_reader() != nullptr) { | |
| bool do_blind_tunnel = false; | |
| // If we had a parse error and we're done reading data | |
| // blind tunnel | |
| if ((event == VC_EVENT_READ_READY || event == VC_EVENT_EOS) && state == ParseResult::ERROR) { | |
| do_blind_tunnel = true; | |
| // If we had a GET request that has data after the | |
| // get request, do blind tunnel | |
| } else if (state == ParseResult::DONE && t_state.hdr_info.client_request.method_get_wksidx() == HTTP_WKSIDX_GET && | |
| _ua.get_txn()->get_remote_reader()->read_avail() > 0 && !t_state.hdr_info.client_request.is_keep_alive_set()) { | |
| do_blind_tunnel = true; | |
| } | |
| if (do_blind_tunnel) { | |
| SMDbg(dbg_ctl_http, "first request on connection failed parsing, switching to passthrough."); | |
| t_state.transparent_passthrough = true; | |
| http_parser_clear(&http_parser); | |
| // Turn off read eventing until we get the | |
| // blind tunnel infrastructure set up | |
| if (netvc) { | |
| netvc->do_io_read(nullptr, 0, nullptr); | |
| } | |
| /* establish blind tunnel */ | |
| setup_blind_tunnel_port(); | |
| // Setting half close means we will send the FIN when we've written all of the data. | |
| if (event == VC_EVENT_EOS) { | |
| this->set_ua_half_close_flag(); | |
| t_state.client_info.keep_alive = HTTPKeepAlive::NO_KEEPALIVE; | |
| } | |
| return 0; | |
| } | |
| } | |
| // Check to see if we are done parsing the header | |
| if (state != ParseResult::CONT || _ua.get_entry()->eos || (state == ParseResult::CONT && event == VC_EVENT_READ_COMPLETE)) { | |
| if (_ua.get_raw_buffer_reader() != nullptr) { | |
| _ua.get_raw_buffer_reader()->dealloc(); | |
| _ua.set_raw_buffer_reader(nullptr); | |
| } | |
| http_parser_clear(&http_parser); | |
| _ua.get_entry()->vc_read_handler = &HttpSM::state_watch_for_client_abort; | |
| _ua.get_entry()->vc_write_handler = &HttpSM::state_watch_for_client_abort; | |
| _ua.get_txn()->cancel_inactivity_timeout(); | |
| milestones[TS_MILESTONE_UA_READ_HEADER_DONE] = ink_get_hrtime(); | |
| #ifdef ENABLE_SYSTEMTAP_PROBES | |
| { | |
| char url_buf[4096] = {0}; | |
| int url_len = 0; | |
| int offset = 0; | |
| URL *url = t_state.hdr_info.client_request.url_get(); | |
| if (url) { | |
| url->print(url_buf, sizeof(url_buf) - 1, &url_len, &offset); | |
| ATS_PROBE3(milestone_ua_read_header_done_url, sm_id, url_buf, url_len); | |
| } | |
| } | |
| #endif | |
| } | |
| switch (state) { | |
| case ParseResult::ERROR: | |
| SMDbg(dbg_ctl_http, "error parsing client request header"); | |
| // Disable further I/O on the client | |
| _ua.get_entry()->read_vio->nbytes = _ua.get_entry()->read_vio->ndone; | |
| (bytes_used > t_state.http_config_param->http_request_line_max_size) ? | |
| t_state.http_return_code = HTTPStatus::REQUEST_URI_TOO_LONG : | |
| t_state.http_return_code = HTTPStatus::NONE; | |
| if (!is_http1_hdr_version_supported(t_state.hdr_info.client_request.version_get())) { | |
| t_state.http_return_code = HTTPStatus::HTTPVER_NOT_SUPPORTED; | |
| } | |
| call_transact_and_set_next_state(HttpTransact::BadRequest); | |
| break; | |
| case ParseResult::CONT: | |
| if (_ua.get_entry()->eos) { | |
| SMDbg(dbg_ctl_http_seq, "EOS before client request parsing finished"); | |
| set_ua_abort(HttpTransact::ABORTED, event); | |
| // Disable further I/O on the client | |
| _ua.get_entry()->read_vio->nbytes = _ua.get_entry()->read_vio->ndone; | |
| call_transact_and_set_next_state(HttpTransact::BadRequest); | |
| break; | |
| } else if (event == VC_EVENT_READ_COMPLETE) { | |
| SMDbg(dbg_ctl_http_parse, "VC_EVENT_READ_COMPLETE and PARSE CONT state"); | |
| break; | |
| } else { | |
| if (is_transparent_passthrough_allowed() && _ua.get_raw_buffer_reader() != nullptr && | |
| _ua.get_raw_buffer_reader()->get_current_block()->write_avail() <= 0) { | |
| // Disable passthrough regardless of eventual parsing failure or success -- otherwise | |
| // we either have to consume some data or risk blocking the writer. | |
| _ua.get_raw_buffer_reader()->dealloc(); | |
| _ua.set_raw_buffer_reader(nullptr); | |
| } | |
| _ua.get_entry()->read_vio->reenable(); | |
| return VC_EVENT_CONT; | |
| } | |
| case ParseResult::DONE: | |
| SMDbg(dbg_ctl_http, "done parsing client request header"); | |
| if (!t_state.hdr_info.client_request.check_hdr_implements()) { | |
| t_state.http_return_code = HTTPStatus::NOT_IMPLEMENTED; | |
| call_transact_and_set_next_state(HttpTransact::BadRequest); | |
| break; | |
| } | |
| if (!is_internal && t_state.http_config_param->scheme_proto_mismatch_policy != 0) { | |
| auto scheme = t_state.hdr_info.client_request.url_get()->scheme_get_wksidx(); | |
| if ((_ua.get_client_connection_is_ssl() && (scheme == URL_WKSIDX_HTTP || scheme == URL_WKSIDX_WS)) || | |
| (!_ua.get_client_connection_is_ssl() && (scheme == URL_WKSIDX_HTTPS || scheme == URL_WKSIDX_WSS))) { | |
| Warning("scheme [%s] vs. protocol [%s] mismatch", hdrtoken_index_to_wks(scheme), | |
| _ua.get_client_connection_is_ssl() ? "tls" : "plaintext"); | |
| if (t_state.http_config_param->scheme_proto_mismatch_policy == 2) { | |
| t_state.http_return_code = HTTPStatus::BAD_REQUEST; | |
| call_transact_and_set_next_state(HttpTransact::BadRequest); | |
| break; | |
| } | |
| } | |
| } | |
| if (_from_early_data) { | |
| // Only allow early data for safe methods defined in RFC7231 Section 4.2.1. | |
| // https://tools.ietf.org/html/rfc7231#section-4.2.1 | |
| SMDbg(dbg_ctl_ssl_early_data, "%d", t_state.hdr_info.client_request.method_get_wksidx()); | |
| if (!HttpTransactHeaders::is_method_safe(t_state.hdr_info.client_request.method_get_wksidx())) { | |
| SMDbg(dbg_ctl_http, "client request was from early data but is NOT safe"); | |
| call_transact_and_set_next_state(HttpTransact::TooEarly); | |
| return 0; | |
| } else if (!SSLConfigParams::server_allow_early_data_params && | |
| t_state.hdr_info.client_request.m_http->u.req.m_url_impl->m_len_query > 0) { | |
| SMDbg(dbg_ctl_http, "client request was from early data but HAS parameters"); | |
| call_transact_and_set_next_state(HttpTransact::TooEarly); | |
| return 0; | |
| } | |
| t_state.hdr_info.client_request.mark_early_data(); | |
| } | |
| _ua.get_txn()->set_session_active(); | |
| if (t_state.hdr_info.client_request.version_get() == HTTP_1_1 && | |
| (t_state.hdr_info.client_request.method_get_wksidx() == HTTP_WKSIDX_POST || | |
| t_state.hdr_info.client_request.method_get_wksidx() == HTTP_WKSIDX_PUT)) { | |
| auto expect{t_state.hdr_info.client_request.value_get(static_cast<std::string_view>(MIME_FIELD_EXPECT))}; | |
| if (ts::iequals(expect, static_cast<std::string_view>(HTTP_VALUE_100_CONTINUE))) { | |
| // When receive an "Expect: 100-continue" request from client, ATS sends a "100 Continue" response to client | |
| // immediately, before receive the real response from original server. | |
| if (t_state.http_config_param->send_100_continue_response) { | |
| int64_t alloc_index = buffer_size_to_index(len_100_continue_response, t_state.http_config_param->max_payload_iobuf_index); | |
| if (_ua.get_entry()->write_buffer) { | |
| free_MIOBuffer(_ua.get_entry()->write_buffer); | |
| _ua.get_entry()->write_buffer = nullptr; | |
| } | |
| _ua.get_entry()->write_buffer = new_MIOBuffer(alloc_index); | |
| IOBufferReader *buf_start = _ua.get_entry()->write_buffer->alloc_reader(); | |
| SMDbg(dbg_ctl_http_seq, "send 100 Continue response to client"); | |
| int64_t nbytes = _ua.get_entry()->write_buffer->write(str_100_continue_response, len_100_continue_response); | |
| _ua.get_entry()->write_vio = _ua.get_txn()->do_io_write(this, nbytes, buf_start); | |
| t_state.hdr_info.client_request.m_100_continue_sent = true; | |
| } else { | |
| t_state.hdr_info.client_request.m_100_continue_required = true; | |
| } | |
| } | |
| } | |
| if (t_state.hdr_info.client_request.method_get_wksidx() == HTTP_WKSIDX_PUSH && | |
| t_state.http_config_param->push_method_enabled == 0) { | |
| SMDbg(dbg_ctl_http, "Rejecting PUSH request because push_method_enabled is 0."); | |
| call_transact_and_set_next_state(HttpTransact::Forbidden); | |
| return 0; | |
| } | |
| // Call to ensure the content-length and transfer_encoding elements in client_request are filled in | |
| HttpTransact::set_client_request_state(&t_state, &t_state.hdr_info.client_request); | |
| if (t_state.hdr_info.client_request.get_content_length() == 0 && | |
| t_state.client_info.transfer_encoding != HttpTransact::TransferEncoding_t::CHUNKED) { | |
| // Enable further IO to watch for client aborts | |
| _ua.get_entry()->read_vio->reenable(); | |
| } else if (t_state.hdr_info.client_request.method_get_wksidx() == HTTP_WKSIDX_TRACE) { | |
| // Trace with request body is not allowed | |
| call_transact_and_set_next_state(HttpTransact::BadRequest); | |
| return 0; | |
| } else { | |
| // Disable further I/O on the client since there could | |
| // be body that we are tunneling POST/PUT/CONNECT or | |
| // extension methods and we can't issue another | |
| // another IO later for the body with a different buffer | |
| _ua.get_entry()->read_vio->nbytes = _ua.get_entry()->read_vio->ndone; | |
| } | |
| call_transact_and_set_next_state(HttpTransact::ModifyRequest); | |
| break; | |
| default: | |
| ink_assert(!"not reached"); | |
| } | |
| return 0; | |
| } | |
| void | |
| HttpSM::wait_for_full_body() | |
| { | |
| is_waiting_for_full_body = true; | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::tunnel_handler_post); | |
| bool chunked = (t_state.client_info.transfer_encoding == HttpTransact::TransferEncoding_t::CHUNKED); | |
| int64_t alloc_index; | |
| HttpTunnelProducer *p = nullptr; | |
| // content length is undefined, use default buffer size | |
| if (t_state.hdr_info.request_content_length == HTTP_UNDEFINED_CL) { | |
| alloc_index = static_cast<int>(t_state.txn_conf->default_buffer_size_index); | |
| if (alloc_index < MIN_CONFIG_BUFFER_SIZE_INDEX || alloc_index > MAX_BUFFER_SIZE_INDEX) { | |
| alloc_index = DEFAULT_REQUEST_BUFFER_SIZE_INDEX; | |
| } | |
| } else { | |
| alloc_index = buffer_size_to_index(t_state.hdr_info.request_content_length, t_state.http_config_param->max_payload_iobuf_index); | |
| } | |
| MIOBuffer *post_buffer = new_MIOBuffer(alloc_index); | |
| IOBufferReader *buf_start = post_buffer->alloc_reader(); | |
| this->_postbuf.init(post_buffer->clone_reader(buf_start)); | |
| // Note: Many browsers, Netscape and IE included send two extra | |
| // bytes (CRLF) at the end of the post. We just ignore those | |
| // bytes since the sending them is not spec | |
| // Next order of business if copy the remaining data from the | |
| // header buffer into new buffer | |
| int64_t post_bytes = chunked ? INT64_MAX : t_state.hdr_info.request_content_length; | |
| post_buffer->write(_ua.get_txn()->get_remote_reader(), chunked ? _ua.get_txn()->get_remote_reader()->read_avail() : post_bytes); | |
| p = tunnel.add_producer(_ua.get_entry()->vc, post_bytes, buf_start, &HttpSM::tunnel_handler_post_ua, | |
| HttpTunnelType_t::BUFFER_READ, "ua post buffer"); | |
| if (chunked) { | |
| bool const drop_chunked_trailers = t_state.http_config_param->oride.http_drop_chunked_trailers == 1; | |
| bool const parse_chunk_strictly = t_state.http_config_param->oride.http_strict_chunk_parsing == 1; | |
| tunnel.set_producer_chunking_action(p, 0, TunnelChunkingAction_t::PASSTHRU_CHUNKED_CONTENT, drop_chunked_trailers, | |
| parse_chunk_strictly); | |
| } | |
| _ua.get_entry()->in_tunnel = true; | |
| _ua.get_txn()->set_inactivity_timeout(HRTIME_SECONDS(t_state.txn_conf->transaction_no_activity_timeout_in)); | |
| tunnel.tunnel_run(p); | |
| } | |
| void | |
| HttpSM::generate_cache_key(HttpCacheKey *key, URL *url) | |
| { | |
| Cache::generate_key(key, url, t_state.txn_conf->cache_ignore_query, t_state.txn_conf->cache_generation_number); | |
| } | |
| void | |
| HttpSM::generate_cache_key92(HttpCacheKey *key, URL *url) | |
| { | |
| Cache::generate_key92(key, url, t_state.txn_conf->cache_ignore_query, t_state.txn_conf->cache_generation_number); | |
| } | |
| int | |
| HttpSM::state_watch_for_client_abort(int event, void *data) | |
| { | |
| STATE_ENTER(state_watch_for_client_abort, event); | |
| ink_assert(_ua.get_entry()->read_vio == (VIO *)data || _ua.get_entry()->write_vio == (VIO *)data); | |
| ink_assert(_ua.get_entry()->vc == _ua.get_txn()); | |
| switch (event) { | |
| /* EOS means that the client has initiated the connection shut down. | |
| * Only half close the client connection so ATS can read additional | |
| * data that may still be sent from the server and send it to the | |
| * client. | |
| */ | |
| case VC_EVENT_EOS: { | |
| // We got an early EOS. | |
| if (!terminate_sm) { // Not done already | |
| // ProxySession::do_io_shutdown dereferences its NetVConnection | |
| // unconditionally, so only shut down while the peer is still attached. | |
| NetVConnection *netvc = _ua.get_txn()->get_netvc(); | |
| if (_ua.get_txn()->allow_half_open() || tunnel.has_consumer_besides_client()) { | |
| if (netvc) { | |
| // Shut the read side down through the transaction rather than through | |
| // the NetVConnection. For multiplexed protocols the NetVConnection is | |
| // shared by every stream on the connection, so shutting its read side | |
| // down here would stop the session from reading frames for all of the | |
| // other streams. HTTP/2 and HTTP/3 therefore implement | |
| // do_io_shutdown() as a no-op. | |
| _ua.get_txn()->do_io_shutdown(IO_SHUTDOWN_READ); | |
| } | |
| } else if (t_state.txn_conf->allow_half_open > 0 && t_state.txn_conf->cache_http && | |
| (server_entry != nullptr && server_entry->vc_read_handler == &HttpSM::state_read_server_response_header)) { | |
| // Half open connections are configured, but the transport does not support them (e.g. TLS or HTTP/2). If HttpSM is | |
| // waiting response header from origin server, keep it for a while to run background fetch. Note that the operator | |
| // disabling half open connections is handled below: the transaction is aborted rather than kept alive for a client | |
| // that is no longer there. | |
| _ua.get_txn()->do_io_shutdown(IO_SHUTDOWN_READWRITE); | |
| } else { | |
| _ua.get_txn()->do_io_close(); | |
| vc_table.cleanup_entry(_ua.get_entry()); | |
| _ua.set_entry(nullptr); | |
| tunnel.kill_tunnel(); | |
| terminate_sm = true; // Just die already, the requester is gone | |
| set_ua_abort(HttpTransact::ABORTED, event); | |
| } | |
| if (_ua.get_entry()) { | |
| _ua.get_entry()->eos = true; | |
| } | |
| } | |
| break; | |
| } | |
| case VC_EVENT_ERROR: | |
| case VC_EVENT_ACTIVE_TIMEOUT: | |
| case VC_EVENT_INACTIVITY_TIMEOUT: { | |
| if (tunnel.is_tunnel_active()) { | |
| // Check to see if the user agent is part of the tunnel. | |
| // If so forward the event to the tunnel. Otherwise, | |
| // kill the tunnel and fallthrough to the case | |
| // where the tunnel is not active | |
| HttpTunnelConsumer *c = tunnel.get_consumer(_ua.get_txn()); | |
| if (c && c->alive) { | |
| SMDbg(dbg_ctl_http, "forwarding event %s to tunnel", HttpDebugNames::get_event_name(event)); | |
| tunnel.handleEvent(event, c->write_vio); | |
| return 0; | |
| } else { | |
| tunnel.kill_tunnel(); | |
| } | |
| } | |
| // Disable further I/O on the client | |
| if (_ua.get_entry()->read_vio) { | |
| _ua.get_entry()->read_vio->nbytes = _ua.get_entry()->read_vio->ndone; | |
| } | |
| ATS_PROBE1(milestone_ua_close, sm_id); | |
| milestones[TS_MILESTONE_UA_CLOSE] = ink_get_hrtime(); | |
| set_ua_abort(HttpTransact::ABORTED, event); | |
| terminate_sm = true; | |
| break; | |
| } | |
| case VC_EVENT_READ_COMPLETE: | |
| // XXX Work around for TS-1233. | |
| case VC_EVENT_READ_READY: | |
| // Ignore. Could be a pipelined request. We'll get to it | |
| // when we finish the current transaction | |
| break; | |
| case VC_EVENT_WRITE_READY: | |
| // 100-continue handler | |
| ink_assert(t_state.hdr_info.client_request.m_100_continue_required || t_state.http_config_param->send_100_continue_response); | |
| _ua.get_entry()->write_vio->reenable(); | |
| break; | |
| case VC_EVENT_WRITE_COMPLETE: | |
| // 100-continue handler | |
| ink_assert(t_state.hdr_info.client_request.m_100_continue_required || t_state.http_config_param->send_100_continue_response); | |
| if (_ua.get_entry()->write_buffer) { | |
| ink_assert(_ua.get_entry()->write_vio && !_ua.get_entry()->write_vio->ntodo()); | |
| free_MIOBuffer(_ua.get_entry()->write_buffer); | |
| _ua.get_entry()->write_buffer = nullptr; | |
| } | |
| break; | |
| default: | |
| ink_release_assert(0); | |
| break; | |
| } | |
| return 0; | |
| } | |
| void | |
| HttpSM::setup_push_read_response_header() | |
| { | |
| ink_assert(server_txn == nullptr); | |
| ink_assert(server_entry == nullptr); | |
| ink_assert(_ua.get_txn() != nullptr); | |
| ink_assert(t_state.method == HTTP_WKSIDX_PUSH); | |
| // Set the handler to read the pushed response hdr | |
| _ua.get_entry()->vc_read_handler = &HttpSM::state_read_push_response_header; | |
| // We record both the total payload size as | |
| // client_request_body_bytes and the bytes for the individual | |
| // pushed hdr and body components | |
| pushed_response_hdr_bytes = 0; | |
| client_request_body_bytes = 0; | |
| // Note: we must use destroy() here since clear() | |
| // does not free the memory from the header | |
| t_state.hdr_info.server_response.destroy(); | |
| t_state.hdr_info.server_response.create(HTTPType::RESPONSE); | |
| http_parser_clear(&http_parser); | |
| // We already done the READ when we read the client | |
| // request header | |
| ink_assert(_ua.get_entry()->read_vio); | |
| // If there is anything in the buffer call the parsing routines | |
| // since if the response is finished, we won't get any | |
| // additional callbacks | |
| int resp_hdr_state = VC_EVENT_CONT; | |
| if (_ua.get_txn()->get_remote_reader()->read_avail() > 0) { | |
| if (_ua.get_entry()->eos) { | |
| resp_hdr_state = state_read_push_response_header(VC_EVENT_EOS, _ua.get_entry()->read_vio); | |
| } else { | |
| resp_hdr_state = state_read_push_response_header(VC_EVENT_READ_READY, _ua.get_entry()->read_vio); | |
| } | |
| } | |
| // It is possible that the entire PUSHed response header was already | |
| // in the buffer. In this case we don't want to fire off any more | |
| // IO since we are going to switch buffers when we go to tunnel to | |
| // the cache | |
| if (resp_hdr_state == VC_EVENT_CONT) { | |
| ink_assert(_ua.get_entry()->eos == false); | |
| _ua.get_entry()->read_vio = _ua.get_txn()->do_io_read(this, INT64_MAX, _ua.get_txn()->get_remote_reader()->mbuf); | |
| } | |
| } | |
| int | |
| HttpSM::state_read_push_response_header(int event, void *data) | |
| { | |
| STATE_ENTER(state_read_push_response_header, event); | |
| ink_assert(_ua.get_entry()->read_vio == (VIO *)data); | |
| ink_assert(t_state.current.server == nullptr); | |
| switch (event) { | |
| case VC_EVENT_EOS: | |
| _ua.get_entry()->eos = true; | |
| // Fall through | |
| case VC_EVENT_READ_READY: | |
| case VC_EVENT_READ_COMPLETE: | |
| // More data to parse | |
| break; | |
| case VC_EVENT_ERROR: | |
| case VC_EVENT_INACTIVITY_TIMEOUT: | |
| case VC_EVENT_ACTIVE_TIMEOUT: | |
| // The user agent is hosed. Send an error | |
| set_ua_abort(HttpTransact::ABORTED, event); | |
| call_transact_and_set_next_state(HttpTransact::HandleBadPushRespHdr); | |
| return 0; | |
| } | |
| auto state = ParseResult::CONT; | |
| while (_ua.get_txn()->get_remote_reader()->read_avail() && state == ParseResult::CONT) { | |
| const char *start = _ua.get_txn()->get_remote_reader()->start(); | |
| const char *tmp = start; | |
| int64_t data_size = _ua.get_txn()->get_remote_reader()->block_read_avail(); | |
| ink_assert(data_size >= 0); | |
| ///////////////////// | |
| // tokenize header // | |
| ///////////////////// | |
| state = t_state.hdr_info.server_response.parse_resp(&http_parser, &tmp, tmp + data_size, | |
| false); // Only call w/ eof when data exhausted | |
| int64_t bytes_used = tmp - start; | |
| ink_release_assert(bytes_used <= data_size); | |
| _ua.get_txn()->get_remote_reader()->consume(bytes_used); | |
| pushed_response_hdr_bytes += bytes_used; | |
| client_request_body_bytes += bytes_used; | |
| } | |
| // We are out of data. If we've received an EOS we need to | |
| // call the parser with (eof == true) so it can determine | |
| // whether to use the response as is or declare a parse error | |
| if (_ua.get_entry()->eos) { | |
| const char *end = _ua.get_txn()->get_remote_reader()->start(); | |
| state = t_state.hdr_info.server_response.parse_resp(&http_parser, &end, end, true // We are out of data after server eos | |
| ); | |
| ink_release_assert(state == ParseResult::DONE || state == ParseResult::ERROR); | |
| } | |
| // Don't allow 0.9 (unparsable headers) since TS doesn't | |
| // cache 0.9 responses | |
| if (state == ParseResult::DONE && t_state.hdr_info.server_response.version_get() == HTTP_0_9) { | |
| state = ParseResult::ERROR; | |
| } | |
| if (state != ParseResult::CONT) { | |
| // Disable further IO | |
| _ua.get_entry()->read_vio->nbytes = _ua.get_entry()->read_vio->ndone; | |
| http_parser_clear(&http_parser); | |
| ATS_PROBE1(milestone_server_read_header_done, sm_id); | |
| milestones[TS_MILESTONE_SERVER_READ_HEADER_DONE] = ink_get_hrtime(); | |
| } | |
| switch (state) { | |
| case ParseResult::ERROR: | |
| SMDbg(dbg_ctl_http, "error parsing push response header"); | |
| call_transact_and_set_next_state(HttpTransact::HandleBadPushRespHdr); | |
| break; | |
| case ParseResult::CONT: | |
| _ua.get_entry()->read_vio->reenable(); | |
| return VC_EVENT_CONT; | |
| case ParseResult::DONE: | |
| SMDbg(dbg_ctl_http, "done parsing push response header"); | |
| call_transact_and_set_next_state(HttpTransact::HandlePushResponseHdr); | |
| break; | |
| default: | |
| ink_assert(!"not reached"); | |
| } | |
| return VC_EVENT_DONE; | |
| } | |
| ////////////////////////////////////////////////////////////////////////////// | |
| // | |
| // HttpSM::state_raw_http_server_open() | |
| // | |
| ////////////////////////////////////////////////////////////////////////////// | |
| int | |
| HttpSM::state_raw_http_server_open(int event, void *data) | |
| { | |
| STATE_ENTER(state_raw_http_server_open, event); | |
| ink_assert(server_entry == nullptr); | |
| ATS_PROBE1(milestone_server_connect_end, sm_id); | |
| milestones[TS_MILESTONE_SERVER_CONNECT_END] = ink_get_hrtime(); | |
| NetVConnection *netvc = nullptr; | |
| pending_action = nullptr; | |
| switch (event) { | |
| case NET_EVENT_OPEN: { | |
| netvc = static_cast<NetVConnection *>(data); | |
| if (plugin_tunnel_type == HttpPluginTunnel_t::NONE) { | |
| _netvc = netvc; | |
| _netvc_read_buffer = new_MIOBuffer(HTTP_SERVER_RESP_HDR_BUFFER_INDEX); | |
| _netvc_reader = _netvc_read_buffer->alloc_reader(); | |
| // Wait for write readiness to verify that the nonblocking TCP connection | |
| // completed before reporting a successful tunnel to the client. | |
| _netvc->do_io_write(this, 1, _netvc_reader); | |
| _netvc->set_inactivity_timeout(get_server_connect_timeout()); | |
| return 0; | |
| } | |
| [[fallthrough]]; | |
| } | |
| case VC_EVENT_READ_COMPLETE: | |
| case VC_EVENT_WRITE_READY: | |
| case VC_EVENT_WRITE_COMPLETE: { | |
| if (netvc == nullptr) { | |
| netvc = _netvc; | |
| netvc->do_io_write(nullptr, 0, nullptr); | |
| free_MIOBuffer(_netvc_read_buffer); | |
| _netvc = nullptr; | |
| _netvc_read_buffer = nullptr; | |
| _netvc_reader = nullptr; | |
| } | |
| // Record the VC in our table | |
| server_entry = vc_table.new_entry(); | |
| server_entry->vc = netvc; | |
| server_entry->vc_type = HttpVC_t::RAW_SERVER_VC; | |
| t_state.current.state = HttpTransact::CONNECTION_ALIVE; | |
| ats_ip_copy(&t_state.server_info.src_addr, netvc->get_local_addr()); | |
| netvc->set_inactivity_timeout(get_server_inactivity_timeout()); | |
| netvc->set_active_timeout(get_server_active_timeout()); | |
| t_state.current.server->clear_connect_fail(); | |
| if (get_tunnel_type() != SNIRoutingType::NONE) { | |
| tunnel.mark_tls_tunnel_active(); | |
| } | |
| break; | |
| } | |
| case VC_EVENT_INACTIVITY_TIMEOUT: | |
| case VC_EVENT_ACTIVE_TIMEOUT: | |
| t_state.set_connect_fail(ETIMEDOUT); | |
| [[fallthrough]]; | |
| case VC_EVENT_ERROR: | |
| case VC_EVENT_EOS: | |
| case NET_EVENT_OPEN_FAILED: { | |
| if (_netvc != nullptr) { | |
| if (event == VC_EVENT_ERROR || event == NET_EVENT_OPEN_FAILED) { | |
| t_state.set_connect_fail(_netvc->lerrno); | |
| } | |
| _netvc->do_io_write(nullptr, 0, nullptr); | |
| _netvc->do_io_close(); | |
| _netvc = nullptr; | |
| free_MIOBuffer(_netvc_read_buffer); | |
| _netvc_read_buffer = nullptr; | |
| _netvc_reader = nullptr; | |
| } | |
| if (t_state.cause_of_death_errno == -UNKNOWN_INTERNAL_ERROR) { | |
| if (event == VC_EVENT_EOS) { | |
| t_state.set_connect_fail(EPIPE); | |
| } else { | |
| t_state.set_connect_fail(EIO); | |
| } | |
| } | |
| t_state.current.state = HttpTransact::OPEN_RAW_ERROR; | |
| // use this value just to get around other values | |
| t_state.hdr_info.response_error = HttpTransact::ResponseError_t::STATUS_CODE_SERVER_ERROR; | |
| break; | |
| } | |
| case EVENT_INTERVAL: | |
| // If we get EVENT_INTERNAL it means that we moved the transaction | |
| // to a different thread in do_http_server_open. Since we didn't | |
| // do any of the actual work in do_http_server_open, we have to | |
| // go back and do it now. | |
| do_http_server_open(true); | |
| return 0; | |
| default: | |
| ink_release_assert(0); | |
| break; | |
| } | |
| call_transact_and_set_next_state(HttpTransact::OriginServerRawOpen); | |
| return 0; | |
| } | |
| // int HttpSM::state_request_wait_for_transform_read(int event, void* data) | |
| // | |
| // We've done a successful transform open and issued a do_io_write | |
| // to the transform. We are now ready for the transform to tell | |
| // us it is now ready to be read from and it done modifying the | |
| // server request header | |
| // | |
| int | |
| HttpSM::state_request_wait_for_transform_read(int event, void *data) | |
| { | |
| STATE_ENTER(state_request_wait_for_transform_read, event); | |
| int64_t size; | |
| switch (event) { | |
| case TRANSFORM_READ_READY: | |
| size = *(static_cast<int64_t *>(data)); | |
| if (size != INT64_MAX && size >= 0) { | |
| // We got a content length so update our internal | |
| // data as well as fix up the request header | |
| t_state.hdr_info.transform_request_cl = size; | |
| t_state.hdr_info.server_request.value_set_int64(static_cast<std::string_view>(MIME_FIELD_CONTENT_LENGTH), size); | |
| setup_server_send_request_api(); | |
| break; | |
| } else { | |
| // No content length from the post. This is a no go | |
| // since http spec requires content length when | |
| // sending a request message body. Change the event | |
| // to an error and fall through | |
| event = VC_EVENT_ERROR; | |
| Log::error("Request transformation failed to set content length"); | |
| } | |
| // FALLTHROUGH | |
| default: | |
| state_common_wait_for_transform_read(&post_transform_info, &HttpSM::tunnel_handler_post, event, data); | |
| break; | |
| } | |
| return 0; | |
| } | |
| // int HttpSM::state_response_wait_for_transform_read(int event, void* data) | |
| // | |
| // We've done a successful transform open and issued a do_io_write | |
| // to the transform. We are now ready for the transform to tell | |
| // us it is now ready to be read from and it done modifying the | |
| // user agent response header | |
| // | |
| int | |
| HttpSM::state_response_wait_for_transform_read(int event, void *data) | |
| { | |
| STATE_ENTER(state_response_wait_for_transform_read, event); | |
| int64_t size = *(static_cast<int64_t *>(data)); | |
| switch (event) { | |
| case TRANSFORM_READ_READY: | |
| if (size != INT64_MAX && size >= 0) { | |
| // We got a content length so update our internal state | |
| t_state.hdr_info.transform_response_cl = size; | |
| t_state.hdr_info.transform_response.value_set_int64(static_cast<std::string_view>(MIME_FIELD_CONTENT_LENGTH), size); | |
| } else { | |
| t_state.hdr_info.transform_response_cl = HTTP_UNDEFINED_CL; | |
| } | |
| call_transact_and_set_next_state(HttpTransact::handle_transform_ready); | |
| break; | |
| default: | |
| state_common_wait_for_transform_read(&transform_info, &HttpSM::tunnel_handler, event, data); | |
| break; | |
| } | |
| return 0; | |
| } | |
| // int HttpSM::state_common_wait_for_transform_read(...) | |
| // | |
| // This function handles the overlapping cases between request and response | |
| // transforms which prevents code duplication | |
| // | |
| int | |
| HttpSM::state_common_wait_for_transform_read(HttpTransformInfo *t_info, HttpSMHandler tunnel_handler, int event, void *data) | |
| { | |
| STATE_ENTER(state_common_wait_for_transform_read, event); | |
| HttpTunnelConsumer *c = nullptr; | |
| bool transform_closed = false; | |
| switch (event) { | |
| case HTTP_TUNNEL_EVENT_DONE: | |
| // There are three reasons why the tunnel could signal completed | |
| // 1) there was error from the transform write | |
| // 2) there was an error from the data source | |
| // 3) the transform write completed before it sent | |
| // TRANSFORM_READ_READY which is legal and in which | |
| // case we should just wait for the transform read ready | |
| c = tunnel.get_consumer(t_info->vc); | |
| ink_assert(c != nullptr); | |
| ink_assert(c->vc == t_info->entry->vc); | |
| if (c->handler_state == HTTP_SM_TRANSFORM_FAIL) { | |
| // Case 1 we failed to complete the write to the | |
| // transform fall through to vc event error case | |
| ink_assert(c->write_success == false); | |
| } else if (c->producer->read_success == false) { | |
| // Case 2 - error from data source | |
| if (c->producer->vc_type == HttpTunnelType_t::HTTP_CLIENT) { | |
| // Our source is the client. POST can't | |
| // be truncated so forward to the tunnel | |
| // handler to clean this mess up | |
| ink_assert(t_info == &post_transform_info); | |
| return (this->*tunnel_handler)(event, data); | |
| } else if (c->write_success) { | |
| // On the response side, we just forward as much | |
| // as we can of truncated documents so | |
| // just don't cache the result | |
| ink_assert(t_info == &transform_info); | |
| t_state.api_info.cache_transformed = false; | |
| return 0; | |
| } | |
| // The tunnel aborted and closed the transform before the | |
| // transform signaled TRANSFORM_READ_READY, so it never | |
| // will. Fall through to the error case so the client gets | |
| // an error response instead of waiting forever. | |
| ink_assert(t_info == &transform_info); | |
| transform_closed = true; | |
| if (c->producer->vc_type == HttpTunnelType_t::HTTP_SERVER) { | |
| t_state.http_return_code = HTTPStatus::BAD_GATEWAY; | |
| } | |
| } else { | |
| // Case 3 - wait for transform read ready | |
| return 0; | |
| } | |
| // FALLTHROUGH | |
| case VC_EVENT_ERROR: | |
| case VC_EVENT_EOS: | |
| case VC_EVENT_INACTIVITY_TIMEOUT: | |
| // Transform VC sends NULL on error conditions | |
| if (!c) { | |
| c = tunnel.get_consumer(t_info->vc); | |
| ink_assert(c != nullptr); | |
| } | |
| vc_table.cleanup_entry(t_info->entry); | |
| t_info->entry = nullptr; | |
| // In Case 1: error due to transform write, and when the tunnel | |
| // already closed the transform, we need to keep the original | |
| // t_info->vc for transform_cleanup() to skip do_io_close(); | |
| // otherwise, set it to NULL. | |
| if (c->handler_state != HTTP_SM_TRANSFORM_FAIL && !transform_closed) { | |
| t_info->vc = nullptr; | |
| } | |
| if (c->producer->vc_type == HttpTunnelType_t::HTTP_CLIENT) { | |
| /* Producer was the user agent and there was a failure transforming the POST. | |
| Handling this is challenging and this isn't the best way but it at least | |
| avoids a crash due to trying to send a response to a NULL'd out user agent. | |
| The problem with not closing the user agent is handling draining of the | |
| rest of the POST - the user agent may well not check for a response until that's | |
| done in which case we can get a deadlock where the user agent never reads the | |
| error response because the POST wasn't drained and the buffers filled up. | |
| Draining has a potential bad impact on any pipelining which must be considered. | |
| If we're not going to drain properly the next best choice is to shut down the | |
| entire state machine since (1) there's no point in finishing the POST to the | |
| origin and (2) there's no user agent connection to which to send the error response. | |
| */ | |
| terminate_sm = true; | |
| } else { | |
| tunnel.kill_tunnel(); | |
| call_transact_and_set_next_state(HttpTransact::HandleApiErrorJump); | |
| } | |
| break; | |
| default: | |
| ink_release_assert(0); | |
| } | |
| return 0; | |
| } | |
| // int HttpSM::state_api_callback(int event, void *data) | |
| // InkAPI.cc calls us directly here to avoid problems | |
| // with setting and changing the default_handler | |
| // function. As such, this is an entry point | |
| // and needs to handle the reentrancy counter and | |
| // deallocation of the state machine if necessary | |
| // | |
| int | |
| HttpSM::state_api_callback(int event, void *data) | |
| { | |
| ink_release_assert(magic == HttpSmMagic_t::ALIVE); | |
| ink_assert(reentrancy_count >= 0); | |
| reentrancy_count++; | |
| this->milestone_update_api_time(); | |
| STATE_ENTER(state_api_callback, event); | |
| state_api_callout(event, data); | |
| // The sub-handler signals when it is time for the state | |
| // machine to exit. We can only exit if we are not reentrantly | |
| // called otherwise when the our call unwinds, we will be | |
| // running on a dead state machine | |
| // | |
| // Because of the need for an api shutdown hook, kill_this() | |
| // is also reentrant. As such, we don't want to decrement | |
| // the reentrancy count until after we run kill_this() | |
| // | |
| if (terminate_sm == true && reentrancy_count == 1) { | |
| kill_this(); | |
| } else { | |
| reentrancy_count--; | |
| ink_assert(reentrancy_count >= 0); | |
| } | |
| return VC_EVENT_CONT; | |
| } | |
| int | |
| HttpSM::state_api_callout(int event, void * /* data ATS_UNUSED */) | |
| { | |
| // enum and variable for figuring out what the next action is after | |
| // after we've finished the api state | |
| enum class AfterApiReturn_t { | |
| UNKNOWN = 0, | |
| CONTINUE, | |
| DEFERED_CLOSE, | |
| DEFERED_SERVER_ERROR, | |
| ERROR_JUMP, | |
| SHUTDOWN, | |
| INVALIDATE_ERROR | |
| }; | |
| AfterApiReturn_t api_next = AfterApiReturn_t::UNKNOWN; | |
| if (event != EVENT_NONE) { | |
| STATE_ENTER(state_api_callout, event); | |
| } | |
| if (api_timer < 0) { | |
| // This happens when either the plugin lock was missed and the hook rescheduled or | |
| // the transaction got an event without the plugin calling TsHttpTxnReenable(). | |
| // The call chain does not recurse here if @a api_timer < 0 which means this call | |
| // is the first from an event dispatch in this case. | |
| this->milestone_update_api_time(); | |
| } | |
| switch (event) { | |
| case HTTP_TUNNEL_EVENT_DONE: | |
| // This is a reschedule via the tunnel. Just fall through | |
| // | |
| case EVENT_INTERVAL: | |
| pending_action = nullptr; | |
| // FALLTHROUGH | |
| case EVENT_NONE: | |
| if (cur_hook_id == TS_HTTP_TXN_START_HOOK && t_state.client_info.port_attribute == HttpProxyPort::TRANSPORT_BLIND_TUNNEL) { | |
| /* Creating the request object early to set the host header and port for blind tunneling here for the | |
| plugins required to work with sni_routing. | |
| */ | |
| // Plugins triggered on txn_start_hook will get the host and port at that point | |
| // We've received a request on a port which we blind forward | |
| URL u; | |
| t_state.hdr_info.client_request.create(HTTPType::REQUEST); | |
| t_state.hdr_info.client_request.method_set(static_cast<std::string_view>(HTTP_METHOD_CONNECT)); | |
| t_state.hdr_info.client_request.url_create(&u); | |
| u.scheme_set(std::string_view{URL_SCHEME_TUNNEL}); | |
| t_state.hdr_info.client_request.url_set(&u); | |
| NetVConnection *netvc = _ua.get_txn()->get_netvc(); | |
| if (auto tts = netvc->get_service<TLSTunnelSupport>(); tts) { | |
| if (tts->has_tunnel_destination()) { | |
| auto tunnel_host = tts->get_tunnel_host(); | |
| t_state.hdr_info.client_request.url_get()->host_set(tunnel_host); | |
| ushort tunnel_port = tts->get_tunnel_port(); | |
| if (tunnel_port > 0) { | |
| t_state.hdr_info.client_request.url_get()->port_set(tunnel_port); | |
| } else { | |
| t_state.hdr_info.client_request.url_get()->port_set(netvc->get_local_port()); | |
| } | |
| } else { | |
| const char *server_name = ""; | |
| if (auto *snis = netvc->get_service<TLSSNISupport>(); snis) { | |
| server_name = snis->get_sni_server_name(); | |
| } | |
| t_state.hdr_info.client_request.url_get()->host_set({server_name}); | |
| t_state.hdr_info.client_request.url_get()->port_set(netvc->get_local_port()); | |
| } | |
| } | |
| } | |
| // FALLTHROUGH | |
| case HTTP_API_CONTINUE: | |
| if (nullptr == cur_hook) { | |
| cur_hook = hook_state.getNext(); | |
| } | |
| if (cur_hook) { | |
| if (callout_state == HttpApiState_t::NO_CALLOUT) { | |
| callout_state = HttpApiState_t::IN_CALLOUT; | |
| } | |
| WEAK_MUTEX_TRY_LOCK(lock, cur_hook->m_cont->mutex, mutex->thread_holding); | |
| // Have a mutex but didn't get the lock, reschedule | |
| if (!lock.is_locked()) { | |
| api_timer = -ink_get_hrtime(); | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::state_api_callout); | |
| ink_release_assert(pending_action.empty()); | |
| pending_action = this_ethread()->schedule_in(this, HRTIME_MSECONDS(10)); | |
| return -1; | |
| } | |
| SMDbg(dbg_ctl_http, "calling plugin on hook %s at hook %p", HttpDebugNames::get_api_hook_name(cur_hook_id), cur_hook); | |
| APIHook const *hook = cur_hook; | |
| // Need to delay the next hook update until after this hook is called to handle dynamic | |
| // callback manipulation. cur_hook isn't needed to track state (in hook_state). | |
| cur_hook = nullptr; | |
| if (!api_timer) { | |
| api_timer = ink_get_hrtime(); | |
| } | |
| hook->invoke(TS_EVENT_HTTP_READ_REQUEST_HDR + static_cast<int>(cur_hook_id), this); | |
| if (api_timer > 0) { // true if the hook did not call TxnReenable() | |
| this->milestone_update_api_time(); | |
| api_timer = -ink_get_hrtime(); // set in order to track non-active callout duration | |
| // which means that if we get back from the invoke with api_timer < 0 we're already | |
| // tracking a non-complete callout from a chain so just let it ride. It will get cleaned | |
| // up in state_api_callback when the plugin re-enables this transaction. | |
| } | |
| return 0; | |
| } | |
| // Map the callout state into api_next | |
| switch (callout_state) { | |
| case HttpApiState_t::NO_CALLOUT: | |
| case HttpApiState_t::IN_CALLOUT: | |
| if (t_state.api_modifiable_cached_resp && t_state.api_update_cached_object == HttpTransact::UpdateCachedObject_t::PREPARE) { | |
| t_state.api_update_cached_object = HttpTransact::UpdateCachedObject_t::CONTINUE; | |
| } | |
| api_next = AfterApiReturn_t::CONTINUE; | |
| break; | |
| case HttpApiState_t::DEFERED_CLOSE: | |
| api_next = AfterApiReturn_t::DEFERED_CLOSE; | |
| break; | |
| case HttpApiState_t::DEFERED_SERVER_ERROR: | |
| api_next = AfterApiReturn_t::DEFERED_SERVER_ERROR; | |
| break; | |
| case HttpApiState_t::REWIND_STATE_MACHINE: | |
| SMDbg(dbg_ctl_http, "REWIND"); | |
| callout_state = HttpApiState_t::NO_CALLOUT; | |
| set_next_state(); | |
| return 0; | |
| default: | |
| ink_release_assert(0); | |
| } | |
| break; | |
| case HTTP_API_ERROR: | |
| if (callout_state == HttpApiState_t::DEFERED_CLOSE) { | |
| api_next = AfterApiReturn_t::DEFERED_CLOSE; | |
| } else if (cur_hook_id == TS_HTTP_TXN_CLOSE_HOOK) { | |
| // If we are closing the state machine, we can't | |
| // jump to an error state so just continue | |
| api_next = AfterApiReturn_t::CONTINUE; | |
| } else if (t_state.api_http_sm_shutdown) { | |
| t_state.api_http_sm_shutdown = false; | |
| t_state.cache_info.object_read = nullptr; | |
| cache_sm.close_read(); | |
| transform_cache_sm.close_read(); | |
| release_server_session(); | |
| terminate_sm = true; | |
| api_next = AfterApiReturn_t::SHUTDOWN; | |
| t_state.squid_codes.log_code = SquidLogCode::TCP_DENIED; | |
| } else if (t_state.api_modifiable_cached_resp && | |
| t_state.api_update_cached_object == HttpTransact::UpdateCachedObject_t::PREPARE) { | |
| t_state.api_update_cached_object = HttpTransact::UpdateCachedObject_t::ERROR; | |
| api_next = AfterApiReturn_t::INVALIDATE_ERROR; | |
| } else { | |
| api_next = AfterApiReturn_t::ERROR_JUMP; | |
| } | |
| break; | |
| // Eat the EOS while we are waiting for any locks to complete the transaction | |
| case VC_EVENT_EOS: | |
| return 0; | |
| default: | |
| ink_assert(false); | |
| terminate_sm = true; | |
| return 0; | |
| } | |
| // Now that we're completed with the api state and figured out what | |
| // to do next, do it | |
| callout_state = HttpApiState_t::NO_CALLOUT; | |
| api_timer = 0; | |
| switch (api_next) { | |
| case AfterApiReturn_t::CONTINUE: | |
| handle_api_return(); | |
| break; | |
| case AfterApiReturn_t::DEFERED_CLOSE: | |
| ink_assert(t_state.api_next_action == HttpTransact::StateMachineAction_t::API_SM_SHUTDOWN); | |
| do_api_callout(); | |
| break; | |
| case AfterApiReturn_t::DEFERED_SERVER_ERROR: | |
| ink_assert(t_state.api_next_action == HttpTransact::StateMachineAction_t::API_SEND_REQUEST_HDR); | |
| ink_assert(t_state.current.state != HttpTransact::CONNECTION_ALIVE); | |
| call_transact_and_set_next_state(HttpTransact::HandleResponse); | |
| break; | |
| case AfterApiReturn_t::ERROR_JUMP: | |
| call_transact_and_set_next_state(HttpTransact::HandleApiErrorJump); | |
| break; | |
| case AfterApiReturn_t::SHUTDOWN: | |
| break; | |
| case AfterApiReturn_t::INVALIDATE_ERROR: | |
| do_cache_prepare_update(); | |
| break; | |
| default: | |
| case AfterApiReturn_t::UNKNOWN: | |
| ink_release_assert(0); | |
| } | |
| return 0; | |
| } | |
| // void HttpSM::handle_api_return() | |
| // | |
| // Figures out what to do after calling api callouts | |
| // have finished. This is messy and I would like | |
| // to come up with a cleaner way to handle the api | |
| // return. The way we are doing things also makes a | |
| // mess of set_next_state() | |
| // | |
| void | |
| HttpSM::handle_api_return() | |
| { | |
| switch (t_state.api_next_action) { | |
| case HttpTransact::StateMachineAction_t::API_SM_START: { | |
| NetVConnection *netvc = _ua.get_txn()->get_netvc(); | |
| auto *tts = netvc->get_service<TLSTunnelSupport>(); | |
| bool forward_dest = tts != nullptr && tts->is_decryption_needed(); | |
| if (t_state.client_info.port_attribute == HttpProxyPort::TRANSPORT_BLIND_TUNNEL || forward_dest) { | |
| setup_blind_tunnel_port(); | |
| } else { | |
| setup_client_read_request_header(); | |
| } | |
| return; | |
| } | |
| case HttpTransact::StateMachineAction_t::API_CACHE_LOOKUP_COMPLETE: | |
| case HttpTransact::StateMachineAction_t::API_READ_CACHE_HDR: | |
| if (t_state.api_cleanup_cache_read && t_state.api_update_cached_object != HttpTransact::UpdateCachedObject_t::PREPARE) { | |
| t_state.api_cleanup_cache_read = false; | |
| t_state.cache_info.object_read = nullptr; | |
| t_state.request_sent_time = UNDEFINED_TIME; | |
| t_state.response_received_time = UNDEFINED_TIME; | |
| cache_sm.close_read(); | |
| transform_cache_sm.close_read(); | |
| } | |
| // fallthrough | |
| case HttpTransact::StateMachineAction_t::API_PRE_REMAP: | |
| case HttpTransact::StateMachineAction_t::API_POST_REMAP: | |
| case HttpTransact::StateMachineAction_t::API_READ_REQUEST_HDR: | |
| case HttpTransact::StateMachineAction_t::REQUEST_BUFFER_READ_COMPLETE: | |
| case HttpTransact::StateMachineAction_t::API_OS_DNS: | |
| call_transact_and_set_next_state(nullptr); | |
| return; | |
| case HttpTransact::StateMachineAction_t::API_READ_RESPONSE_HDR: | |
| // Plugins may mutate response cache-control headers in this hook. Re-cook | |
| // targeted cache-control after hooks so downstream cache decisions see the | |
| // final header state with targeted precedence. | |
| if (t_state.hdr_info.server_response.m_mime) { | |
| t_state.hdr_info.server_response.m_mime->recompute_cooked_stuff( | |
| nullptr, t_state.txn_conf->targeted_cache_control_headers.get_headers(), | |
| t_state.txn_conf->targeted_cache_control_headers.get_count()); | |
| } | |
| call_transact_and_set_next_state(nullptr); | |
| return; | |
| case HttpTransact::StateMachineAction_t::API_TUNNEL_START: | |
| // Finished the Tunnel start callback. Go ahead and do the HandleBlindTunnel | |
| call_transact_and_set_next_state(HttpTransact::HandleBlindTunnel); | |
| return; | |
| case HttpTransact::StateMachineAction_t::API_SEND_REQUEST_HDR: | |
| setup_server_send_request(); | |
| return; | |
| case HttpTransact::StateMachineAction_t::API_SEND_RESPONSE_HDR: | |
| // Set back the inactivity timeout | |
| if (_ua.get_txn()) { | |
| _ua.get_txn()->set_inactivity_timeout(HRTIME_SECONDS(t_state.txn_conf->transaction_no_activity_timeout_in)); | |
| } | |
| // We only follow 3xx when redirect_in_process == false. Otherwise the redirection has already been launched (in | |
| // StateMachineAction_t::SERVER_READ).redirect_in_process is set before this logic if we need more direction. | |
| // This redirection is only used with the build_error_response. Then, the redirection_tries will be increased by | |
| // state_read_server_response_header and never get into this logic again. | |
| if (enable_redirection && !t_state.redirect_info.redirect_in_process && is_redirect_required()) { | |
| do_redirect(); | |
| } | |
| // we have further processing to do | |
| // based on what t_state.next_action is | |
| break; | |
| case HttpTransact::StateMachineAction_t::API_SM_SHUTDOWN: | |
| state_remove_from_list(EVENT_NONE, nullptr); | |
| return; | |
| default: | |
| ink_release_assert(!"Not reached"); | |
| break; | |
| } | |
| ATS_PROBE2(http_transfer_setup, sm_id, static_cast<int>(t_state.next_action)); | |
| switch (t_state.next_action) { | |
| case HttpTransact::StateMachineAction_t::TRANSFORM_READ: { | |
| HttpTunnelProducer *p = setup_transfer_from_transform(); | |
| perform_transform_cache_write_action(); | |
| tunnel.tunnel_run(p); | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::SERVER_READ: { | |
| milestones.mark(TS_MILESTONE_UA_BEGIN_WRITE); | |
| if (unlikely(t_state.did_upgrade_succeed)) { | |
| // We've successfully handled the upgrade, let's now setup | |
| // a blind tunnel. | |
| IOBufferReader *initial_data = nullptr; | |
| if (t_state.is_websocket) { | |
| Metrics::Gauge::increment(http_rsb.websocket_current_active_client_connections); | |
| if (server_txn) { | |
| initial_data = server_txn->get_remote_reader(); | |
| } | |
| if (_ua.get_txn()) { | |
| SMDbg(dbg_ctl_http_websocket, | |
| "(client session) Setting websocket active timeout=%" PRId64 "s and inactive timeout=%" PRId64 "s", | |
| t_state.txn_conf->websocket_active_timeout, t_state.txn_conf->websocket_inactive_timeout); | |
| _ua.get_txn()->set_active_timeout(HRTIME_SECONDS(t_state.txn_conf->websocket_active_timeout)); | |
| _ua.get_txn()->set_inactivity_timeout(HRTIME_SECONDS(t_state.txn_conf->websocket_inactive_timeout)); | |
| } | |
| if (server_txn) { | |
| SMDbg(dbg_ctl_http_websocket, | |
| "(server session) Setting websocket active timeout=%" PRId64 "s and inactive timeout=%" PRId64 "s", | |
| t_state.txn_conf->websocket_active_timeout, t_state.txn_conf->websocket_inactive_timeout); | |
| server_txn->set_active_timeout(HRTIME_SECONDS(t_state.txn_conf->websocket_active_timeout)); | |
| server_txn->set_inactivity_timeout(HRTIME_SECONDS(t_state.txn_conf->websocket_inactive_timeout)); | |
| } | |
| } | |
| setup_blind_tunnel(true, initial_data); | |
| } else { | |
| HttpTunnelProducer *p = setup_server_transfer(); | |
| perform_cache_write_action(); | |
| tunnel.tunnel_run(p); | |
| } | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::SERVE_FROM_CACHE: { | |
| milestones.mark(TS_MILESTONE_UA_BEGIN_WRITE); | |
| HttpTunnelProducer *p = setup_cache_read_transfer(); | |
| tunnel.tunnel_run(p); | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::INTERNAL_CACHE_WRITE: { | |
| if (cache_sm.cache_write_vc) { | |
| setup_internal_transfer(&HttpSM::tunnel_handler_cache_fill); | |
| } else { | |
| setup_internal_transfer(&HttpSM::tunnel_handler); | |
| } | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::INTERNAL_CACHE_NOOP: | |
| case HttpTransact::StateMachineAction_t::INTERNAL_CACHE_DELETE: | |
| case HttpTransact::StateMachineAction_t::INTERNAL_CACHE_UPDATE_HEADERS: | |
| case HttpTransact::StateMachineAction_t::SEND_ERROR_CACHE_NOOP: { | |
| // Consume any unforwarded request body, or close the connection when it cannot be | |
| // fully consumed, so leftover bytes are not framed as the next request. | |
| do_drain_request_body(t_state.hdr_info.client_response); | |
| setup_internal_transfer(&HttpSM::tunnel_handler); | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::REDIRECT_READ: { | |
| // Clean up from any communication with previous servers | |
| release_server_session(); | |
| call_transact_and_set_next_state(HttpTransact::HandleRequest); | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::SSL_TUNNEL: { | |
| setup_blind_tunnel(true); | |
| break; | |
| } | |
| default: { | |
| ink_release_assert(!"Should not get here"); | |
| } | |
| } | |
| } | |
| PoolableSession * | |
| HttpSM::create_server_session(NetVConnection &netvc, MIOBuffer *netvc_read_buffer, IOBufferReader *netvc_reader) | |
| { | |
| // Figure out what protocol was negotiated | |
| int proto_index = SessionProtocolNameRegistry::INVALID; | |
| if (auto const alpn = netvc.get_service<ALPNSupport>(); alpn) { | |
| proto_index = alpn->get_negotiated_protocol_id(); | |
| } | |
| // No ALPN occurred. Assume it was HTTP/1.x and hope for the best | |
| if (proto_index == SessionProtocolNameRegistry::INVALID) { | |
| proto_index = TS_ALPN_PROTOCOL_INDEX_HTTP_1_1; | |
| } | |
| PoolableSession *retval = ProxySession::create_outbound_session(proto_index); | |
| retval->sharing_pool = static_cast<TSServerSessionSharingPoolType>(t_state.http_config_param->server_session_sharing_pool); | |
| retval->sharing_match = static_cast<TSServerSessionSharingMatchMask>(t_state.txn_conf->server_session_sharing_match); | |
| retval->attach_hostname(t_state.current.server->name); | |
| retval->new_connection(&netvc, netvc_read_buffer, netvc_reader); | |
| ATS_PROBE1(new_origin_server_connection, t_state.current.server->name); | |
| retval->set_active(); | |
| ats_ip_copy(&t_state.server_info.src_addr, netvc.get_local_addr()); | |
| // If origin_max_connections or origin_min_keep_alive_connections is set then we are metering | |
| // the max and or min number of connections per host. Transfer responsibility for this to the | |
| // session object. | |
| if (t_state.outbound_conn_track_state.is_active()) { | |
| SMDbg(dbg_ctl_http_connect, "max number of outbound connections: %d", t_state.txn_conf->connection_tracker_config.server_max); | |
| retval->enable_outbound_connection_tracking(t_state.outbound_conn_track_state.drop()); | |
| } | |
| return retval; | |
| } | |
| bool | |
| HttpSM::create_server_txn(PoolableSession *new_session) | |
| { | |
| ink_assert(new_session != nullptr); | |
| bool retval = false; | |
| server_txn = new_session->new_transaction(); | |
| if (server_txn) { | |
| retval = true; | |
| server_txn->attach_transaction(this); | |
| if (t_state.current.request_to == ResolveInfo::PARENT_PROXY) { | |
| new_session->to_parent_proxy = true; | |
| if (server_txn->get_proxy_ssn()->get_transact_count() == 1) { | |
| // These are connection-level metrics, so only increment them for the | |
| // first transaction lest they get overcounted. | |
| Metrics::Gauge::increment(http_rsb.current_parent_proxy_connections); | |
| Metrics::Counter::increment(http_rsb.total_parent_proxy_connections); | |
| } | |
| } else { | |
| new_session->to_parent_proxy = false; | |
| } | |
| server_txn->do_io_write(this, 0, nullptr); | |
| attach_server_session(); | |
| } | |
| _netvc = nullptr; | |
| _netvc_read_buffer = nullptr; | |
| _netvc_reader = nullptr; | |
| return retval; | |
| } | |
| ////////////////////////////////////////////////////////////////////////////// | |
| // | |
| // HttpSM::state_http_server_open() | |
| // | |
| ////////////////////////////////////////////////////////////////////////////// | |
| int | |
| HttpSM::state_http_server_open(int event, void *data) | |
| { | |
| SMDbg(dbg_ctl_http_track, "entered inside state_http_server_open: %s", HttpDebugNames::get_event_name(event)); | |
| STATE_ENTER(state_http_server_open, event); | |
| if (event == CONNECT_EVENT_RETRY) { | |
| pending_action.clear_if_action_is(reinterpret_cast<Action *>(data)); | |
| } | |
| ink_release_assert(event == EVENT_INTERVAL || event == NET_EVENT_OPEN || event == NET_EVENT_OPEN_FAILED || | |
| pending_action.empty()); | |
| if (event != NET_EVENT_OPEN) { | |
| pending_action = nullptr; | |
| } | |
| ATS_PROBE1(milestone_server_connect_end, sm_id); | |
| milestones[TS_MILESTONE_SERVER_CONNECT_END] = ink_get_hrtime(); | |
| switch (event) { | |
| case NET_EVENT_OPEN: { | |
| // Since the UnixNetVConnection::action_ or SocksEntry::action_ may be returned from netProcessor.connect_re, and the | |
| // SocksEntry::action_ will be copied into UnixNetVConnection::action_ before call back NET_EVENT_OPEN from | |
| // SocksEntry::free(), so we just compare the Continuation between pending_action and VC's action_. | |
| _netvc = static_cast<NetVConnection *>(data); | |
| _netvc_read_buffer = new_MIOBuffer(HTTP_SERVER_RESP_HDR_BUFFER_INDEX); | |
| _netvc_reader = _netvc_read_buffer->alloc_reader(); | |
| UnixNetVConnection *vc = static_cast<UnixNetVConnection *>(_netvc); | |
| ink_release_assert(pending_action.empty() || pending_action.get_continuation() == vc->get_action()->continuation); | |
| pending_action = nullptr; | |
| if (this->plugin_tunnel_type == HttpPluginTunnel_t::NONE) { | |
| SMDbg(dbg_ctl_http_connect, "setting handler for connection handshake timeout %" PRId64, this->get_server_connect_timeout()); | |
| // Just want to get a write-ready event so we know that the connection handshake is complete. | |
| // The buffer we create will be handed over to the eventually created server session | |
| _netvc->do_io_write(this, 1, _netvc_reader); | |
| _netvc->set_inactivity_timeout(this->get_server_connect_timeout()); | |
| // Pre-emptively set a server connect failure that will be cleared once a WRITE_READY is received from origin or | |
| // bytes are received back | |
| t_state.set_connect_fail(EIO); | |
| } else { // in the case of an intercept plugin don't to the connect timeout change | |
| SMDbg(dbg_ctl_http_connect, "not setting handler for connection handshake"); | |
| this->create_server_txn(this->create_server_session(*_netvc, _netvc_read_buffer, _netvc_reader)); | |
| handle_http_server_open(); | |
| } | |
| ink_assert(pending_action.empty()); | |
| return 0; | |
| } | |
| case CONNECT_EVENT_DIRECT: | |
| // Try it again, but direct this time | |
| do_http_server_open(false, true); | |
| break; | |
| case CONNECT_EVENT_RETRY: | |
| do_http_server_open(); | |
| break; | |
| case CONNECT_EVENT_TXN: { | |
| // Multiplexed connection path (H2/H3): ConnectingEntry owns the connection and | |
| // dispatches this event with the PoolableSession once the handshake completes. | |
| // The HttpSM has no access to the netvc during the handshake, so we copy the | |
| // timestamps that were recorded in real-time by sslClientHandShakeEvent(). | |
| // See also VC_EVENT_WRITE_COMPLETE below for the direct (H1) path -- these | |
| // two paths are mutually exclusive. | |
| SMDbg(dbg_ctl_http, "Connection handshake complete via CONNECT_EVENT_TXN"); | |
| PoolableSession *session = static_cast<PoolableSession *>(data); | |
| ink_assert(session != nullptr); | |
| if (auto *netvc = session->get_netvc()) { | |
| if (auto tbs = netvc->get_service<TLSBasicSupport>()) { | |
| milestones[TS_MILESTONE_SERVER_TLS_HANDSHAKE_START] = tbs->get_tls_handshake_begin_time(); | |
| milestones[TS_MILESTONE_SERVER_TLS_HANDSHAKE_END] = tbs->get_tls_handshake_end_time(); | |
| } | |
| } | |
| if (this->create_server_txn(session)) { | |
| t_state.current.server->clear_connect_fail(); | |
| handle_http_server_open(); | |
| } else { // Failed to create transaction. Maybe too many active transactions already | |
| // Try again (probably need a bounding counter here) | |
| do_http_server_open(false); | |
| } | |
| return 0; | |
| } | |
| case VC_EVENT_READ_COMPLETE: | |
| case VC_EVENT_WRITE_READY: | |
| case VC_EVENT_WRITE_COMPLETE: | |
| // Direct connection path (H1): HttpSM owns the netvc directly. | |
| // Same milestone copy as CONNECT_EVENT_TXN above -- these two paths are | |
| // mutually exclusive. Timestamps were recorded in real-time by | |
| // sslClientHandShakeEvent(); we copy them into milestones here. | |
| SMDbg(dbg_ctl_http_ss, "Connection handshake complete"); | |
| if (auto tbs = _netvc->get_service<TLSBasicSupport>()) { | |
| milestones[TS_MILESTONE_SERVER_TLS_HANDSHAKE_START] = tbs->get_tls_handshake_begin_time(); | |
| milestones[TS_MILESTONE_SERVER_TLS_HANDSHAKE_END] = tbs->get_tls_handshake_end_time(); | |
| } | |
| this->create_server_txn(this->create_server_session(*_netvc, _netvc_read_buffer, _netvc_reader)); | |
| t_state.current.server->clear_connect_fail(); | |
| handle_http_server_open(); | |
| return 0; | |
| case VC_EVENT_INACTIVITY_TIMEOUT: | |
| case VC_EVENT_ACTIVE_TIMEOUT: | |
| t_state.set_connect_fail(ETIMEDOUT); | |
| /* fallthrough */ | |
| case VC_EVENT_ERROR: | |
| case VC_EVENT_EOS: | |
| case NET_EVENT_OPEN_FAILED: { | |
| t_state.current.state = HttpTransact::CONNECTION_ERROR; | |
| t_state.outbound_conn_track_state.clear(); | |
| if (event == NET_EVENT_OPEN_FAILED && data != nullptr) { | |
| int const lerrno = -static_cast<int>(reinterpret_cast<intptr_t>(data)); | |
| t_state.current.server->connect_result = lerrno; | |
| t_state.cause_of_death_errno = lerrno; | |
| } | |
| if (_netvc != nullptr) { | |
| if (event == VC_EVENT_ERROR || event == NET_EVENT_OPEN_FAILED) { | |
| t_state.set_connect_fail(_netvc->lerrno); | |
| } | |
| this->server_connection_provided_cert = _netvc->provided_cert(); | |
| _netvc->do_io_write(nullptr, 0, nullptr); | |
| _netvc->do_io_close(); | |
| _netvc = nullptr; | |
| } | |
| if (t_state.cause_of_death_errno == -UNKNOWN_INTERNAL_ERROR) { | |
| // We set this to 0 because otherwise | |
| // HttpTransact::retry_server_connection_not_open will raise an assertion | |
| // if the value is the default UNKNOWN_INTERNAL_ERROR. | |
| t_state.cause_of_death_errno = 0; | |
| } | |
| /* If we get this error in transparent mode, then we simply can't bind to the 4-tuple to make the connection. There's no hope | |
| of retries succeeding in the near future. The best option is to just shut down the connection without further comment. The | |
| only known cause for this is outbound transparency combined with use client target address / source port, as noted in | |
| TS-1424. If the keep alives desync the current connection can be attempting to rebind the 4 tuple simultaneously with the | |
| shut down of an existing connection. Dropping the client side will cause it to pick a new source port and recover from this | |
| issue. | |
| */ | |
| if (EADDRNOTAVAIL == t_state.current.server->connect_result && t_state.client_info.is_transparent) { | |
| if (dbg_ctl_http_tproxy.on()) { | |
| ip_port_text_buffer ip_c, ip_s; | |
| SMDbg(dbg_ctl_http_tproxy, "Force close of client connect (%s->%s) due to EADDRNOTAVAIL", | |
| ats_ip_nptop(&t_state.client_info.src_addr.sa, ip_c, sizeof ip_c), | |
| ats_ip_nptop(&t_state.server_info.dst_addr.sa, ip_s, sizeof ip_s)); | |
| } | |
| t_state.client_info.keep_alive = HTTPKeepAlive::NO_KEEPALIVE; // part of the problem, clear it. | |
| terminate_sm = true; | |
| } else if (ENET_THROTTLING == t_state.current.server->connect_result) { | |
| Metrics::Counter::increment(http_rsb.origin_connections_throttled); | |
| send_origin_throttled_response(); | |
| } else { | |
| // Go ahead and release the failed server session. Since it didn't receive a response, the release logic will | |
| // see that it didn't get a valid response and it will close it rather than returning it to the server session pool | |
| release_server_session(); | |
| call_transact_and_set_next_state(HttpTransact::HandleResponse); | |
| } | |
| return 0; | |
| } | |
| case EVENT_INTERVAL: // Delayed call from another thread | |
| if (server_txn == nullptr) { | |
| do_http_server_open(); | |
| } | |
| break; | |
| default: | |
| Error("[HttpSM::state_http_server_open] Unknown event: %d", event); | |
| ink_release_assert(0); | |
| return 0; | |
| } | |
| return 0; | |
| } | |
| int | |
| HttpSM::state_read_server_response_header(int event, void *data) | |
| { | |
| STATE_ENTER(state_read_server_response_header, event); | |
| // If we had already received EOS, just go away. We would sometimes see | |
| // a WRITE event appear after receiving EOS from the server connection | |
| if (server_entry->eos) { | |
| return 0; | |
| } | |
| ink_assert(server_entry->eos != true); | |
| ink_assert(server_entry->read_vio == (VIO *)data); | |
| ink_assert(t_state.current.server->state == HttpTransact::STATE_UNDEFINED); | |
| ink_assert(t_state.current.state == HttpTransact::STATE_UNDEFINED); | |
| int bytes_used = 0; | |
| switch (event) { | |
| case VC_EVENT_EOS: | |
| log_server_close_with_origin(t_state, "Server closed connection while reading response header."); | |
| server_entry->eos = true; | |
| // If we have received any bytes for this transaction do not retry | |
| if (server_response_hdr_bytes > 0) { | |
| t_state.current.retry_attempts.maximize(t_state.configured_connect_attempts_max_retries()); | |
| } | |
| break; | |
| case VC_EVENT_READ_READY: | |
| case VC_EVENT_READ_COMPLETE: | |
| // More data to parse | |
| break; | |
| case VC_EVENT_ERROR: | |
| case VC_EVENT_INACTIVITY_TIMEOUT: | |
| case VC_EVENT_ACTIVE_TIMEOUT: | |
| // Error handling function | |
| handle_server_setup_error(event, data); | |
| return 0; | |
| } | |
| // Reset the inactivity timeout if this is the first | |
| // time we've been called. The timeout had been set to | |
| // the connect timeout when we set up to read the header | |
| // | |
| if (server_response_hdr_bytes == 0) { | |
| ATS_PROBE1(milestone_server_first_read, sm_id); | |
| milestones[TS_MILESTONE_SERVER_FIRST_READ] = ink_get_hrtime(); | |
| server_txn->set_inactivity_timeout(get_server_inactivity_timeout()); | |
| // For requests that contain a body, we can cancel the ua inactivity timeout. | |
| if (_ua.get_txn() && | |
| _ua.get_txn()->has_request_body(t_state.hdr_info.request_content_length, | |
| t_state.client_info.transfer_encoding == HttpTransact::TransferEncoding_t::CHUNKED)) { | |
| _ua.get_txn()->cancel_inactivity_timeout(); | |
| } | |
| } | |
| ///////////////////// | |
| // tokenize header // | |
| ///////////////////// | |
| ParseResult state = | |
| t_state.hdr_info.server_response.parse_resp(&http_parser, server_txn->get_remote_reader(), &bytes_used, server_entry->eos); | |
| server_response_hdr_bytes += bytes_used; | |
| // Don't allow HTTP 0.9 (unparsable headers) on resued connections. | |
| // And don't allow empty headers from closed connections | |
| if ((state == ParseResult::DONE && t_state.hdr_info.server_response.version_get() == HTTP_0_9 && | |
| server_txn->get_transaction_id() > 1) || | |
| (server_entry->eos && state == ParseResult::CONT)) { // No more data will be coming | |
| state = ParseResult::ERROR; | |
| } | |
| // Check to see if we are over the hdr size limit | |
| if (server_response_hdr_bytes > t_state.txn_conf->response_hdr_max_size) { | |
| state = ParseResult::ERROR; | |
| } | |
| if (state != ParseResult::CONT) { | |
| // Disable further IO | |
| server_entry->read_vio->nbytes = server_entry->read_vio->ndone; | |
| http_parser_clear(&http_parser); | |
| ATS_PROBE1(milestone_server_read_header_done, sm_id); | |
| milestones[TS_MILESTONE_SERVER_READ_HEADER_DONE] = ink_get_hrtime(); | |
| // Any other events to the end | |
| if (server_entry->vc_type == HttpVC_t::SERVER_VC) { | |
| server_entry->vc_read_handler = &HttpSM::tunnel_handler; | |
| server_entry->vc_write_handler = &HttpSM::tunnel_handler; | |
| } | |
| // If there is a post body in transit, give up on it | |
| if (tunnel.is_tunnel_alive()) { | |
| tunnel.abort_tunnel(); | |
| // abort_tunnel() cancels I/O but does not close VCs or clean up | |
| // vc_table entries. When a request transform is active the | |
| // post_transform_info entry still references the TransformVConnection | |
| // with in_tunnel=true, which causes cleanup_entry() to skip | |
| // do_io_close() — leaking the VC. Close it explicitly here. | |
| if (post_transform_info.entry != nullptr) { | |
| post_transform_info.entry->vc->do_io_close(); | |
| vc_table.cleanup_entry(post_transform_info.entry); | |
| post_transform_info.entry = nullptr; | |
| // .vc stays non-null so transform_cleanup() skips the already-closed chain. | |
| // tunnel_handler_post_or_put() is unreachable after | |
| // abort_tunnel() since the tunnel is dead and reset. | |
| } | |
| // Make sure client connection is closed when we are done in case there is cruft left over | |
| t_state.client_info.keep_alive = HTTPKeepAlive::NO_KEEPALIVE; | |
| // Similarly the server connection should also be closed | |
| t_state.current.server->keep_alive = HTTPKeepAlive::NO_KEEPALIVE; | |
| } | |
| } | |
| switch (state) { | |
| case ParseResult::ERROR: | |
| SMDbg(dbg_ctl_http_seq, "Error parsing server response header"); | |
| t_state.current.state = HttpTransact::PARSE_ERROR; | |
| t_state.set_connect_fail(EBADMSG); | |
| // If the server closed prematurely on us, use the | |
| // server setup error routine since it will forward | |
| // error to a POST tunnel if any | |
| if (event == VC_EVENT_EOS) { | |
| handle_server_setup_error(VC_EVENT_EOS, data); | |
| } else { | |
| call_transact_and_set_next_state(HttpTransact::HandleResponse); | |
| } | |
| break; | |
| case ParseResult::DONE: { | |
| if (!t_state.hdr_info.server_response.check_hdr_implements()) { | |
| t_state.http_return_code = HTTPStatus::BAD_GATEWAY; | |
| call_transact_and_set_next_state(HttpTransact::BadRequest); | |
| break; | |
| } | |
| // Recompute cooked cache control with targeted headers. | |
| t_state.hdr_info.server_response.m_mime->recompute_cooked_stuff(nullptr, | |
| t_state.txn_conf->targeted_cache_control_headers.get_headers(), | |
| t_state.txn_conf->targeted_cache_control_headers.get_count()); | |
| SMDbg(dbg_ctl_http_seq, "Done parsing server response header"); | |
| // Now that we know that we have all of the origin server | |
| // response headers, we can reset the client inactivity | |
| // timeout. | |
| // we now reset the client inactivity timeout only | |
| // when we are ready to send the response headers. In the | |
| // case of transform plugin, this is after the transform | |
| // outputs the 1st byte, which can take a long time if the | |
| // plugin buffers the whole response. | |
| _ua.get_txn()->set_inactivity_timeout(HRTIME_SECONDS(t_state.txn_conf->transaction_no_activity_timeout_in)); | |
| t_state.current.state = HttpTransact::CONNECTION_ALIVE; | |
| t_state.transact_return_point = HttpTransact::HandleResponse; | |
| HttpTransact::strip_at_headers(t_state.hdr_info.server_response, HttpTransact::AtHeaderSource::ORIGIN_RESPONSE, sm_id); | |
| t_state.api_next_action = HttpTransact::StateMachineAction_t::API_READ_RESPONSE_HDR; | |
| // if exceeded limit deallocate postdata buffers and disable redirection | |
| if (!(enable_redirection && (redirection_tries < t_state.txn_conf->number_of_redirections))) { | |
| this->disable_redirect(); | |
| } | |
| // Go ahead and process the hooks assuming any body tunnel has already completed | |
| if (!tunnel.is_tunnel_alive()) { | |
| SMDbg(dbg_ctl_http_seq, "Continue processing response"); | |
| do_api_callout(); | |
| } else { | |
| SMDbg(dbg_ctl_http_seq, "Defer processing response until post body is processed"); | |
| server_entry->read_vio->disable(); // Disable the read until we finish the tunnel | |
| } | |
| break; | |
| } | |
| case ParseResult::CONT: | |
| ink_assert(server_entry->eos == false); | |
| server_entry->read_vio->reenable(); | |
| return VC_EVENT_CONT; | |
| default: | |
| ink_assert(!"not reached"); | |
| } | |
| return 0; | |
| } | |
| int | |
| HttpSM::state_send_server_request_header(int event, void *data) | |
| { | |
| ink_assert(server_entry != nullptr); | |
| ink_assert(server_entry->eos == false); | |
| ink_assert(server_entry->write_vio == (VIO *)data); | |
| STATE_ENTER(state_send_server_request_header, event); | |
| int method; | |
| switch (event) { | |
| case VC_EVENT_WRITE_READY: | |
| server_entry->write_vio->reenable(); | |
| break; | |
| case VC_EVENT_WRITE_COMPLETE: | |
| // We are done sending the request header, deallocate | |
| // our buffer and then decide what to do next | |
| if (server_entry->write_buffer) { | |
| free_MIOBuffer(server_entry->write_buffer); | |
| server_entry->write_buffer = nullptr; | |
| method = t_state.hdr_info.server_request.method_get_wksidx(); | |
| if (!t_state.api_server_request_body_set && method != HTTP_WKSIDX_TRACE && | |
| _ua.get_txn()->has_request_body(t_state.hdr_info.request_content_length, | |
| t_state.client_info.transfer_encoding == HttpTransact::TransferEncoding_t::CHUNKED)) { | |
| // The origin has now been told to expect a body. Record that it is owed those | |
| // bytes, whether or not a tunnel to deliver them is started below. When the | |
| // client asked for a 100 Continue the body is withheld until the origin sends | |
| // one, so there is no tunnel to infer this from. | |
| server_owed_request_body = true; | |
| if (post_transform_info.vc) { | |
| setup_transform_to_server_transfer(); | |
| } else { | |
| // Go ahead and set up the post tunnel if we are not waiting for a 100 response | |
| if (!t_state.hdr_info.client_request.m_100_continue_required) { | |
| do_setup_client_request_body_tunnel(HttpVC_t::SERVER_VC); | |
| } | |
| } | |
| } | |
| // The request body tunnel setup can synchronously abort the chain, which | |
| // cleans up server_entry before returning here. | |
| if (server_entry == nullptr) { | |
| break; | |
| } | |
| // Any other events to these read response | |
| if (server_entry->vc_type == HttpVC_t::SERVER_VC) { | |
| server_entry->vc_read_handler = &HttpSM::state_read_server_response_header; | |
| } | |
| } | |
| break; | |
| case VC_EVENT_EOS: | |
| // EOS of stream comes from the read side. Treat it as | |
| // as error if there is nothing in the read buffer. If | |
| // there is something the server may have blasted back | |
| // the response before receiving the request. Happens | |
| // often with redirects | |
| // | |
| // If we are in the middle of an api callout, it | |
| // means we haven't actually sent the request yet | |
| // so the stuff in the buffer is garbage and we | |
| // want to ignore it | |
| // | |
| server_entry->eos = true; | |
| // I'm not sure about the above comment, but if EOS is received on read and we are | |
| // still in this state, we must have not gotten WRITE_COMPLETE. With epoll we might not receive EOS | |
| // from both read and write sides of a connection so it should be handled correctly (close tunnels, | |
| // deallocate, etc) here with handle_server_setup_error(). Otherwise we might hang due to not shutting | |
| // down and never receiving another event again. | |
| /*if (server_txn->get_remote_reader()->read_avail() > 0 && callout_state == HttpApiState_t::NO_CALLOUT) { | |
| break; | |
| } */ | |
| // Nothing in the buffer | |
| // proceed to error | |
| // fallthrough | |
| case VC_EVENT_ERROR: | |
| case VC_EVENT_ACTIVE_TIMEOUT: | |
| case VC_EVENT_INACTIVITY_TIMEOUT: | |
| handle_server_setup_error(event, data); | |
| break; | |
| case VC_EVENT_READ_COMPLETE: | |
| // new event expected due to TS-3189 | |
| SMDbg(dbg_ctl_http_ss, "read complete due to 0 byte do_io_read"); | |
| break; | |
| default: | |
| ink_release_assert(0); | |
| break; | |
| } | |
| return 0; | |
| } | |
| bool | |
| HttpSM::origin_multiplexed() const | |
| { | |
| return (t_state.dns_info.http_version == HTTP_2_0 || t_state.dns_info.http_version == HTTP_INVALID); | |
| } | |
| void | |
| HttpSM::cancel_pending_server_connection() | |
| { | |
| EThread *ethread = this_ethread(); | |
| if (nullptr == ethread->connecting_pool || !t_state.current.server) { | |
| return; // No pending requests | |
| } | |
| IpEndpoint ip; | |
| ip.assign(&this->t_state.current.server->dst_addr.sa); | |
| auto [iter_start, iter_end] = ethread->connecting_pool->m_ip_pool.equal_range(ip); | |
| for (auto ip_iter = iter_start; ip_iter != iter_end; ++ip_iter) { | |
| ConnectingEntry *connecting_entry = ip_iter->second; | |
| // Found a match, look for our sm in the queue. | |
| auto entry = connecting_entry->connect_sms.find(this); | |
| if (entry != connecting_entry->connect_sms.end()) { | |
| // Found the sm, remove it. | |
| connecting_entry->connect_sms.erase(entry); | |
| if (connecting_entry->connect_sms.empty()) { | |
| if (connecting_entry->netvc) { | |
| connecting_entry->netvc->do_io_write(nullptr, 0, nullptr); | |
| connecting_entry->netvc->do_io_close(); | |
| } | |
| ethread->connecting_pool->m_ip_pool.erase(ip_iter); | |
| delete connecting_entry; | |
| break; | |
| } else { | |
| // Leave the shared entry remaining alone | |
| } | |
| } | |
| } | |
| } | |
| // Returns true if there was a matching entry that we | |
| // queued this request on | |
| bool | |
| HttpSM::add_to_existing_request() | |
| { | |
| HttpTransact::State &s = this->t_state; | |
| bool retval = false; | |
| EThread *ethread = this_ethread(); | |
| if (this->plugin_tunnel_type != HttpPluginTunnel_t::NONE) { | |
| return false; | |
| } | |
| if (nullptr == ethread->connecting_pool) { | |
| initialize_thread_for_connecting_pools(ethread); | |
| } | |
| NetVConnection *vc = _ua.get_txn()->get_netvc(); | |
| ink_assert(dynamic_cast<UnixNetVConnection *>(vc) == nullptr /* PluginVC */ || | |
| dynamic_cast<UnixNetVConnection *>(vc)->nh == get_NetHandler(this_ethread())); | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::state_http_server_open); | |
| IpEndpoint ip; | |
| ip.assign(&s.current.server->dst_addr.sa); | |
| std::string_view proposed_sni = this->get_outbound_sni(); | |
| std::string_view proposed_cert = this->get_outbound_cert(); | |
| std::string_view proposed_hostname = this->t_state.current.server->name; | |
| auto [iter_start, iter_end] = ethread->connecting_pool->m_ip_pool.equal_range(ip); | |
| for (auto ip_iter = iter_start; ip_iter != iter_end; ++ip_iter) { | |
| // Check that entry matches sni, hostname, and cert. | |
| if (proposed_hostname == ip_iter->second->hostname && proposed_sni == ip_iter->second->sni && | |
| proposed_cert == ip_iter->second->cert_name && ip_iter->second->connect_sms.size() < 50) { | |
| // Pre-emptively set a server connect failure that will be cleared once a | |
| // WRITE_READY is received from origin or bytes are received back. | |
| this->t_state.set_connect_fail(EIO); | |
| ip_iter->second->connect_sms.insert(this); | |
| Dbg(dbg_ctl_http_connect, "Add entry to connection queue. size=%zd", ip_iter->second->connect_sms.size()); | |
| retval = true; | |
| break; | |
| } | |
| } | |
| return retval; | |
| } | |
| void | |
| HttpSM::process_srv_info(HostDBRecord *record) | |
| { | |
| SMDbg(dbg_ctl_dns_srv, "beginning process_srv_info"); | |
| t_state.dns_info.record = record; | |
| /* we didn't get any SRV records, continue w normal lookup */ | |
| if (!record || !record->is_srv()) { | |
| t_state.dns_info.srv_hostname[0] = '\0'; | |
| t_state.dns_info.resolved_p = false; | |
| t_state.my_txn_conf().srv_enabled = false; | |
| SMDbg(dbg_ctl_dns_srv, "No SRV records were available, continuing to lookup %s", t_state.dns_info.lookup_name); | |
| } else { | |
| HostDBInfo *srv = record->select_best_srv(t_state.dns_info.srv_hostname, &mutex->thread_holding->generator, ts_clock::now(), | |
| t_state.txn_conf->down_server_timeout); | |
| if (!srv) { | |
| // t_state.dns_info.srv_lookup_success = false; | |
| t_state.dns_info.srv_hostname[0] = '\0'; | |
| t_state.my_txn_conf().srv_enabled = false; | |
| SMDbg(dbg_ctl_dns_srv, "SRV records empty for %s", t_state.dns_info.lookup_name); | |
| } else { | |
| t_state.dns_info.resolved_p = false; | |
| t_state.dns_info.srv_port = srv->data.srv.srv_port; | |
| ink_assert(srv->data.srv.key == makeHostHash(t_state.dns_info.srv_hostname)); | |
| SMDbg(dbg_ctl_dns_srv, "select SRV records %s", t_state.dns_info.srv_hostname); | |
| } | |
| } | |
| return; | |
| } | |
| void | |
| HttpSM::process_hostdb_info(HostDBRecord *record) | |
| { | |
| t_state.dns_info.record = record; // protect record. | |
| bool use_client_addr = t_state.http_config_param->use_client_target_addr == 1 && t_state.client_info.is_transparent && | |
| t_state.dns_info.os_addr_style == ResolveInfo::OS_Addr::TRY_DEFAULT; | |
| t_state.dns_info.set_active(nullptr); | |
| if (use_client_addr) { | |
| NetVConnection *vc = _ua.get_txn() ? _ua.get_txn()->get_netvc() : nullptr; | |
| if (vc) { | |
| t_state.dns_info.set_upstream_address(vc->get_local_addr()); | |
| t_state.dns_info.os_addr_style = ResolveInfo::OS_Addr::TRY_CLIENT; | |
| } | |
| } | |
| if (record && !record->is_failed()) { | |
| // HostDB parent-selection hashes by inbound_remote_addr; using | |
| // effective_client_addr keeps sharding affinity tied to the real client | |
| // when :pp-clnt is in effect, and is the TCP peer otherwise. | |
| t_state.dns_info.inbound_remote_addr = &t_state.effective_client_addr.sa; | |
| if (!use_client_addr) { | |
| t_state.dns_info.set_active( | |
| record->select_best_http(ts_clock::now(), t_state.txn_conf->down_server_timeout, t_state.dns_info.inbound_remote_addr)); | |
| } else { | |
| // if use_client_target_addr is set, make sure the client addr is in the results pool | |
| t_state.dns_info.cta_validated_p = true; | |
| t_state.dns_info.record = record; // Cache this but do not make it active. | |
| if (record->find(t_state.dns_info.addr) == nullptr) { | |
| SMDbg(dbg_ctl_http, "use_client_target_addr == 1. Client specified address is not in the pool, not validated."); | |
| t_state.dns_info.cta_validated_p = false; | |
| } | |
| } | |
| } else { | |
| SMDbg(dbg_ctl_http, "DNS lookup failed for '%s'", t_state.dns_info.lookup_name); | |
| } | |
| if (!t_state.dns_info.resolved_p) { | |
| SMDbg(dbg_ctl_http, "[%" PRId64 "] resolution failed for '%s'", sm_id, t_state.dns_info.lookup_name); | |
| } | |
| ATS_PROBE1(milestone_dns_lookup_end, sm_id); | |
| milestones[TS_MILESTONE_DNS_LOOKUP_END] = ink_get_hrtime(); | |
| if (dbg_ctl_http_timeout.on()) { | |
| if (t_state.api_txn_dns_timeout_value != -1) { | |
| int foo = static_cast<int>(milestones.difference_msec(TS_MILESTONE_DNS_LOOKUP_BEGIN, TS_MILESTONE_DNS_LOOKUP_END)); | |
| SMDbg(dbg_ctl_http_timeout, "DNS took: %d msec", foo); | |
| } | |
| } | |
| } | |
| ////////////////////////////////////////////////////////////////////////////// | |
| // | |
| // HttpSM::state_hostdb_lookup() | |
| // | |
| ////////////////////////////////////////////////////////////////////////////// | |
| int | |
| HttpSM::state_hostdb_lookup(int event, void *data) | |
| { | |
| STATE_ENTER(state_hostdb_lookup, event); | |
| switch (event) { | |
| case EVENT_HOST_DB_LOOKUP: | |
| pending_action = nullptr; | |
| process_hostdb_info(static_cast<HostDBRecord *>(data)); | |
| call_transact_and_set_next_state(nullptr); | |
| break; | |
| case EVENT_SRV_LOOKUP: { | |
| pending_action = nullptr; | |
| process_srv_info(static_cast<HostDBRecord *>(data)); | |
| char const *host_name = t_state.dns_info.is_srv() ? t_state.dns_info.srv_hostname : t_state.dns_info.lookup_name; | |
| HostDBProcessor::Options opt; | |
| opt.port = t_state.dns_info.is_srv() ? t_state.dns_info.srv_port : t_state.server_info.dst_addr.host_order_port(); | |
| opt.flags = (t_state.cache_info.directives.does_client_permit_dns_storing) ? HostDBProcessor::HOSTDB_DO_NOT_FORCE_DNS : | |
| HostDBProcessor::HOSTDB_FORCE_DNS_RELOAD; | |
| opt.timeout = (t_state.api_txn_dns_timeout_value != -1) ? t_state.api_txn_dns_timeout_value : 0; | |
| opt.host_res_style = | |
| ats_host_res_from(_ua.get_txn()->get_netvc()->get_local_addr()->sa_family, t_state.txn_conf->host_res_data.order); | |
| pending_action = hostDBProcessor.getbyname_imm(this, (cb_process_result_pfn)&HttpSM::process_hostdb_info, host_name, 0, opt); | |
| if (pending_action.empty()) { | |
| call_transact_and_set_next_state(nullptr); | |
| } | |
| } break; | |
| case EVENT_HOST_DB_IP_REMOVED: | |
| ink_assert(!"Unexpected event from HostDB"); | |
| break; | |
| default: | |
| ink_assert(!"Unexpected event"); | |
| } | |
| return 0; | |
| } | |
| int | |
| HttpSM::state_hostdb_reverse_lookup(int event, void *data) | |
| { | |
| STATE_ENTER(state_hostdb_reverse_lookup, event); | |
| // HttpRequestFlavor_t::SCHEDULED_UPDATE can be transformed into | |
| // HttpRequestFlavor_t::REVPROXY | |
| ink_assert(t_state.req_flavor == HttpTransact::HttpRequestFlavor_t::SCHEDULED_UPDATE || | |
| t_state.req_flavor == HttpTransact::HttpRequestFlavor_t::REVPROXY || _ua.get_entry()->vc != nullptr); | |
| switch (event) { | |
| case EVENT_HOST_DB_LOOKUP: | |
| pending_action = nullptr; | |
| if (data) { | |
| t_state.request_data.hostname_str = (static_cast<HostDBRecord *>(data))->name(); | |
| } else { | |
| SMDbg(dbg_ctl_http, "reverse DNS lookup failed for '%s'", t_state.dns_info.lookup_name); | |
| } | |
| call_transact_and_set_next_state(nullptr); | |
| break; | |
| default: | |
| ink_assert(!"Unexpected event"); | |
| } | |
| return 0; | |
| } | |
| ///////////////////////////////////////////////////////////////////////////////// | |
| // HttpSM::state_cache_open_write() | |
| // | |
| // This state is set by set_next_state() for a cache open write | |
| // (SERVER_READ_CACHE_WRITE) | |
| // | |
| ////////////////////////////////////////////////////////////////////////// | |
| int | |
| HttpSM::state_cache_open_write(int event, void *data) | |
| { | |
| STATE_ENTER(state_cache_open_write, event); | |
| // The cache action has already delivered this callback, so drop it before any | |
| // thread adjustment below can assign over it. Assigning to pending_action | |
| // cancels whatever it holds, and canceling the cache SM's reusable captive | |
| // action here would break every cache operation this transaction makes later. | |
| pending_action.clear_if_action_is(reinterpret_cast<Action *>(data)); | |
| // Make sure we are on the "right" thread | |
| if (_ua.get_txn()) { | |
| pending_action = _ua.get_txn()->adjust_thread(this, event, data); | |
| if (!pending_action.empty()) { | |
| Metrics::Counter::increment(http_rsb.cache_open_write_adjust_thread); | |
| return 0; // Go away if we reschedule | |
| } | |
| NetVConnection *vc = _ua.get_txn()->get_netvc(); | |
| ink_release_assert(vc && vc->thread == this_ethread()); | |
| } | |
| ATS_PROBE1(milestone_cache_open_write_end, sm_id); | |
| milestones[TS_MILESTONE_CACHE_OPEN_WRITE_END] = ink_get_hrtime(); | |
| pending_action = nullptr; | |
| switch (event) { | |
| case CACHE_EVENT_OPEN_WRITE: | |
| ////////////////////////////// | |
| // OPEN WRITE is successful // | |
| ////////////////////////////// | |
| t_state.cache_info.write_lock_state = HttpTransact::CacheWriteLock_t::SUCCESS; | |
| break; | |
| case CACHE_EVENT_OPEN_WRITE_FAILED: | |
| // Failed on the write lock and retrying the vector | |
| // for reading | |
| if (t_state.redirect_info.redirect_in_process) { | |
| SMDbg(dbg_ctl_http_redirect, "CACHE_EVENT_OPEN_WRITE_FAILED during redirect follow"); | |
| t_state.cache_open_write_fail_action = static_cast<MgmtByte>(CacheOpenWriteFailAction_t::DEFAULT); | |
| t_state.cache_info.write_lock_state = HttpTransact::CacheWriteLock_t::FAIL; | |
| break; | |
| } | |
| if (t_state.txn_conf->cache_open_write_fail_action == static_cast<MgmtByte>(CacheOpenWriteFailAction_t::DEFAULT)) { | |
| t_state.cache_info.write_lock_state = HttpTransact::CacheWriteLock_t::FAIL; | |
| break; | |
| } else { | |
| t_state.cache_open_write_fail_action = t_state.txn_conf->cache_open_write_fail_action; | |
| if (!t_state.cache_info.object_read || (t_state.cache_open_write_fail_action == | |
| static_cast<MgmtByte>(CacheOpenWriteFailAction_t::ERROR_ON_MISS_OR_REVALIDATE))) { | |
| // cache miss, set wl_state to fail | |
| SMDbg(dbg_ctl_http, "cache object read %p, cache_wl_fail_action %d", t_state.cache_info.object_read, | |
| t_state.cache_open_write_fail_action); | |
| t_state.cache_info.write_lock_state = HttpTransact::CacheWriteLock_t::FAIL; | |
| break; | |
| } | |
| } | |
| // INTENTIONAL FALL THROUGH | |
| // Allow for stale object to be served | |
| case CACHE_EVENT_OPEN_READ: | |
| if (!t_state.cache_info.object_read) { | |
| t_state.cache_open_write_fail_action = t_state.txn_conf->cache_open_write_fail_action; | |
| // READ_RETRY mode: write lock failed, no stale object available. | |
| // CACHE_LOOKUP_COMPLETE will fire from HandleCacheOpenReadMiss with MISS result. | |
| ink_assert(t_state.cache_open_write_fail_action == static_cast<MgmtByte>(CacheOpenWriteFailAction_t::READ_RETRY) || | |
| t_state.cache_open_write_fail_action == | |
| static_cast<MgmtByte>(CacheOpenWriteFailAction_t::READ_RETRY_STALE_ON_REVALIDATE)); | |
| t_state.cache_lookup_result = HttpTransact::CacheLookupResult_t::NONE; | |
| t_state.cache_info.write_lock_state = HttpTransact::CacheWriteLock_t::READ_RETRY; | |
| break; | |
| } | |
| // The write vector was locked and the cache_sm retried | |
| // and got the read vector again. | |
| cache_sm.cache_read_vc->get_http_info(&t_state.cache_info.object_read); | |
| // ToDo: Should support other levels of cache hits here, but the cache does not support it (yet) | |
| if (cache_sm.cache_read_vc->is_ram_cache_hit()) { | |
| t_state.cache_info.hit_miss_code = SQUID_HIT_RAM; | |
| } else { | |
| t_state.cache_info.hit_miss_code = SQUID_HIT_DISK; | |
| } | |
| ink_assert(t_state.cache_info.object_read != nullptr); | |
| t_state.source = HttpTransact::Source_t::CACHE; | |
| // clear up CacheLookupResult_t::MISS, let Freshness function decide | |
| // hit status | |
| t_state.cache_open_write_fail_action = t_state.txn_conf->cache_open_write_fail_action; | |
| t_state.cache_lookup_result = HttpTransact::CacheLookupResult_t::NONE; | |
| t_state.cache_info.write_lock_state = HttpTransact::CacheWriteLock_t::READ_RETRY; | |
| break; | |
| case HTTP_TUNNEL_EVENT_DONE: | |
| // In the case where we have issued a cache write for the | |
| // transformed copy, the tunnel from the origin server to | |
| // the transform may complete while we are waiting for | |
| // the cache write. If this is the case, forward the event | |
| // to the transform read state as it will know how to | |
| // handle it | |
| if (t_state.next_action == HttpTransact::StateMachineAction_t::CACHE_ISSUE_WRITE_TRANSFORM) { | |
| state_common_wait_for_transform_read(&transform_info, &HttpSM::tunnel_handler, event, data); | |
| return 0; | |
| } | |
| // Fallthrough | |
| default: | |
| ink_release_assert(0); | |
| } | |
| // The write either succeeded or failed, notify transact | |
| call_transact_and_set_next_state(nullptr); | |
| return 0; | |
| } | |
| inline void | |
| HttpSM::setup_cache_lookup_complete_api() | |
| { | |
| t_state.api_next_action = HttpTransact::StateMachineAction_t::API_CACHE_LOOKUP_COMPLETE; | |
| do_api_callout(); | |
| } | |
| ////////////////////////////////////////////////////////////////////////// | |
| // | |
| // HttpSM::state_cache_open_read() | |
| // | |
| // This state handles the result of CacheProcessor::open_read() | |
| // that attempts to do cache lookup and open a particular cached | |
| // object for reading. | |
| // | |
| ////////////////////////////////////////////////////////////////////////// | |
| int | |
| HttpSM::state_cache_open_read(int event, void *data) | |
| { | |
| STATE_ENTER(state_cache_open_read, event); | |
| pending_action.clear_if_action_is(reinterpret_cast<Action *>(data)); | |
| ink_assert(server_entry == nullptr); | |
| ink_assert(t_state.cache_info.object_read == nullptr); | |
| switch (event) { | |
| case CACHE_EVENT_OPEN_READ: { | |
| pending_action = nullptr; | |
| SMDbg(dbg_ctl_http, "cache_open_read - CACHE_EVENT_OPEN_READ"); | |
| ///////////////////////////////// | |
| // lookup/open is successful. // | |
| ///////////////////////////////// | |
| ink_assert(cache_sm.cache_read_vc != nullptr); | |
| t_state.source = HttpTransact::Source_t::CACHE; | |
| cache_sm.cache_read_vc->get_http_info(&t_state.cache_info.object_read); | |
| // ToDo: Should support other levels of cache hits here, but the cache does not support it (yet) | |
| if (cache_sm.cache_read_vc->is_ram_cache_hit()) { | |
| t_state.cache_info.hit_miss_code = SQUID_HIT_RAM; | |
| } else { | |
| t_state.cache_info.hit_miss_code = SQUID_HIT_DISK; | |
| } | |
| if (compatibility_cache_lookup == CompatibilityCacheLookup::COMPAT_CACHE_LOOKUP_92) { | |
| Metrics::Counter::increment(http_rsb.cache_compat_key_reads); | |
| } | |
| // Record the cache open-read completion before state handling can advance | |
| // into cache serving and stamp later transaction milestones. | |
| ATS_PROBE1(milestone_cache_open_read_end, sm_id); | |
| milestones[TS_MILESTONE_CACHE_OPEN_READ_END] = ink_get_hrtime(); | |
| ink_assert(t_state.cache_info.object_read != nullptr); | |
| call_transact_and_set_next_state(HttpTransact::HandleCacheOpenRead); | |
| break; | |
| } | |
| case CACHE_EVENT_OPEN_READ_FAILED: | |
| pending_action = nullptr; | |
| SMDbg(dbg_ctl_http, "cache_open_read - CACHE_EVENT_OPEN_READ_FAILED with %s (%d)", InkStrerror(-cache_sm.get_last_error()), | |
| -cache_sm.get_last_error()); | |
| SMDbg(dbg_ctl_http, "open read failed."); | |
| // A path that carries its own ";params" segment already hashes to the 9.2 | |
| // key, so a compatibility lookup would just repeat the lookup that missed. | |
| if (cache_sm.get_last_error() != -ECACHE_DOC_BUSY && t_state.http_config_param->cache_try_compat_key_read && | |
| compatibility_cache_lookup == CompatibilityCacheLookup::COMPAT_CACHE_LOOKUP_NORMAL && | |
| !cache_lookup_url()->has_path_params()) { | |
| // do the retry | |
| compatibility_cache_lookup = CompatibilityCacheLookup::COMPAT_CACHE_LOOKUP_92; | |
| do_cache_lookup_and_read(); | |
| return 0; | |
| } | |
| // Nothing was found under either key, or the document is busy, so no legacy | |
| // object is in play past this point. The reset follows the retry decision | |
| // so that it cannot re-arm the retry. | |
| compatibility_cache_lookup = CompatibilityCacheLookup::COMPAT_CACHE_LOOKUP_NORMAL; | |
| if (cache_sm.get_last_error() == -ECACHE_DOC_BUSY) { | |
| // Inform HttpTransact somebody else is updating the document | |
| // HttpCacheSM already waited so transact should go ahead. | |
| t_state.cache_lookup_result = HttpTransact::CacheLookupResult_t::DOC_BUSY; | |
| } else { | |
| t_state.cache_lookup_result = HttpTransact::CacheLookupResult_t::MISS; | |
| } | |
| // Record the final cache open-read completion after any compatibility-key | |
| // retry has been ruled out. | |
| ATS_PROBE1(milestone_cache_open_read_end, sm_id); | |
| milestones[TS_MILESTONE_CACHE_OPEN_READ_END] = ink_get_hrtime(); | |
| ink_assert(t_state.transact_return_point == nullptr); | |
| t_state.transact_return_point = HttpTransact::HandleCacheOpenRead; | |
| // For READ_RETRY actions (5 and 6), skip the CACHE_LOOKUP_COMPLETE hook now. | |
| // The hook will fire later with the final result: HIT if stale content is found | |
| // during retry, or MISS if nothing is found (from HandleCacheOpenReadMiss). | |
| // This ensures plugins see only the final cache lookup result, avoiding issues | |
| // like stats double-counting and duplicate hook registrations. | |
| if (t_state.txn_conf->cache_open_write_fail_action == static_cast<MgmtByte>(CacheOpenWriteFailAction_t::READ_RETRY) || | |
| t_state.txn_conf->cache_open_write_fail_action == | |
| static_cast<MgmtByte>(CacheOpenWriteFailAction_t::READ_RETRY_STALE_ON_REVALIDATE)) { | |
| SMDbg(dbg_ctl_http, "READ_RETRY configured, deferring CACHE_LOOKUP_COMPLETE hook"); | |
| t_state.cache_lookup_complete_deferred = true; | |
| call_transact_and_set_next_state(nullptr); | |
| } else { | |
| setup_cache_lookup_complete_api(); | |
| } | |
| break; | |
| default: | |
| ink_release_assert(!"Unknown event"); | |
| break; | |
| } | |
| return 0; | |
| } | |
| int | |
| HttpSM::main_handler(int event, void *data) | |
| { | |
| ink_release_assert(magic == HttpSmMagic_t::ALIVE); | |
| HttpSMHandler jump_point = nullptr; | |
| ink_assert(reentrancy_count >= 0); | |
| reentrancy_count++; | |
| // Don't use the state enter macro since it uses history | |
| // space that we don't care about | |
| SMDbg(dbg_ctl_http, "%s, %d", HttpDebugNames::get_event_name(event), event); | |
| HttpVCTableEntry *vc_entry = nullptr; | |
| if (data != nullptr) { | |
| // Only search the VC table if the event could have to | |
| // do with a VIO to save a few cycles | |
| if (event < VC_EVENT_EVENTS_START + 100) { | |
| vc_entry = vc_table.find_entry(static_cast<VIO *>(data)); | |
| } | |
| } | |
| // Is there an active background-fill tunnel? | |
| // Background fill detaches the user-agent VC and lets the server/cache side | |
| // finish the response body. An Http2Stream can still deliver a scheduled UA | |
| // close/error callback after that detach, but the UA VIO no longer has a VC | |
| // table entry. This predicate recognizes only those detached UA-side events | |
| // so they do not fall through to the default tunnel handler and tear down the | |
| // HttpSM before the active background-fill tunnel completes. | |
| auto is_stale_bg_fill_ua_event = [&]() -> bool { | |
| if (background_fill != BackgroundFill_t::STARTED || !tunnel.is_tunnel_alive() || _ua.get_txn() == nullptr) { | |
| return false; | |
| } | |
| switch (event) { | |
| case VC_EVENT_EOS: | |
| case VC_EVENT_ERROR: | |
| case VC_EVENT_INACTIVITY_TIMEOUT: | |
| case VC_EVENT_ACTIVE_TIMEOUT: | |
| case VC_EVENT_WRITE_READY: | |
| case VC_EVENT_WRITE_COMPLETE: | |
| case VC_EVENT_READ_READY: | |
| case VC_EVENT_READ_COMPLETE: | |
| break; | |
| default: | |
| return false; | |
| } | |
| if (data == nullptr) { | |
| return true; | |
| } | |
| return static_cast<VIO *>(data)->vc_server == _ua.get_txn(); | |
| }; | |
| if (vc_entry) { | |
| jump_point = (static_cast<VIO *>(data) == vc_entry->read_vio) ? vc_entry->vc_read_handler : vc_entry->vc_write_handler; | |
| ink_assert(jump_point != (HttpSMHandler) nullptr); | |
| ink_assert(vc_entry->vc != (VConnection *)nullptr); | |
| (this->*jump_point)(event, data); | |
| } else if (is_stale_bg_fill_ua_event()) { | |
| SMDbg(dbg_ctl_http, "ignoring stale %s event for closed user agent while background fill is active", | |
| HttpDebugNames::get_event_name(event)); | |
| } else { | |
| ink_assert(default_handler != (HttpSMHandler) nullptr); | |
| (this->*default_handler)(event, data); | |
| } | |
| // The sub-handler signals when it is time for the state | |
| // machine to exit. We can only exit if we are not reentrantly | |
| // called otherwise when the our call unwinds, we will be | |
| // running on a dead state machine | |
| // | |
| // Because of the need for an api shutdown hook, kill_this() | |
| // is also reentrant. As such, we don't want to decrement | |
| // the reentrancy count until after we run kill_this() | |
| // | |
| if (terminate_sm == true && reentrancy_count == 1) { | |
| kill_this(); | |
| } else { | |
| reentrancy_count--; | |
| ink_assert(reentrancy_count >= 0); | |
| } | |
| return (VC_EVENT_CONT); | |
| } | |
| // void HttpSM::tunnel_handler_post_or_put() | |
| // | |
| // Handles the common cleanup tasks for Http post/put | |
| // to prevent code duplication | |
| // | |
| void | |
| HttpSM::tunnel_handler_post_or_put(HttpTunnelProducer *p) | |
| { | |
| ink_assert(p->vc_type == HttpTunnelType_t::HTTP_CLIENT || | |
| (static_cast<HttpSmPost_t>(p->handler_state) == HttpSmPost_t::UA_FAIL && p->vc_type == HttpTunnelType_t::BUFFER_READ)); | |
| HttpTunnelConsumer *c; | |
| // If there is a post transform, remove it's entry from the State | |
| // Machine's VC table | |
| // | |
| // MUST NOT clear the vc pointer from post_transform_info | |
| // as this causes a double close of the transform vc in transform_cleanup | |
| // | |
| if (post_transform_info.vc != nullptr) { | |
| ink_assert(post_transform_info.entry->in_tunnel == true); | |
| ink_assert(post_transform_info.vc == post_transform_info.entry->vc); | |
| vc_table.cleanup_entry(post_transform_info.entry); | |
| post_transform_info.entry = nullptr; | |
| } | |
| switch (static_cast<HttpSmPost_t>(p->handler_state)) { | |
| case HttpSmPost_t::SERVER_FAIL: | |
| c = tunnel.get_consumer(server_entry->vc); | |
| ink_assert(c->write_success == false); | |
| break; | |
| case HttpSmPost_t::UA_FAIL: | |
| // UA quit - shutdown the SM | |
| ink_assert(p->read_success == false); | |
| terminate_sm = true; | |
| break; | |
| case HttpSmPost_t::SUCCESS: | |
| // The post succeeded | |
| ink_assert(p->read_success == true); | |
| ink_assert(p->consumer_list.head->write_success == true); | |
| tunnel.deallocate_buffers(); | |
| tunnel.reset(); | |
| // When the ua completed sending it's data we must have | |
| // removed it from the tunnel | |
| _ua.get_entry()->in_tunnel = false; | |
| server_entry->in_tunnel = false; | |
| break; | |
| default: | |
| ink_release_assert(0); | |
| } | |
| } | |
| // int HttpSM::tunnel_handler_post(int event, void* data) | |
| // | |
| // Handles completion of any http request body tunnel | |
| // Having 'post' in its name is a misnomer | |
| // | |
| int | |
| HttpSM::tunnel_handler_post(int event, void *data) | |
| { | |
| STATE_ENTER(tunnel_handler_post, event); | |
| HttpTunnelProducer *p = | |
| _ua.get_txn() != nullptr ? tunnel.get_producer(_ua.get_txn()) : tunnel.get_producer(HttpTunnelType_t::HTTP_CLIENT); | |
| if (!p) { | |
| return 0; // Cannot do anything if there is no producer | |
| } | |
| switch (event) { | |
| case HTTP_TUNNEL_EVENT_DONE: // Tunnel done. | |
| if (static_cast<HttpSmPost_t>(p->handler_state) == HttpSmPost_t::UA_FAIL) { | |
| // post failed | |
| switch (t_state.client_info.state) { | |
| case HttpTransact::ACTIVE_TIMEOUT: | |
| tunnel.deallocate_buffers(); | |
| tunnel.reset(); | |
| call_transact_and_set_next_state(HttpTransact::PostActiveTimeoutResponse); | |
| return 0; | |
| case HttpTransact::INACTIVE_TIMEOUT: | |
| tunnel.deallocate_buffers(); | |
| tunnel.reset(); | |
| call_transact_and_set_next_state(HttpTransact::PostInactiveTimeoutResponse); | |
| return 0; | |
| case HttpTransact::PARSE_ERROR: | |
| tunnel.deallocate_buffers(); | |
| tunnel.reset(); | |
| call_transact_and_set_next_state(HttpTransact::BadRequest); | |
| return 0; | |
| default: | |
| break; | |
| } | |
| } | |
| break; | |
| case VC_EVENT_WRITE_READY: // iocore may callback first before send. | |
| return 0; | |
| case VC_EVENT_EOS: // SSLNetVC may callback EOS during write error (6.0.x or early) | |
| case VC_EVENT_ERROR: // Send HTTP 408 error | |
| case VC_EVENT_WRITE_COMPLETE: // tunnel_handler_post_ua has sent HTTP 408 response | |
| case VC_EVENT_INACTIVITY_TIMEOUT: // _ua.get_txn() timeout during sending the HTTP 408 response | |
| case VC_EVENT_ACTIVE_TIMEOUT: // _ua.get_txn() timeout | |
| if (_ua.get_entry()->write_buffer) { | |
| free_MIOBuffer(_ua.get_entry()->write_buffer); | |
| _ua.get_entry()->write_buffer = nullptr; | |
| } | |
| if (p->handler_state == static_cast<int>(HttpSmPost_t::UNKNOWN)) { | |
| p->handler_state = static_cast<int>(HttpSmPost_t::UA_FAIL); | |
| } | |
| break; | |
| case VC_EVENT_READ_READY: | |
| case VC_EVENT_READ_COMPLETE: | |
| default: | |
| ink_assert(!"not reached"); | |
| return 0; | |
| } | |
| ink_assert(event == HTTP_TUNNEL_EVENT_DONE); | |
| ink_assert(data == &tunnel); | |
| // The tunnel calls this when it is done | |
| int p_handler_state = p->handler_state; | |
| if (is_waiting_for_full_body && !this->is_postbuf_valid()) { | |
| p_handler_state = static_cast<int>(HttpSmPost_t::SERVER_FAIL); | |
| } | |
| if (p->vc_type != HttpTunnelType_t::BUFFER_READ) { | |
| tunnel_handler_post_or_put(p); | |
| } | |
| switch (static_cast<HttpSmPost_t>(p_handler_state)) { | |
| case HttpSmPost_t::SERVER_FAIL: | |
| handle_post_failure(); | |
| break; | |
| case HttpSmPost_t::UA_FAIL: | |
| // Client side failed. Shutdown and go home. No need to communicate back to UA | |
| terminate_sm = true; | |
| break; | |
| case HttpSmPost_t::SUCCESS: | |
| // It's time to start reading the response | |
| if (is_waiting_for_full_body) { | |
| is_waiting_for_full_body = false; | |
| is_buffering_request_body = true; | |
| client_request_body_bytes = this->postbuf_buffer_avail(); | |
| call_transact_and_set_next_state(HttpTransact::HandleRequestBufferDone); | |
| break; | |
| } | |
| // Is the response header ready and waiting? | |
| // If so, go ahead and do the hook processing | |
| if (milestones[TS_MILESTONE_SERVER_READ_HEADER_DONE] != 0) { | |
| t_state.current.state = HttpTransact::CONNECTION_ALIVE; | |
| t_state.transact_return_point = HttpTransact::HandleResponse; | |
| HttpTransact::strip_at_headers(t_state.hdr_info.server_response, HttpTransact::AtHeaderSource::ORIGIN_RESPONSE, sm_id); | |
| t_state.api_next_action = HttpTransact::StateMachineAction_t::API_READ_RESPONSE_HDR; | |
| do_api_callout(); | |
| } | |
| break; | |
| default: | |
| ink_release_assert(0); | |
| } | |
| return 0; | |
| } | |
| void | |
| HttpSM::setup_tunnel_handler_trailer(HttpTunnelProducer *p) | |
| { | |
| p->read_success = true; | |
| t_state.current.server->state = HttpTransact::TRANSACTION_COMPLETE; | |
| t_state.current.server->abort = HttpTransact::DIDNOT_ABORT; | |
| SMDbg(dbg_ctl_http, "Wait for the trailing header"); | |
| ProxyTransaction *ua_txn = _ua.get_txn(); | |
| if (!ua_txn) { | |
| tunnel.local_finish_all(p); | |
| return; | |
| } | |
| if (!ua_txn->can_send_h2_trailer()) { | |
| SMDbg(dbg_ctl_http, "User agent transaction cannot send HTTP/2 trailers; dropping the origin trailer"); | |
| tunnel.local_finish_all(p); | |
| return; | |
| } | |
| // Mark this before the body tunnel completes so HTTP/2 does not send END_STREAM | |
| // on the final DATA frame. | |
| ua_txn->set_expect_send_trailer(); | |
| // Swap out the default hander to set up the new tunnel for the trailer exchange. | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::tunnel_handler_trailer); | |
| tunnel.local_finish_all(p); | |
| } | |
| int | |
| HttpSM::tunnel_handler_trailer(int event, void *data) | |
| { | |
| STATE_ENTER(tunnel_handler_trailer, event); | |
| switch (event) { | |
| case HTTP_TUNNEL_EVENT_DONE: // Response tunnel done. | |
| break; | |
| default: | |
| // If the response tunnel did not succeed, just clean up as in the default case | |
| return tunnel_handler(event, data); | |
| } | |
| ink_assert(event == HTTP_TUNNEL_EVENT_DONE); | |
| // Set up a new tunnel to transport the trailing header to the UA | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::tunnel_handler); | |
| ProxyTransaction *ua_txn = _ua.get_txn(); | |
| HttpVCTableEntry *ua_entry = _ua.get_entry(); | |
| IOBufferReader *server_reader = server_txn ? server_txn->get_remote_reader() : nullptr; | |
| if (!ua_txn || !ua_entry || !ua_entry->vc || !server_reader || !server_entry || !server_entry->vc) { | |
| if (server_reader) { | |
| server_reader->consume(server_reader->read_avail()); | |
| } | |
| SMDbg(dbg_ctl_http, "Cannot set up trailer tunnel; dropping the origin trailer"); | |
| return tunnel_handler(event, data); | |
| } | |
| if (!ua_txn->expect_send_trailer()) { | |
| if (!ua_txn->can_send_h2_trailer()) { | |
| server_reader->consume(server_reader->read_avail()); | |
| SMDbg(dbg_ctl_http, "User agent transaction cannot send HTTP/2 trailers; dropping the origin trailer"); | |
| return tunnel_handler(event, data); | |
| } | |
| ua_txn->set_expect_send_trailer(); | |
| } | |
| MIOBuffer *trailer_buffer = new_MIOBuffer(HTTP_HEADER_BUFFER_SIZE_INDEX); | |
| IOBufferReader *buf_start = trailer_buffer->alloc_reader(); | |
| size_t nbytes = INT64_MAX; | |
| int start_bytes = trailer_buffer->write(server_reader, server_reader->read_avail()); | |
| server_reader->consume(start_bytes); | |
| // The server has already sent all it has | |
| if (server_txn->is_read_closed()) { | |
| nbytes = start_bytes; | |
| } | |
| tunnel.deallocate_buffers(); | |
| tunnel.reset(); | |
| HttpTunnelProducer *p = tunnel.add_producer(server_entry->vc, nbytes, buf_start, &HttpSM::tunnel_handler_trailer_server, | |
| HttpTunnelType_t::HTTP_SERVER, "http server trailer"); | |
| tunnel.add_consumer(ua_entry->vc, server_entry->vc, &HttpSM::tunnel_handler_trailer_ua, HttpTunnelType_t::HTTP_CLIENT, | |
| "user agent trailer"); | |
| ua_entry->in_tunnel = true; | |
| server_entry->in_tunnel = true; | |
| tunnel.tunnel_run(p); | |
| return 0; | |
| } | |
| int | |
| HttpSM::tunnel_handler_cache_fill(int event, void *data) | |
| { | |
| STATE_ENTER(tunnel_handler_cache_fill, event); | |
| ink_assert(event == HTTP_TUNNEL_EVENT_DONE); | |
| ink_assert(data == &tunnel); | |
| ink_release_assert(cache_sm.cache_write_vc); | |
| int64_t alloc_index = find_server_buffer_size(); | |
| MIOBuffer *buf = new_MIOBuffer(alloc_index); | |
| IOBufferReader *buf_start = buf->alloc_reader(); | |
| TunnelChunkingAction_t action = | |
| (t_state.current.server && t_state.current.server->transfer_encoding == HttpTransact::TransferEncoding_t::CHUNKED) ? | |
| TunnelChunkingAction_t::DECHUNK_CONTENT : | |
| TunnelChunkingAction_t::PASSTHRU_DECHUNKED_CONTENT; | |
| int64_t nbytes = server_transfer_init(buf, 0); | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::tunnel_handler); | |
| server_entry->vc = server_txn; | |
| HttpTunnelProducer *p = tunnel.add_producer(server_entry->vc, nbytes, buf_start, &HttpSM::tunnel_handler_server, | |
| HttpTunnelType_t::HTTP_SERVER, "http server"); | |
| bool const drop_chunked_trailers = t_state.http_config_param->oride.http_drop_chunked_trailers == 1; | |
| bool const parse_chunk_strictly = t_state.http_config_param->oride.http_strict_chunk_parsing == 1; | |
| tunnel.set_producer_chunking_action(p, 0, action, drop_chunked_trailers, parse_chunk_strictly); | |
| tunnel.set_producer_chunking_size(p, t_state.txn_conf->http_chunking_size); | |
| setup_cache_write_transfer(&cache_sm, server_entry->vc, &t_state.cache_info.object_store, 0, "cache write"); | |
| server_entry->in_tunnel = true; | |
| // Kick off the new producer | |
| tunnel.tunnel_run(p); | |
| return 0; | |
| } | |
| int | |
| HttpSM::tunnel_handler_100_continue(int event, void *data) | |
| { | |
| STATE_ENTER(tunnel_handler_100_continue, event); | |
| ink_assert(event == HTTP_TUNNEL_EVENT_DONE); | |
| ink_assert(data == &tunnel); | |
| // We're done sending the 100 continue. If we succeeded, | |
| // we set up to parse the next server response. If we | |
| // failed, shutdown the state machine | |
| HttpTunnelConsumer *c = tunnel.get_consumer(_ua.get_txn()); | |
| if (c->write_success) { | |
| // Note: we must use destroy() here since clear() | |
| // does not free the memory from the header | |
| t_state.hdr_info.client_response.destroy(); | |
| tunnel.deallocate_buffers(); | |
| tunnel.reset(); | |
| if (server_entry->eos) { | |
| // if the server closed while sending the | |
| // 100 continue header, handle it here so we | |
| // don't assert later | |
| SMDbg(dbg_ctl_http, "server already closed, terminating connection"); | |
| // Since 100 isn't a final (loggable) response header | |
| // kill the 100 continue header and create an empty one | |
| t_state.hdr_info.server_response.destroy(); | |
| t_state.hdr_info.server_response.create(HTTPType::RESPONSE); | |
| handle_server_setup_error(VC_EVENT_EOS, server_entry->read_vio); | |
| } else { | |
| do_setup_client_request_body_tunnel(HttpVC_t::SERVER_VC); | |
| } | |
| } else { | |
| terminate_sm = true; | |
| } | |
| return 0; | |
| } | |
| int | |
| HttpSM::tunnel_handler_push(int event, void *data) | |
| { | |
| STATE_ENTER(tunnel_handler_push, event); | |
| ink_assert(event == HTTP_TUNNEL_EVENT_DONE); | |
| ink_assert(data == &tunnel); | |
| // Check to see if the client is still around | |
| HttpTunnelProducer *ua = | |
| (_ua.get_txn()) ? tunnel.get_producer(_ua.get_txn()) : tunnel.get_producer(HttpTunnelType_t::HTTP_CLIENT); | |
| if (ua == nullptr || !ua->read_success) { | |
| // Client failed to send the body, it's gone. Kill the | |
| // state machine | |
| terminate_sm = true; | |
| return 0; | |
| } | |
| HttpTunnelConsumer *cache = ua->consumer_list.head; | |
| ink_release_assert(cache->vc_type == HttpTunnelType_t::CACHE_WRITE); | |
| bool cache_write_success = cache->write_success; | |
| // Reset tunneling state since we need to send a response | |
| // to client as whether we succeeded | |
| tunnel.deallocate_buffers(); | |
| this->postbuf_clear(); | |
| tunnel.reset(); | |
| if (cache_write_success) { | |
| call_transact_and_set_next_state(HttpTransact::HandlePushTunnelSuccess); | |
| } else { | |
| call_transact_and_set_next_state(HttpTransact::HandlePushTunnelFailure); | |
| } | |
| return 0; | |
| } | |
| int | |
| HttpSM::tunnel_handler(int event, void * /* data ATS_UNUSED */) | |
| { | |
| STATE_ENTER(tunnel_handler, event); | |
| // If we had already received EOS, just go away. We would sometimes see | |
| // a WRITE event appear after receiving EOS from the server connection | |
| if ((event == VC_EVENT_WRITE_READY || event == VC_EVENT_WRITE_COMPLETE) && server_entry->eos) { | |
| return 0; | |
| } | |
| ink_assert(event == HTTP_TUNNEL_EVENT_DONE || event == VC_EVENT_INACTIVITY_TIMEOUT || event == VC_EVENT_ACTIVE_TIMEOUT || | |
| event == VC_EVENT_ERROR || event == VC_EVENT_EOS); | |
| // The tunnel calls this when it is done | |
| terminate_sm = true; | |
| if (unlikely(t_state.is_websocket)) { | |
| Metrics::Gauge::decrement(http_rsb.websocket_current_active_client_connections); | |
| } | |
| return 0; | |
| } | |
| /**************************************************** | |
| TUNNELING HANDLERS | |
| ******************************************************/ | |
| bool | |
| HttpSM::is_http_server_eos_truncation(HttpTunnelProducer *p) | |
| { | |
| if ((p->do_dechunking || p->do_chunked_passthru) && p->chunked_handler.truncation) { | |
| return true; | |
| } | |
| ////////////////////////////////////////////////////////////// | |
| // If we did not get or did not trust the origin server's // | |
| // content-length, read_content_length is unset. The // | |
| // only way the end of the document is signaled is the // | |
| // origin server closing the connection. However, we // | |
| // need to protect against the document getting truncated // | |
| // because the origin server crashed. The following // | |
| // tabled outlines when we mark the server read as failed // | |
| // // | |
| // No C-L : read success // | |
| // Received byts < C-L : read failed (=> Cache Abort) // | |
| // Received byts == C-L : read success // | |
| // Received byts > C-L : read success // | |
| ////////////////////////////////////////////////////////////// | |
| int64_t cl = t_state.hdr_info.server_response.get_content_length(); | |
| if (cl != UNDEFINED_COUNT && cl > server_response_body_bytes) { | |
| SMDbg(dbg_ctl_http, "server EOS after %" PRId64 " bytes, expected %" PRId64, server_response_body_bytes, cl); | |
| return true; | |
| } else { | |
| return false; | |
| } | |
| } | |
| int | |
| HttpSM::tunnel_handler_server(int event, HttpTunnelProducer *p) | |
| { | |
| STATE_ENTER(tunnel_handler_server, event); | |
| // An intercept handler may not set TS_MILESTONE_SERVER_CONNECT | |
| // by default. Therefore we only set TS_MILESTONE_SERVER_CLOSE if | |
| // TS_MILESTONE_SERVER_CONNECT is set (non-zero), lest certain time | |
| // statistics are calculated from epoch time. | |
| if (0 != milestones[TS_MILESTONE_SERVER_CONNECT]) { | |
| ATS_PROBE1(milestone_server_close, sm_id); | |
| milestones[TS_MILESTONE_SERVER_CLOSE] = ink_get_hrtime(); | |
| } | |
| bool close_connection = false; | |
| if (t_state.current.server->keep_alive == HTTPKeepAlive::KEEPALIVE && server_entry->eos == false && | |
| plugin_tunnel_type == HttpPluginTunnel_t::NONE && t_state.txn_conf->keep_alive_enabled_out == 1) { | |
| close_connection = false; | |
| } else { | |
| if (t_state.current.server->keep_alive != HTTPKeepAlive::KEEPALIVE) { | |
| Metrics::Counter::increment(http_rsb.origin_shutdown_tunnel_server_no_keep_alive); | |
| } else if (server_entry->eos == true) { | |
| Metrics::Counter::increment(http_rsb.origin_shutdown_tunnel_server_eos); | |
| } else { | |
| Metrics::Counter::increment(http_rsb.origin_shutdown_tunnel_server_plugin_tunnel); | |
| } | |
| close_connection = true; | |
| } | |
| switch (event) { | |
| case VC_EVENT_INACTIVITY_TIMEOUT: | |
| case VC_EVENT_ACTIVE_TIMEOUT: | |
| case VC_EVENT_ERROR: | |
| t_state.squid_codes.log_code = SquidLogCode::ERR_READ_TIMEOUT; | |
| t_state.squid_codes.hier_code = SquidHierarchyCode::TIMEOUT_DIRECT; | |
| /* fallthru */ | |
| case VC_EVENT_EOS: | |
| case HTTP_TUNNEL_EVENT_PARSE_ERROR: | |
| switch (event) { | |
| case VC_EVENT_INACTIVITY_TIMEOUT: | |
| t_state.current.server->state = HttpTransact::INACTIVE_TIMEOUT; | |
| break; | |
| case VC_EVENT_ACTIVE_TIMEOUT: | |
| t_state.current.server->state = HttpTransact::ACTIVE_TIMEOUT; | |
| break; | |
| case VC_EVENT_ERROR: | |
| t_state.current.server->state = HttpTransact::CONNECTION_ERROR; | |
| break; | |
| case VC_EVENT_EOS: | |
| t_state.current.server->state = HttpTransact::TRANSACTION_COMPLETE; | |
| break; | |
| case HTTP_TUNNEL_EVENT_PARSE_ERROR: | |
| t_state.current.server->state = HttpTransact::PARSE_ERROR; | |
| break; | |
| } | |
| Metrics::Counter::increment(http_rsb.origin_shutdown_tunnel_server); | |
| close_connection = true; | |
| ink_assert(p->vc_type == HttpTunnelType_t::HTTP_SERVER); | |
| if (is_http_server_eos_truncation(p)) { | |
| SMDbg(dbg_ctl_http, "aborting HTTP tunnel due to server truncation"); | |
| tunnel.chain_abort_all(p); | |
| // UA session may not be in the tunnel yet, don't NULL out the pointer in that case. | |
| // Note: This is a hack. The correct solution is for the UA session to signal back to the SM | |
| // when the UA is about to be destroyed and clean up the pointer there. That should be done once | |
| // the TS-3612 changes are in place (and similarly for the server session). | |
| /*if (_ua.get_entry()->in_tunnel) | |
| _ua.set_txn(nullptr); */ | |
| t_state.current.server->abort = HttpTransact::ABORTED; | |
| t_state.client_info.keep_alive = HTTPKeepAlive::NO_KEEPALIVE; | |
| t_state.current.server->keep_alive = HTTPKeepAlive::NO_KEEPALIVE; | |
| if (event == VC_EVENT_EOS) { | |
| t_state.squid_codes.log_code = SquidLogCode::ERR_READ_ERROR; | |
| } | |
| } else { | |
| SMDbg(dbg_ctl_http, "finishing HTTP tunnel"); | |
| p->read_success = true; | |
| t_state.current.server->abort = HttpTransact::DIDNOT_ABORT; | |
| // Appending reason to a response without Content-Length will result in | |
| // the reason string being written to the client and a bad CL when reading from cache. | |
| // I didn't find anywhere this appended reason is being used, so commenting it out. | |
| /* | |
| if (t_state.is_cacheable_due_to_negative_caching_configuration && p->bytes_read == 0) { | |
| int reason_len; | |
| const char *reason = t_state.hdr_info.server_response.reason_get(&reason_len); | |
| if (reason == NULL) | |
| tunnel.append_message_to_producer_buffer(p, "Negative Response", sizeof("Negative Response") - 1); | |
| else | |
| tunnel.append_message_to_producer_buffer(p, reason, reason_len); | |
| } | |
| */ | |
| if (server_txn->expect_receive_trailer()) { | |
| setup_tunnel_handler_trailer(p); | |
| return 0; | |
| } | |
| tunnel.local_finish_all(p); | |
| } | |
| break; | |
| case HTTP_TUNNEL_EVENT_PRECOMPLETE: | |
| case VC_EVENT_READ_COMPLETE: | |
| // | |
| // The transfer completed successfully | |
| // If there is still data in the buffer, the server | |
| // sent too much indicating a failed transfer | |
| p->read_success = true; | |
| t_state.current.server->state = HttpTransact::TRANSACTION_COMPLETE; | |
| t_state.current.server->abort = HttpTransact::DIDNOT_ABORT; | |
| if (p->do_dechunking || p->do_chunked_passthru) { | |
| if (p->chunked_handler.truncation) { | |
| tunnel.abort_cache_write_finish_others(p); | |
| // We couldn't read all chunks successfully: | |
| // Disable keep-alive. | |
| t_state.client_info.keep_alive = HTTPKeepAlive::NO_KEEPALIVE; | |
| t_state.current.server->keep_alive = HTTPKeepAlive::NO_KEEPALIVE; | |
| } else { | |
| tunnel.local_finish_all(p); | |
| } | |
| } | |
| if (server_txn->expect_receive_trailer()) { | |
| setup_tunnel_handler_trailer(p); | |
| return 0; | |
| } | |
| break; | |
| case HTTP_TUNNEL_EVENT_CONSUMER_DETACH: | |
| // All consumers are prematurely gone. Shutdown | |
| // the server connection | |
| p->read_success = true; | |
| t_state.current.server->state = HttpTransact::TRANSACTION_COMPLETE; | |
| t_state.current.server->abort = HttpTransact::DIDNOT_ABORT; | |
| Metrics::Counter::increment(http_rsb.origin_shutdown_tunnel_server_detach); | |
| close_connection = true; | |
| break; | |
| case VC_EVENT_READ_READY: | |
| case VC_EVENT_WRITE_READY: | |
| case VC_EVENT_WRITE_COMPLETE: | |
| default: | |
| // None of these events should ever come our way | |
| ink_assert(0); | |
| break; | |
| } | |
| // turn off negative caching in case there are multiple server contacts | |
| if (t_state.is_cacheable_due_to_negative_caching_configuration) { | |
| t_state.is_cacheable_due_to_negative_caching_configuration = false; | |
| } | |
| // If we had a ground fill, check update our status | |
| if (background_fill == BackgroundFill_t::STARTED) { | |
| background_fill = p->read_success ? BackgroundFill_t::COMPLETED : BackgroundFill_t::ABORTED; | |
| ATS_PROBE3(http_bg_fill_finish, sm_id, event, static_cast<int>(background_fill)); | |
| Metrics::Gauge::decrement(http_rsb.background_fill_current_count); | |
| } | |
| // We handled the event. Now either shutdown the connection or | |
| // setup it up for keep-alive | |
| ink_assert(p->vc_type == HttpTunnelType_t::HTTP_SERVER); | |
| ink_assert(p->vc == server_txn); | |
| // The server session has been released. Clean all pointer | |
| // Calling remove_entry instead of server_entry because we don't | |
| // want to close the server VC at this point | |
| vc_table.remove_entry(server_entry); | |
| if (close_connection) { | |
| p->vc->do_io_close(); | |
| p->read_vio = nullptr; | |
| /* TS-1424: if we're outbound transparent and using the client | |
| source port for the outbound connection we must effectively | |
| propagate server closes back to the client. Part of that is | |
| disabling KeepAlive if the server closes. | |
| */ | |
| if (_ua.get_txn() && _ua.get_txn()->is_outbound_transparent() && t_state.http_config_param->use_client_source_port) { | |
| t_state.client_info.keep_alive = HTTPKeepAlive::NO_KEEPALIVE; | |
| } | |
| } else { | |
| // If the option to attach the server session to the client session is set | |
| // and if the client is still around and the client is keep-alive, attach the | |
| // server session to so the next ka request can use it. Server sessions will | |
| // be placed into the shared pool if the next incoming request is for a different | |
| // origin server | |
| bool release_origin_connection = true; | |
| if (t_state.txn_conf->attach_server_session_to_client == 1 && _ua.get_txn() && | |
| t_state.client_info.keep_alive == HTTPKeepAlive::KEEPALIVE) { | |
| SMDbg(dbg_ctl_http, "attaching server session to the client"); | |
| if (_ua.get_txn()->attach_server_session(static_cast<PoolableSession *>(server_txn->get_proxy_ssn()))) { | |
| release_origin_connection = false; | |
| } | |
| } | |
| if (release_origin_connection) { | |
| // Release the session back into the shared session pool | |
| server_txn->set_inactivity_timeout(HRTIME_SECONDS(t_state.txn_conf->keep_alive_no_activity_timeout_out)); | |
| server_txn->release(); | |
| } | |
| } | |
| return 0; | |
| } | |
| int | |
| HttpSM::tunnel_handler_trailer_server(int event, HttpTunnelProducer *p) | |
| { | |
| STATE_ENTER(tunnel_handler_trailer_server, event); | |
| switch (event) { | |
| case VC_EVENT_INACTIVITY_TIMEOUT: | |
| case VC_EVENT_ACTIVE_TIMEOUT: | |
| case VC_EVENT_ERROR: | |
| t_state.squid_codes.log_code = SquidLogCode::ERR_READ_TIMEOUT; | |
| t_state.squid_codes.hier_code = SquidHierarchyCode::TIMEOUT_DIRECT; | |
| /* fallthru */ | |
| case VC_EVENT_EOS: | |
| switch (event) { | |
| case VC_EVENT_INACTIVITY_TIMEOUT: | |
| t_state.current.server->state = HttpTransact::INACTIVE_TIMEOUT; | |
| break; | |
| case VC_EVENT_ACTIVE_TIMEOUT: | |
| t_state.current.server->state = HttpTransact::ACTIVE_TIMEOUT; | |
| break; | |
| case VC_EVENT_ERROR: | |
| t_state.current.server->state = HttpTransact::CONNECTION_ERROR; | |
| break; | |
| case VC_EVENT_EOS: | |
| t_state.current.server->state = HttpTransact::TRANSACTION_COMPLETE; | |
| break; | |
| } | |
| ink_assert(p->vc_type == HttpTunnelType_t::HTTP_SERVER); | |
| SMDbg(dbg_ctl_http, "aborting HTTP tunnel due to server truncation"); | |
| tunnel.chain_abort_all(p); | |
| t_state.current.server->abort = HttpTransact::ABORTED; | |
| t_state.client_info.keep_alive = HTTPKeepAlive::NO_KEEPALIVE; | |
| t_state.current.server->keep_alive = HTTPKeepAlive::NO_KEEPALIVE; | |
| t_state.squid_codes.log_code = SquidLogCode::ERR_READ_ERROR; | |
| break; | |
| case HTTP_TUNNEL_EVENT_PRECOMPLETE: | |
| case VC_EVENT_READ_COMPLETE: | |
| // | |
| // The transfer completed successfully | |
| p->read_success = true; | |
| t_state.current.server->state = HttpTransact::TRANSACTION_COMPLETE; | |
| t_state.current.server->abort = HttpTransact::DIDNOT_ABORT; | |
| break; | |
| case HTTP_TUNNEL_EVENT_CONSUMER_DETACH: | |
| case VC_EVENT_READ_READY: | |
| case VC_EVENT_WRITE_READY: | |
| case VC_EVENT_WRITE_COMPLETE: | |
| default: | |
| // None of these events should ever come our way | |
| ink_assert(0); | |
| break; | |
| } | |
| // We handled the event. Now either shutdown server transaction | |
| ink_assert(server_entry->vc == p->vc); | |
| ink_assert(p->vc_type == HttpTunnelType_t::HTTP_SERVER); | |
| ink_assert(p->vc == server_txn); | |
| // The server session has been released. Clean all pointer | |
| // Calling remove_entry instead of server_entry because we don't | |
| // want to close the server VC at this point | |
| vc_table.remove_entry(server_entry); | |
| p->vc->do_io_close(); | |
| p->read_vio = nullptr; | |
| server_entry = nullptr; | |
| return 0; | |
| } | |
| // int HttpSM::tunnel_handler_100_continue_ua(int event, HttpTunnelConsumer* c) | |
| // | |
| // Used for tunneling the 100 continue response. The tunnel | |
| // should not close or release the user agent unless there is | |
| // an error since the real response is yet to come | |
| // | |
| int | |
| HttpSM::tunnel_handler_100_continue_ua(int event, HttpTunnelConsumer *c) | |
| { | |
| STATE_ENTER(tunnel_handler_100_continue_ua, event); | |
| ink_assert(c->vc == _ua.get_txn()); | |
| switch (event) { | |
| case VC_EVENT_EOS: | |
| _ua.get_entry()->eos = true; | |
| // FALL-THROUGH | |
| case VC_EVENT_INACTIVITY_TIMEOUT: | |
| case VC_EVENT_ACTIVE_TIMEOUT: | |
| case VC_EVENT_ERROR: | |
| set_ua_abort(HttpTransact::ABORTED, event); | |
| vc_table.remove_entry(_ua.get_entry()); | |
| c->vc->do_io_close(); | |
| break; | |
| case VC_EVENT_WRITE_COMPLETE: | |
| // mark the vc as no longer in tunnel | |
| // so we don't get hosed if the ua abort before | |
| // real response header is received | |
| _ua.get_entry()->in_tunnel = false; | |
| c->write_success = true; | |
| // Disable any write operation in case there are timeout events. | |
| if (c->vc != nullptr) { | |
| c->vc->do_io_write(nullptr, 0, nullptr); | |
| } | |
| } | |
| return 0; | |
| } | |
| bool | |
| HttpSM::is_bg_fill_necessary(HttpTunnelConsumer *c) | |
| { | |
| ink_assert(c->vc_type == HttpTunnelType_t::HTTP_CLIENT); | |
| if (c->producer->alive && // something there to read | |
| // server_entry && server_entry->vc && // from an origin server | |
| // server_txn && server_txn->get_netvc() && // which is still open and valid | |
| c->producer->num_consumers > 1 // with someone else reading it | |
| ) { | |
| HttpTunnelProducer *p = nullptr; | |
| if (!server_txn || !server_txn->get_netvc()) { | |
| // return true if we have finished the reading from OS when client aborted | |
| p = c->producer->self_consumer ? c->producer->self_consumer->producer : c->producer; | |
| if (p->vc_type == HttpTunnelType_t::HTTP_SERVER && p->read_success) { | |
| return true; | |
| } else { | |
| return false; | |
| } | |
| } | |
| // If threshold is 0.0 or negative then do background | |
| // fill regardless of the content length. Since this | |
| // is floating point just make sure the number is near zero | |
| if (t_state.txn_conf->background_fill_threshold <= 0.001) { | |
| return true; | |
| } | |
| int64_t ua_cl = t_state.hdr_info.client_response.get_content_length(); | |
| if (ua_cl > 0) { | |
| int64_t ua_body_done = c->bytes_written - client_response_hdr_bytes; | |
| float pDone = static_cast<float>(ua_body_done) / ua_cl; | |
| // If we got a good content length. Check to make sure that we haven't already | |
| // done more the content length since that would indicate the content-length | |
| // is bogus. If we've done more than the threshold, continue the background fill | |
| if (pDone <= 1.0 && pDone > t_state.txn_conf->background_fill_threshold) { | |
| return true; | |
| } else { | |
| SMDbg(dbg_ctl_http, "no background. Only %%%f of %%%f done [%" PRId64 " / %" PRId64 " ]", pDone, | |
| t_state.txn_conf->background_fill_threshold, ua_body_done, ua_cl); | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| int | |
| HttpSM::tunnel_handler_ua(int event, HttpTunnelConsumer *c) | |
| { | |
| bool close_connection = true; | |
| HttpTunnelProducer *p = nullptr; | |
| HttpTunnelConsumer *selfc = nullptr; | |
| STATE_ENTER(tunnel_handler_ua, event); | |
| ink_assert(c->vc == _ua.get_txn()); | |
| ATS_PROBE1(milestone_ua_close, sm_id); | |
| milestones[TS_MILESTONE_UA_CLOSE] = ink_get_hrtime(); | |
| switch (event) { | |
| case VC_EVENT_EOS: | |
| _ua.get_entry()->eos = true; | |
| // FALL-THROUGH | |
| case VC_EVENT_INACTIVITY_TIMEOUT: | |
| case VC_EVENT_ACTIVE_TIMEOUT: | |
| case VC_EVENT_ERROR: | |
| // The user agent died or aborted. Check to | |
| // see if we should setup a background fill | |
| set_ua_abort(HttpTransact::ABORTED, event); | |
| if (is_bg_fill_necessary(c)) { | |
| p = c->producer->self_consumer ? c->producer->self_consumer->producer : c->producer; | |
| SMDbg(dbg_ctl_http, "Initiating background fill"); | |
| // check whether to finish the reading. | |
| background_fill = p->read_success ? BackgroundFill_t::COMPLETED : BackgroundFill_t::STARTED; | |
| ATS_PROBE6(http_bg_fill_start, sm_id, event, c->producer->num_consumers, static_cast<int>(background_fill), | |
| c->bytes_written - client_response_hdr_bytes, | |
| static_cast<int>(t_state.txn_conf->background_fill_threshold * 1000)); | |
| // There is another consumer (cache write) so | |
| // detach the user agent | |
| if (background_fill == BackgroundFill_t::STARTED) { | |
| Metrics::Gauge::increment(http_rsb.background_fill_current_count); | |
| Metrics::Counter::increment(http_rsb.background_fill_total_count); | |
| ink_assert(c->is_downstream_from(server_txn)); | |
| server_txn->set_active_timeout(HRTIME_SECONDS(t_state.txn_conf->background_fill_active_timeout)); | |
| } | |
| // Even with the background fill, the client side should go down | |
| c->write_vio = nullptr; | |
| vc_table.remove_entry(_ua.get_entry()); | |
| c->vc->do_io_close(EHTTP_ERROR); | |
| c->alive = false; | |
| } else { | |
| // No background fill | |
| p = c->producer; | |
| tunnel.chain_abort_all(c->producer); | |
| selfc = p->self_consumer; | |
| if (selfc) { | |
| // This is the case where there is a transformation between ua and os | |
| p = selfc->producer; | |
| // if producer is the cache or OS, close the producer. | |
| // Otherwise in case of large docs, producer iobuffer gets filled up, | |
| // waiting for a consumer to consume data and the connection is never closed. | |
| if (p->alive && ((p->vc_type == HttpTunnelType_t::CACHE_READ) || (p->vc_type == HttpTunnelType_t::HTTP_SERVER))) { | |
| tunnel.chain_abort_all(p); | |
| } | |
| } | |
| } | |
| break; | |
| case VC_EVENT_WRITE_COMPLETE: | |
| c->write_success = true; | |
| t_state.client_info.abort = HttpTransact::DIDNOT_ABORT; | |
| if (t_state.client_info.keep_alive == HTTPKeepAlive::KEEPALIVE) { | |
| if (t_state.www_auth_content != HttpTransact::CacheAuth_t::SERVE || _ua.get_txn()->get_server_session()) { | |
| // successful keep-alive | |
| close_connection = false; | |
| } | |
| } | |
| break; | |
| case VC_EVENT_WRITE_READY: | |
| case VC_EVENT_READ_READY: | |
| case VC_EVENT_READ_COMPLETE: | |
| default: | |
| // None of these events should ever come our way | |
| ink_assert(0); | |
| break; | |
| } | |
| client_response_body_bytes = c->bytes_written - client_response_hdr_bytes; | |
| if (client_response_body_bytes < 0) { | |
| client_response_body_bytes = 0; | |
| } | |
| // attribute the size written to the client from various sources | |
| // NOTE: responses that go through a range transform are attributed | |
| // to their original sources | |
| // all other transforms attribute the total number of input bytes | |
| // to a source in HttpSM::tunnel_handler_transform_write | |
| // | |
| HttpTransact::Source_t original_source = t_state.source; | |
| if (HttpTransact::Source_t::TRANSFORM == original_source && t_state.range_setup != HttpTransact::RangeSetup_t::NONE) { | |
| original_source = t_state.pre_transform_source; | |
| } | |
| switch (original_source) { | |
| case HttpTransact::Source_t::HTTP_ORIGIN_SERVER: | |
| server_response_body_bytes = client_response_body_bytes; | |
| break; | |
| case HttpTransact::Source_t::CACHE: | |
| cache_response_body_bytes = client_response_body_bytes; | |
| break; | |
| default: | |
| break; | |
| } | |
| ProxyTransaction *ua_txn = _ua.get_txn(); | |
| if (event == VC_EVENT_WRITE_COMPLETE && server_txn && server_txn->expect_receive_trailer() && ua_txn && | |
| ua_txn->expect_send_trailer()) { | |
| // Don't shutdown if we are still expecting a trailer | |
| } else if (close_connection) { | |
| // If the client could be pipelining or is doing a POST, we need to | |
| // set the _ua.get_txn() into half close mode | |
| // only external POSTs should be subject to this logic; ruling out internal POSTs here | |
| bool is_eligible_post_request = ((t_state.method == HTTP_WKSIDX_POST) && !is_internal); | |
| if (is_eligible_post_request && c->producer->vc_type != HttpTunnelType_t::STATIC && event == VC_EVENT_WRITE_COMPLETE) { | |
| _ua.get_txn()->set_half_close_flag(true); | |
| } | |
| vc_table.remove_entry(this->_ua.get_entry()); | |
| ink_release_assert(vc_table.find_entry(_ua.get_txn()) == nullptr); | |
| _ua.get_txn()->do_io_close(); | |
| } else { | |
| ink_assert(_ua.get_txn()->get_remote_reader() != nullptr); | |
| vc_table.remove_entry(this->_ua.get_entry()); | |
| _ua.get_txn()->release(); | |
| } | |
| return 0; | |
| } | |
| int | |
| HttpSM::tunnel_handler_trailer_ua(int event, HttpTunnelConsumer *c) | |
| { | |
| HttpTunnelProducer *p = nullptr; | |
| HttpTunnelConsumer *selfc = nullptr; | |
| STATE_ENTER(tunnel_handler_trailer_ua, event); | |
| ink_assert(c->vc == _ua.get_txn()); | |
| ATS_PROBE1(milestone_ua_close, sm_id); | |
| milestones[TS_MILESTONE_UA_CLOSE] = ink_get_hrtime(); | |
| switch (event) { | |
| case VC_EVENT_EOS: | |
| _ua.get_entry()->eos = true; | |
| // FALL-THROUGH | |
| case VC_EVENT_INACTIVITY_TIMEOUT: | |
| case VC_EVENT_ACTIVE_TIMEOUT: | |
| case VC_EVENT_ERROR: | |
| // The user agent died or aborted. Check to | |
| // see if we should setup a background fill | |
| set_ua_abort(HttpTransact::ABORTED, event); | |
| // Should not be processing trailer headers in the background fill case | |
| ink_assert(!is_bg_fill_necessary(c)); | |
| p = c->producer; | |
| tunnel.chain_abort_all(c->producer); | |
| selfc = p->self_consumer; | |
| if (selfc) { | |
| // This is the case where there is a transformation between ua and os | |
| p = selfc->producer; | |
| // if producer is the cache or OS, close the producer. | |
| // Otherwise in case of large docs, producer iobuffer gets filled up, | |
| // waiting for a consumer to consume data and the connection is never closed. | |
| if (p->alive && ((p->vc_type == HttpTunnelType_t::CACHE_READ) || (p->vc_type == HttpTunnelType_t::HTTP_SERVER))) { | |
| tunnel.chain_abort_all(p); | |
| } | |
| } | |
| break; | |
| case VC_EVENT_WRITE_COMPLETE: | |
| c->write_success = true; | |
| t_state.client_info.abort = HttpTransact::DIDNOT_ABORT; | |
| break; | |
| case VC_EVENT_WRITE_READY: | |
| case VC_EVENT_READ_READY: | |
| case VC_EVENT_READ_COMPLETE: | |
| default: | |
| // None of these events should ever come our way | |
| ink_assert(0); | |
| break; | |
| } | |
| ink_assert(_ua.get_entry()->vc == c->vc); | |
| vc_table.remove_entry(this->_ua.get_entry()); | |
| _ua.get_txn()->do_io_close(); | |
| ink_release_assert(vc_table.find_entry(_ua.get_txn()) == nullptr); | |
| return 0; | |
| } | |
| int | |
| HttpSM::tunnel_handler_ua_push(int event, HttpTunnelProducer *p) | |
| { | |
| STATE_ENTER(tunnel_handler_ua_push, event); | |
| pushed_response_body_bytes += p->bytes_read; | |
| client_request_body_bytes += p->bytes_read; | |
| switch (event) { | |
| case VC_EVENT_INACTIVITY_TIMEOUT: | |
| case VC_EVENT_ACTIVE_TIMEOUT: | |
| case VC_EVENT_ERROR: | |
| case VC_EVENT_EOS: | |
| // Transfer terminated. Bail on the cache write. | |
| set_ua_abort(HttpTransact::ABORTED, event); | |
| p->vc->do_io_close(EHTTP_ERROR); | |
| p->read_vio = nullptr; | |
| tunnel.chain_abort_all(p); | |
| break; | |
| case HTTP_TUNNEL_EVENT_PRECOMPLETE: | |
| case VC_EVENT_READ_COMPLETE: | |
| // | |
| // The transfer completed successfully | |
| p->read_success = true; | |
| _ua.get_entry()->in_tunnel = false; | |
| break; | |
| case VC_EVENT_READ_READY: | |
| case VC_EVENT_WRITE_READY: | |
| case VC_EVENT_WRITE_COMPLETE: | |
| default: | |
| // None of these events should ever come our way | |
| ink_assert(0); | |
| break; | |
| } | |
| return 0; | |
| } | |
| int | |
| HttpSM::tunnel_handler_cache_read(int event, HttpTunnelProducer *p) | |
| { | |
| STATE_ENTER(tunnel_handler_cache_read, event); | |
| switch (event) { | |
| case VC_EVENT_ERROR: | |
| case VC_EVENT_EOS: | |
| ink_assert(t_state.cache_info.object_read->valid()); | |
| if (t_state.cache_info.object_read->object_size_get() != INT64_MAX || event == VC_EVENT_ERROR) { | |
| // Abnormal termination | |
| t_state.squid_codes.log_code = SquidLogCode::TCP_SWAPFAIL; | |
| p->vc->do_io_close(EHTTP_ERROR); | |
| p->read_vio = nullptr; | |
| tunnel.chain_abort_all(p); | |
| Metrics::Counter::increment(http_rsb.cache_read_errors); | |
| break; | |
| } else { | |
| tunnel.local_finish_all(p); | |
| // fall through for the case INT64_MAX read with VC_EVENT_EOS | |
| // callback (read successful) | |
| } | |
| // fallthrough | |
| case VC_EVENT_READ_COMPLETE: | |
| case HTTP_TUNNEL_EVENT_PRECOMPLETE: | |
| case HTTP_TUNNEL_EVENT_CONSUMER_DETACH: | |
| p->read_success = true; | |
| p->vc->do_io_close(); | |
| p->read_vio = nullptr; | |
| break; | |
| default: | |
| ink_release_assert(0); | |
| break; | |
| } | |
| Metrics::Gauge::decrement(http_rsb.current_cache_connections); | |
| return 0; | |
| } | |
| int | |
| HttpSM::tunnel_handler_cache_write(int event, HttpTunnelConsumer *c) | |
| { | |
| STATE_ENTER(tunnel_handler_cache_write, event); | |
| SMDbg(dbg_ctl_http, "handling cache event: %s", HttpDebugNames::get_event_name(event)); | |
| HttpTransact::CacheWriteStatus_t *status_ptr = (c->producer->vc_type == HttpTunnelType_t::TRANSFORM) ? | |
| &t_state.cache_info.transform_write_status : | |
| &t_state.cache_info.write_status; | |
| switch (event) { | |
| case VC_EVENT_ERROR: | |
| case VC_EVENT_EOS: | |
| // Abnormal termination | |
| *status_ptr = HttpTransact::CacheWriteStatus_t::ERROR; | |
| c->write_vio = nullptr; | |
| c->vc->do_io_close(EHTTP_ERROR); | |
| Metrics::Counter::increment(http_rsb.cache_write_errors); | |
| SMDbg(dbg_ctl_http, "aborting cache write due %s event from cache", HttpDebugNames::get_event_name(event)); | |
| // abort the producer if the cache_writevc is the only consumer. | |
| if (c->producer->alive && c->producer->num_consumers == 1) { | |
| tunnel.chain_abort_all(c->producer); | |
| } | |
| break; | |
| case VC_EVENT_WRITE_COMPLETE: | |
| // if we've never initiated a cache write | |
| // abort the cache since it's finicky about a close | |
| // in this case. This case can only occur | |
| // we got a truncated header from the origin server | |
| // but decided to accept it anyways | |
| if (c->write_vio == nullptr) { | |
| *status_ptr = HttpTransact::CacheWriteStatus_t::ERROR; | |
| c->write_success = false; | |
| c->vc->do_io_close(EHTTP_ERROR); | |
| } else { | |
| *status_ptr = HttpTransact::CacheWriteStatus_t::COMPLETE; | |
| c->write_success = true; | |
| c->vc->do_io_close(); | |
| c->write_vio = nullptr; | |
| } | |
| break; | |
| default: | |
| // All other events indicate problems | |
| ink_assert(0); | |
| break; | |
| } | |
| if (background_fill != BackgroundFill_t::NONE) { | |
| server_response_body_bytes = c->bytes_written; | |
| } | |
| // The cache write VC is closed now, so any prepared write lock is gone. | |
| t_state.cache_info.write_lock_state = HttpTransact::CacheWriteLock_t::INIT; | |
| Metrics::Gauge::decrement(http_rsb.current_cache_connections); | |
| return 0; | |
| } | |
| int | |
| HttpSM::tunnel_handler_post_ua(int event, HttpTunnelProducer *p) | |
| { | |
| STATE_ENTER(tunnel_handler_post_ua, event); | |
| // Now that the tunnel is done, it can tell us how many bytes were in the | |
| // body. | |
| if (client_request_body_bytes == 0) { | |
| // This is invoked multiple times for a transaction when buffering request | |
| // body data, so we only call this the first time when | |
| // client_request_body_bytes is 0. | |
| client_request_body_bytes = p->bytes_consumed; | |
| IOBufferReader *client_reader = _ua.get_txn()->get_remote_reader(); | |
| // p->bytes_consumed represents the number of body bytes the tunnel parsed | |
| // and consumed from the client. However, not all those bytes may have been | |
| // written to our _ua client transaction reader. We must not consume past | |
| // the number of bytes available. | |
| int64_t const bytes_to_consume = std::min(p->bytes_consumed, client_reader->read_avail()); | |
| SMDbg(dbg_ctl_http_tunnel, | |
| "Consuming %" PRId64 " bytes from client reader with p->bytes_consumed: %" PRId64 " available: %" PRId64, | |
| bytes_to_consume, p->bytes_consumed, client_reader->read_avail()); | |
| client_reader->consume(bytes_to_consume); | |
| } | |
| switch (event) { | |
| case VC_EVENT_INACTIVITY_TIMEOUT: | |
| case VC_EVENT_ACTIVE_TIMEOUT: | |
| case HTTP_TUNNEL_EVENT_PARSE_ERROR: | |
| if (client_response_hdr_bytes == 0) { | |
| p->handler_state = static_cast<int>(HttpSmPost_t::UA_FAIL); | |
| set_ua_abort(HttpTransact::ABORTED, event); | |
| SMDbg(dbg_ctl_http_tunnel, "send error response to client to vc %p, tunnel vc %p", _ua.get_txn()->get_netvc(), p->vc); | |
| tunnel.chain_abort_all(p); | |
| // Reset the inactivity timeout, otherwise the InactivityCop will callback again in the next second. | |
| _ua.get_txn()->set_inactivity_timeout(HRTIME_SECONDS(t_state.txn_conf->transaction_no_activity_timeout_in)); | |
| // if it is active timeout case, we need to give another chance to send 408 response; | |
| _ua.get_txn()->set_active_timeout(HRTIME_SECONDS(t_state.txn_conf->transaction_active_timeout_in)); | |
| return 0; | |
| } | |
| // fall through | |
| case VC_EVENT_EOS: | |
| // My reading of spec says that user agents can not terminate | |
| // posts with a half close so this is an error | |
| case VC_EVENT_ERROR: | |
| // Did not complete post tunneling. Abort the | |
| // server and close the ua | |
| p->handler_state = static_cast<int>(HttpSmPost_t::UA_FAIL); | |
| set_ua_abort(HttpTransact::ABORTED, event); | |
| tunnel.chain_abort_all(p); | |
| // the in_tunnel status on both the ua & and | |
| // it's consumer must already be set to true. Previously | |
| // we were setting it again to true but incorrectly in | |
| // the case of a transform | |
| hsm_release_assert(_ua.get_entry()->in_tunnel == true); | |
| if (p->consumer_list.head && p->consumer_list.head->vc_type == HttpTunnelType_t::TRANSFORM) { | |
| hsm_release_assert(post_transform_info.entry->in_tunnel == true); | |
| } // server side may have completed before the user agent side, so it may no longer be in tunnel | |
| // In the error case, start to take down the client session. There should | |
| // be no reuse here | |
| vc_table.remove_entry(this->_ua.get_entry()); | |
| _ua.get_txn()->do_io_close(); | |
| break; | |
| case VC_EVENT_READ_COMPLETE: | |
| case HTTP_TUNNEL_EVENT_PRECOMPLETE: | |
| p->handler_state = static_cast<int>(HttpSmPost_t::SUCCESS); | |
| p->read_success = true; | |
| _ua.get_entry()->in_tunnel = false; | |
| if (p->do_dechunking || p->do_chunked_passthru) { | |
| if (p->chunked_handler.truncation) { | |
| tunnel.abort_cache_write_finish_others(p); | |
| } else { | |
| tunnel.local_finish_all(p); | |
| } | |
| } | |
| // Now that we have communicated the post body, turn off the inactivity timeout | |
| // until the server starts sending data back | |
| if (_ua.get_txn()) { | |
| _ua.get_txn()->cancel_inactivity_timeout(); | |
| // Initiate another read to catch aborts | |
| _ua.get_entry()->vc_read_handler = &HttpSM::state_watch_for_client_abort; | |
| _ua.get_entry()->vc_write_handler = &HttpSM::state_watch_for_client_abort; | |
| _ua.get_entry()->read_vio = p->vc->do_io_read(this, INT64_MAX, _ua.get_txn()->get_remote_reader()->mbuf); | |
| } | |
| break; | |
| default: | |
| ink_release_assert(0); | |
| } | |
| return 0; | |
| } | |
| // YTS Team, yamsat Plugin | |
| // Tunnel handler to deallocate the tunnel buffers and | |
| // set redirect_in_process=false | |
| // Copy partial POST data to buffers. Check for the various parameters including | |
| // the maximum configured post data size | |
| int | |
| HttpSM::tunnel_handler_for_partial_post(int event, void * /* data ATS_UNUSED */) | |
| { | |
| STATE_ENTER(tunnel_handler_for_partial_post, event); | |
| tunnel.deallocate_buffers(); | |
| tunnel.reset(); | |
| t_state.redirect_info.redirect_in_process = false; | |
| is_buffering_request_body = false; | |
| if (post_failed) { | |
| post_failed = false; | |
| handle_post_failure(); | |
| } else { | |
| do_setup_client_request_body_tunnel(HttpVC_t::SERVER_VC); | |
| } | |
| return 0; | |
| } | |
| int | |
| HttpSM::tunnel_handler_post_server(int event, HttpTunnelConsumer *c) | |
| { | |
| STATE_ENTER(tunnel_handler_post_server, event); | |
| // If is_using_post_buffer has been used, then this handler gets called | |
| // twice, once with the buffered request body bytes and a second time with | |
| // the (now) zero length user agent buffer. See wait_for_full_body where | |
| // these bytes are read. Don't clobber the server_request_body_bytes with | |
| // zero on that second read. | |
| if (server_request_body_bytes == 0) { | |
| server_request_body_bytes = c->bytes_written; | |
| } | |
| switch (event) { | |
| case VC_EVENT_EOS: | |
| case VC_EVENT_ERROR: | |
| case VC_EVENT_INACTIVITY_TIMEOUT: | |
| case VC_EVENT_ACTIVE_TIMEOUT: | |
| switch (event) { | |
| case VC_EVENT_INACTIVITY_TIMEOUT: | |
| t_state.current.state = HttpTransact::INACTIVE_TIMEOUT; | |
| t_state.set_connect_fail(ETIMEDOUT); | |
| break; | |
| case VC_EVENT_ACTIVE_TIMEOUT: | |
| t_state.current.state = HttpTransact::ACTIVE_TIMEOUT; | |
| t_state.set_connect_fail(ETIMEDOUT); | |
| break; | |
| case VC_EVENT_EOS: | |
| t_state.current.state = HttpTransact::CONNECTION_CLOSED; | |
| t_state.set_connect_fail(EPIPE); | |
| break; | |
| case VC_EVENT_ERROR: | |
| t_state.current.state = HttpTransact::CONNECTION_CLOSED; | |
| t_state.set_connect_fail(server_txn->get_netvc()->lerrno); | |
| break; | |
| default: | |
| break; | |
| } | |
| // Did not complete post tunneling | |
| // | |
| // In the http case, we don't want to close | |
| // the connection because the | |
| // destroys the header buffer which may | |
| // a response even though the tunnel failed. | |
| // Shutdown both sides of the connection. This prevents us | |
| // from getting any further events and signals to client | |
| // that POST data will not be forwarded to the server. Doing | |
| // shutdown on the write side will likely generate a TCP | |
| // reset to the client but if the proxy wasn't here this is | |
| // exactly what would happen. | |
| // we should wait to shutdown read side of the | |
| // client to prevent sending a reset | |
| server_entry->eos = true; | |
| c->vc->do_io_shutdown(IO_SHUTDOWN_WRITE); | |
| // We may be reading from a transform. In that case, we | |
| // want to close the transform | |
| HttpTunnelProducer *ua_producer; | |
| if (c->producer->vc_type == HttpTunnelType_t::TRANSFORM) { | |
| if (c->producer->handler_state == HTTP_SM_TRANSFORM_OPEN) { | |
| ink_assert(c->producer->vc == post_transform_info.vc); | |
| c->producer->vc->do_io_close(); | |
| c->producer->alive = false; | |
| c->producer->self_consumer->alive = false; | |
| } | |
| ua_producer = c->producer->self_consumer->producer; | |
| } else { | |
| ua_producer = c->producer; | |
| } | |
| ink_assert(ua_producer->vc_type == HttpTunnelType_t::HTTP_CLIENT); | |
| ink_assert(ua_producer->vc == _ua.get_txn()); | |
| ink_assert(ua_producer->vc == _ua.get_entry()->vc); | |
| // Before shutting down, initiate another read | |
| // on the user agent in order to get timeouts | |
| // coming to the state machine and not the tunnel | |
| _ua.get_entry()->vc_read_handler = &HttpSM::state_watch_for_client_abort; | |
| _ua.get_entry()->vc_write_handler = &HttpSM::state_watch_for_client_abort; | |
| // YTS Team, yamsat Plugin | |
| // When event is VC_EVENT_ERROR,and when redirection is enabled | |
| // do not shut down the client read | |
| if (enable_redirection) { | |
| if (ua_producer->vc_type == HttpTunnelType_t::STATIC && event != VC_EVENT_ERROR && event != VC_EVENT_EOS) { | |
| _ua.get_entry()->read_vio = ua_producer->vc->do_io_read(this, INT64_MAX, _ua.get_txn()->get_remote_reader()->mbuf); | |
| // ua_producer->vc->do_io_shutdown(IO_SHUTDOWN_READ); | |
| } else { | |
| if (ua_producer->vc_type == HttpTunnelType_t::STATIC && t_state.redirect_info.redirect_in_process) { | |
| post_failed = true; | |
| } | |
| } | |
| } else { | |
| _ua.get_entry()->read_vio = ua_producer->vc->do_io_read(this, INT64_MAX, _ua.get_txn()->get_remote_reader()->mbuf); | |
| // we should not shutdown read side of the client here to prevent sending a reset | |
| // ua_producer->vc->do_io_shutdown(IO_SHUTDOWN_READ); | |
| } // end of added logic | |
| // We want to shutdown the tunnel here and see if there | |
| // is a response on from the server. Mark the user | |
| // agent as down so that tunnel concludes. | |
| ua_producer->alive = false; | |
| ua_producer->handler_state = static_cast<int>(HttpSmPost_t::SERVER_FAIL); | |
| ink_assert(tunnel.is_tunnel_alive() == false); | |
| break; | |
| case VC_EVENT_WRITE_COMPLETE: { | |
| // Completed successfully | |
| c->write_success = true; | |
| server_entry->in_tunnel = false; | |
| // A tunnel can complete having written fewer bytes than the request head declared, so | |
| // the debt is cleared on the byte count rather than on the event alone. Only a body | |
| // matching the framing ATS declared leaves the origin able to parse a following request. | |
| // | |
| // Both routes to a short write here are narrow. A half close makes HttpTunnel clamp the | |
| // consumer write to whatever had already arrived, but the only trigger for that flag | |
| // before the request body tunnel starts is a plugin calling TSHttpTxnCloseAfterResponse. | |
| // A client FIN is not one: it aborts the tunnel in tunnel_handler_post_ua, so this | |
| // consumer never completes. A truncated chunked body sets chunked_handler.truncation, | |
| // which reaches this event only when producer_handler_chunked sees a completion with the | |
| // body unfinished. Neither route is exercised by post-early-response-100-continue.test.py, | |
| // so the check rests on the code path above rather than on a regression test; it is kept | |
| // because clearing the debt on a short body is exactly what pools a connection the | |
| // origin is still parsing. | |
| // | |
| // The length has to come from server_request rather than from the client's | |
| // request_content_length. A request body transform rewrites the outbound | |
| // Content-Length to the transformed size and leaves the client's value alone, so | |
| // comparing against the client's would hold the debt open for every transformed body | |
| // that came out smaller than it went in. | |
| // A null producer should not be read as a complete body, because the debt must only be | |
| // cleared on positive evidence that the origin got what it was promised. | |
| bool const body_truncated = c->producer == nullptr || c->producer->chunked_handler.truncation; | |
| bool body_complete = !body_truncated; | |
| int64_t declared_bytes = HTTP_UNDEFINED_CL; | |
| // Note the truncation flag above is only set by HttpTunnel::producer_handler_chunked(), | |
| // so it covers dechunking and chunked passthrough. When ATS is the one adding chunking | |
| // the framing is correct by construction, because generate_chunked_content() emits the | |
| // terminating chunk even for a body cut short. A chunked request carries no declared | |
| // length either way, so there is no byte count to check against. | |
| if (t_state.hdr_info.server_request.presence(MIME_PRESENCE_CONTENT_LENGTH)) { | |
| declared_bytes = t_state.hdr_info.server_request.get_content_length(); | |
| } | |
| if (body_complete && declared_bytes != HTTP_UNDEFINED_CL) { | |
| // Count from the consumer rather than from server_request_body_bytes. That member is | |
| // first write wins and is never re-armed, so on a retry it still holds the previous | |
| // attempt's total and would clear the debt for a second attempt that fell short. | |
| // HttpTunnel::reset() zeroes the consumers, so this count is per attempt. | |
| body_complete = c->bytes_written >= declared_bytes; | |
| } | |
| if (body_complete) { | |
| server_owed_request_body = false; | |
| } else if (server_owed_request_body) { | |
| SMDbg(dbg_ctl_http_tproxy, "Post tunnel completed but origin is still owed body bytes, wrote %" PRId64 " of %" PRId64, | |
| c->bytes_written, declared_bytes); | |
| } | |
| break; | |
| } | |
| default: | |
| ink_release_assert(0); | |
| } | |
| return 0; | |
| } | |
| int | |
| HttpSM::tunnel_handler_ssl_producer(int event, HttpTunnelProducer *p) | |
| { | |
| STATE_ENTER(tunnel_handler_ssl_producer, event); | |
| switch (event) { | |
| case VC_EVENT_READ_READY: | |
| // This event is triggered when receiving DATA frames without the END_STREAM | |
| // flag set in a HTTP/2 CONNECT request. Breaking as there are more DATA | |
| // frames to come. | |
| break; | |
| case VC_EVENT_READ_COMPLETE: | |
| // This event is triggered during an HTTP/2 CONNECT request when a DATA | |
| // frame with the END_STREAM flag set is received, indicating the end of the | |
| // stream. | |
| [[fallthrough]]; | |
| case VC_EVENT_EOS: | |
| // The write side of this connection is still alive | |
| // so half-close the read | |
| if (p->self_consumer->alive) { | |
| p->vc->do_io_shutdown(IO_SHUTDOWN_READ); | |
| tunnel.local_finish_all(p); | |
| break; | |
| } | |
| // FALL THROUGH - both sides of the tunnel are dea | |
| case VC_EVENT_ERROR: | |
| case VC_EVENT_INACTIVITY_TIMEOUT: | |
| case VC_EVENT_ACTIVE_TIMEOUT: | |
| // Set squid code for tunnel active timeout | |
| if (event == VC_EVENT_ACTIVE_TIMEOUT) { | |
| t_state.squid_codes.log_code = SquidLogCode::ERR_TUN_ACTIVE_TIMEOUT; | |
| } | |
| // The other side of the connection is either already dead | |
| // or rendered inoperative by the error on the connection | |
| // Note: use tunnel close vc so the tunnel knows we are | |
| // nuking the of the connection as well | |
| tunnel.close_vc(p); | |
| tunnel.local_finish_all(p); | |
| // Because we've closed the net vc this error came in, it's write | |
| // direction is now dead as well. If that side still being fed data, | |
| // we need to kill that pipe as well | |
| if (p->self_consumer->producer->alive) { | |
| p->self_consumer->producer->alive = false; | |
| if (p->self_consumer->producer->self_consumer->alive) { | |
| p->self_consumer->producer->vc->do_io_shutdown(IO_SHUTDOWN_READ); | |
| } else { | |
| tunnel.close_vc(p->self_consumer->producer); | |
| } | |
| } | |
| break; | |
| case HTTP_TUNNEL_EVENT_PRECOMPLETE: | |
| // We should never get these event since we don't know | |
| // how long the stream is | |
| default: | |
| ink_release_assert(0); | |
| } | |
| // Update stats | |
| switch (p->vc_type) { | |
| case HttpTunnelType_t::HTTP_SERVER: | |
| server_response_body_bytes += p->bytes_read; | |
| break; | |
| case HttpTunnelType_t::HTTP_CLIENT: | |
| client_request_body_bytes += p->bytes_read; | |
| break; | |
| default: | |
| // Covered here: | |
| // HttpTunnelType_t::CACHE_READ, HttpTunnelType_t::CACHE_WRITE, | |
| // HttpTunnelType_t::TRANSFORM, HttpTunnelType_t::STATIC. | |
| break; | |
| } | |
| return 0; | |
| } | |
| int | |
| HttpSM::tunnel_handler_ssl_consumer(int event, HttpTunnelConsumer *c) | |
| { | |
| STATE_ENTER(tunnel_handler_ssl_consumer, event); | |
| switch (event) { | |
| case VC_EVENT_ERROR: | |
| case VC_EVENT_EOS: | |
| case VC_EVENT_INACTIVITY_TIMEOUT: | |
| case VC_EVENT_ACTIVE_TIMEOUT: | |
| // Set squid code for tunnel active timeout | |
| if (event == VC_EVENT_ACTIVE_TIMEOUT) { | |
| t_state.squid_codes.log_code = SquidLogCode::ERR_TUN_ACTIVE_TIMEOUT; | |
| } | |
| // we need to mark the producer dead | |
| // otherwise it can stay alive forever. | |
| if (c->producer->alive) { | |
| c->producer->alive = false; | |
| if (c->producer->self_consumer->alive) { | |
| c->producer->vc->do_io_shutdown(IO_SHUTDOWN_READ); | |
| } else { | |
| tunnel.close_vc(c->producer); | |
| } | |
| } | |
| // Since we are changing the state of the self_producer | |
| // we must have the tunnel shutdown the vc | |
| tunnel.close_vc(c); | |
| tunnel.local_finish_all(c->self_producer); | |
| break; | |
| case VC_EVENT_WRITE_COMPLETE: | |
| // If we get this event, it means that the producer | |
| // has finished and we wrote the remaining data | |
| // to the consumer | |
| // | |
| // If the read side of this connection has not yet | |
| // closed, do a write half-close and then wait for | |
| // read side to close so that we don't cut off | |
| // pipelined responses with TCP resets | |
| // | |
| // ink_assert(c->producer->alive == false); | |
| c->write_success = true; | |
| if (c->self_producer->alive == true) { | |
| c->vc->do_io_shutdown(IO_SHUTDOWN_WRITE); | |
| } else { | |
| c->vc->do_io_close(); | |
| } | |
| break; | |
| default: | |
| ink_release_assert(0); | |
| } | |
| // Update stats | |
| switch (c->vc_type) { | |
| case HttpTunnelType_t::HTTP_SERVER: | |
| server_request_body_bytes += c->bytes_written; | |
| break; | |
| case HttpTunnelType_t::HTTP_CLIENT: | |
| client_response_body_bytes += c->bytes_written; | |
| break; | |
| default: | |
| // Handled here: | |
| // HttpTunnelType_t::CACHE_READ, HttpTunnelType_t::CACHE_WRITE, HttpTunnelType_t::TRANSFORM, | |
| // HttpTunnelType_t::STATIC | |
| break; | |
| } | |
| return 0; | |
| } | |
| int | |
| HttpSM::tunnel_handler_transform_write(int event, HttpTunnelConsumer *c) | |
| { | |
| STATE_ENTER(tunnel_handler_transform_write, event); | |
| HttpTransformInfo *i; | |
| // Figure out if this the request or response transform | |
| // : use post_transform_info.entry because post_transform_info.vc | |
| // is not set to NULL after the post transform is done. | |
| if (post_transform_info.entry && post_transform_info.entry->vc == c->vc) { | |
| i = &post_transform_info; | |
| } else { | |
| i = &transform_info; | |
| ink_assert(c->vc == i->vc); | |
| ink_assert(c->vc == i->entry->vc); | |
| } | |
| switch (event) { | |
| case VC_EVENT_ERROR: | |
| // Transform error | |
| tunnel.chain_abort_all(c->producer); | |
| c->handler_state = HTTP_SM_TRANSFORM_FAIL; | |
| c->vc->do_io_close(EHTTP_ERROR); | |
| break; | |
| case VC_EVENT_EOS: | |
| // It possible the transform quit | |
| // before the producer finished. If this is true | |
| // we need shut down the producer if it doesn't | |
| // have other consumers to serve or else it will | |
| // fill up buffer and get hung | |
| if (c->producer->alive && c->producer->num_consumers == 1) { | |
| // Send a tunnel detach event to the producer | |
| // to shut it down but indicates it should not abort | |
| // downstream (on the other side of the transform) | |
| // cache writes | |
| tunnel.producer_handler(HTTP_TUNNEL_EVENT_CONSUMER_DETACH, c->producer); | |
| } | |
| // FALLTHROUGH | |
| case VC_EVENT_WRITE_COMPLETE: | |
| // write to transform complete - shutdown the write side | |
| c->write_success = true; | |
| c->vc->do_io_shutdown(IO_SHUTDOWN_WRITE); | |
| // If the read side has not started up yet, then the | |
| // this transform_vc is no longer owned by the tunnel | |
| if (c->self_producer == nullptr) { | |
| i->entry->in_tunnel = false; | |
| } else if (c->self_producer->alive == false) { | |
| // The read side of the Transform | |
| // has already completed (possible when the | |
| // transform intentionally truncates the response). | |
| // So close it | |
| c->vc->do_io_close(); | |
| } | |
| break; | |
| default: | |
| ink_release_assert(0); | |
| } | |
| // attribute the size written to the transform from various sources | |
| // NOTE: the range transform is excluded from this accounting and | |
| // is instead handled in HttpSM::tunnel_handler_ua | |
| // | |
| // the reasoning is that the range transform is internal functionality | |
| // in support of HTTP 1.1 compliance, therefore part of "normal" operation | |
| // all other transforms are plugin driven and the difference between | |
| // source data and final data should represent the transformation delta | |
| // | |
| if (t_state.range_setup == HttpTransact::RangeSetup_t::NONE) { | |
| switch (t_state.pre_transform_source) { | |
| case HttpTransact::Source_t::HTTP_ORIGIN_SERVER: | |
| server_response_body_bytes = client_response_body_bytes; | |
| break; | |
| case HttpTransact::Source_t::CACHE: | |
| cache_response_body_bytes = client_response_body_bytes; | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| return 0; | |
| } | |
| int | |
| HttpSM::tunnel_handler_transform_read(int event, HttpTunnelProducer *p) | |
| { | |
| STATE_ENTER(tunnel_handler_transform_read, event); | |
| ink_assert(p->vc == transform_info.vc || p->vc == post_transform_info.vc); | |
| switch (event) { | |
| case VC_EVENT_ERROR: | |
| // Transform error | |
| tunnel.chain_abort_all(p->self_consumer->producer); | |
| break; | |
| case VC_EVENT_EOS: | |
| // If we did not get enough data from the transform abort the | |
| // cache write otherwise fallthrough to the transform | |
| // completing successfully | |
| if (t_state.hdr_info.transform_response_cl != HTTP_UNDEFINED_CL && | |
| p->read_vio->nbytes < t_state.hdr_info.transform_response_cl) { | |
| tunnel.abort_cache_write_finish_others(p); | |
| break; | |
| } | |
| // FALL-THROUGH | |
| case VC_EVENT_READ_COMPLETE: | |
| case HTTP_TUNNEL_EVENT_PRECOMPLETE: | |
| // Transform complete | |
| p->read_success = true; | |
| tunnel.local_finish_all(p); | |
| break; | |
| default: | |
| ink_release_assert(0); | |
| } | |
| // it's possible that the write side of the | |
| // transform hasn't detached yet. If it is still alive, | |
| // don't close the transform vc | |
| if (p->self_consumer->alive == false) { | |
| Metrics::Counter::increment(http_rsb.origin_shutdown_tunnel_transform_read); | |
| p->vc->do_io_close(); | |
| } | |
| p->handler_state = HTTP_SM_TRANSFORM_CLOSED; | |
| return 0; | |
| } | |
| int | |
| HttpSM::tunnel_handler_plugin_agent(int event, HttpTunnelConsumer *c) | |
| { | |
| STATE_ENTER(tunnel_handler_plugin_agent, event); | |
| switch (event) { | |
| case VC_EVENT_ERROR: | |
| c->vc->do_io_close(EHTTP_ERROR); // close up | |
| // Signal producer if we're the last consumer. | |
| if (c->producer->alive && c->producer->num_consumers == 1) { | |
| tunnel.producer_handler(HTTP_TUNNEL_EVENT_CONSUMER_DETACH, c->producer); | |
| } | |
| break; | |
| case VC_EVENT_EOS: | |
| if (c->producer->alive && c->producer->num_consumers == 1) { | |
| tunnel.producer_handler(HTTP_TUNNEL_EVENT_CONSUMER_DETACH, c->producer); | |
| } | |
| // FALLTHROUGH | |
| case VC_EVENT_WRITE_COMPLETE: | |
| c->write_success = true; | |
| c->vc->do_io_close(); | |
| break; | |
| default: | |
| ink_release_assert(0); | |
| } | |
| return 0; | |
| } | |
| int | |
| HttpSM::state_remap_request(int event, void * /* data ATS_UNUSED */) | |
| { | |
| STATE_ENTER(state_remap_request, event); | |
| switch (event) { | |
| case EVENT_REMAP_ERROR: { | |
| ink_assert(!"this doesn't happen"); | |
| pending_action = nullptr; | |
| Error("error remapping request [see previous errors]"); | |
| call_transact_and_set_next_state(HttpTransact::HandleRequest); // HandleRequest skips EndRemapRequest | |
| break; | |
| } | |
| case EVENT_REMAP_COMPLETE: { | |
| pending_action = nullptr; | |
| SMDbg(dbg_ctl_url_rewrite, "completed processor-based remapping request"); | |
| t_state.url_remap_success = remapProcessor.finish_remap(&t_state, m_remap.get()); | |
| call_transact_and_set_next_state(nullptr); | |
| break; | |
| } | |
| default: | |
| ink_assert(!"Unexpected event inside state_remap_request"); | |
| break; | |
| } | |
| return 0; | |
| } | |
| // This check must be called before remap. Otherwise, the client_request host | |
| // name may be changed. | |
| void | |
| HttpSM::check_sni_host() | |
| { | |
| // Check that the SNI and host name fields match, if it matters | |
| // Issue warning or mark the transaction to be terminated as necessary | |
| auto host_name{t_state.hdr_info.client_request.host_get()}; | |
| auto host_len{static_cast<int>(host_name.length())}; | |
| if (host_name.empty()) { | |
| return; | |
| } | |
| auto *netvc = _ua.get_txn()->get_netvc(); | |
| if (netvc == nullptr) { | |
| return; | |
| } | |
| auto *snis = netvc->get_service<TLSSNISupport>(); | |
| if (snis == nullptr) { | |
| return; | |
| } | |
| int host_sni_policy = static_cast<unsigned char>(t_state.http_config_param->http_host_sni_policy); | |
| if (snis->would_have_actions_for(std::string{host_name}.c_str(), netvc->get_remote_endpoint(), host_sni_policy) && | |
| host_sni_policy > 0) { | |
| // In a SNI/Host mismatch where the Host would have triggered SNI policy, mark the transaction | |
| // to be considered for rejection after the remap phase passes. Gives the opportunity to conf_remap | |
| // override the policy to be rejected in the end_remap logic | |
| const char *sni_value = snis->get_sni_server_name(); | |
| const char *action_value = host_sni_policy == 2 ? "terminate" : "continue"; | |
| if (!sni_value || sni_value[0] == '\0') { // No SNI | |
| Warning("No SNI for TLS request with hostname %.*s action=%s", host_len, host_name.data(), action_value); | |
| SMDbg(dbg_ctl_ssl_sni, "No SNI for TLS request with hostname %.*s action=%s", host_len, host_name.data(), action_value); | |
| if (host_sni_policy == 2) { | |
| swoc::bwprint(error_bw_buffer, "No SNI for TLS request: connecting to {} for host='{}', returning a 403", | |
| t_state.client_info.dst_addr, host_name); | |
| Log::error("%s", error_bw_buffer.c_str()); | |
| this->t_state.client_connection_allowed = false; | |
| } | |
| } else if (strlen(sni_value) != static_cast<size_t>(host_len) || | |
| strncasecmp(host_name.data(), sni_value, host_len) != 0) { // Name mismatch | |
| Warning("SNI/hostname mismatch sni=%s host=%.*s action=%s", sni_value, host_len, host_name.data(), action_value); | |
| SMDbg(dbg_ctl_ssl_sni, "SNI/hostname mismatch sni=%s host=%.*s action=%s", sni_value, host_len, host_name.data(), | |
| action_value); | |
| if (host_sni_policy == 2) { | |
| swoc::bwprint(error_bw_buffer, "SNI/hostname mismatch: connecting to {} for host='{}' sni='{}', returning a 403", | |
| t_state.client_info.dst_addr, host_name, sni_value); | |
| Log::error("%s", error_bw_buffer.c_str()); | |
| this->t_state.client_connection_allowed = false; | |
| } | |
| } else { | |
| SMDbg(dbg_ctl_ssl_sni, "SNI/hostname successfully match sni=%s host=%.*s", sni_value, host_len, host_name.data()); | |
| } | |
| } else { | |
| SMDbg(dbg_ctl_ssl_sni, "No SNI/hostname check configured for host=%.*s", host_len, host_name.data()); | |
| } | |
| } | |
| void | |
| HttpSM::set_virtualhost_entry(std::string_view domain) | |
| { | |
| VirtualHost::scoped_config vhost_config; | |
| // If already set, don't need to look at configs | |
| if (m_virtualhost_entry || domain.empty() || !vhost_config) { | |
| return; | |
| } | |
| auto vhost_entry = vhost_config->find_by_domain(domain); | |
| if (vhost_entry) { | |
| SMDbg(dbg_ctl_url_rewrite, "Found virtualhost: %s", vhost_entry->get_id().c_str()); | |
| m_virtualhost_entry = std::move(vhost_entry); | |
| } | |
| } | |
| void | |
| HttpSM::do_remap_request(bool run_inline) | |
| { | |
| SMDbg(dbg_ctl_http_seq, "Remapping request"); | |
| SMDbg(dbg_ctl_url_rewrite, "Starting a possible remapping for request"); | |
| if (!m_virtualhost_entry) { | |
| auto host_name{t_state.hdr_info.client_request.host_get()}; | |
| set_virtualhost_entry(host_name); | |
| } | |
| // Check virtualhost remap rules before looking at remap.config. Copying the shared_ptr pins the | |
| // table for the life of this transaction, so a reload that drops the entry cannot pull the table | |
| // out from under us mid-transaction. | |
| bool virtualhost_remap = false; | |
| if (m_virtualhost_entry && m_virtualhost_entry->remap_table) { | |
| m_remap = m_virtualhost_entry->remap_table; | |
| virtualhost_remap = true; | |
| SMDbg(dbg_ctl_url_rewrite, "Using virtualhost remap table: %s", m_virtualhost_entry->get_id().c_str()); | |
| } | |
| bool ret = remapProcessor.setup_for_remap(&t_state, m_remap.get()); | |
| // If no remap matches in virtualhost, revert to default remap configs | |
| if (!ret && virtualhost_remap) { | |
| SMDbg(dbg_ctl_url_rewrite, "No virtualhost remap rules found: using global remap table"); | |
| // May be null once shutdown has cleared the table; setup_for_remap() handles that. | |
| m_remap = rewrite_table.load(std::memory_order_acquire); | |
| ret = remapProcessor.setup_for_remap(&t_state, m_remap.get()); | |
| } | |
| check_sni_host(); | |
| if (!ret) { | |
| SMDbg(dbg_ctl_url_rewrite, "Could not find a valid remapping entry for this request"); | |
| Metrics::Counter::increment(http_rsb.no_remap_matched); | |
| if (!run_inline) { | |
| handleEvent(EVENT_REMAP_COMPLETE, nullptr); | |
| } | |
| return; | |
| } | |
| SMDbg(dbg_ctl_url_rewrite, "Found a remap map entry, attempting to remap request and call any plugins"); | |
| pending_action = remapProcessor.perform_remap(this, &t_state); | |
| return; | |
| } | |
| void | |
| HttpSM::do_hostdb_lookup() | |
| { | |
| ink_assert(t_state.dns_info.lookup_name != nullptr); | |
| ink_assert(pending_action.empty()); | |
| ATS_PROBE1(milestone_dns_lookup_begin, sm_id); | |
| milestones[TS_MILESTONE_DNS_LOOKUP_BEGIN] = ink_get_hrtime(); | |
| // If directed to not look up fqdns then mark as resolved | |
| if (t_state.txn_conf->no_dns_forward_to_parent && t_state.parent_result.result == ParentResultType::UNDEFINED) { | |
| // resolved_p documents that dns_info.addr holds a valid resolved address. | |
| // We are not actually resolving anything here, so set addr to INADDR_ANY | |
| // so the ats_is_ip_any check in HttpTransact::OSDNSLookup produces a | |
| // clean Bad Request response rather than reusing leftover State data. | |
| ats_ip4_set(&t_state.dns_info.addr, INADDR_ANY, 0); | |
| t_state.dns_info.resolved_p = true; | |
| call_transact_and_set_next_state(nullptr); | |
| return; | |
| } else if (t_state.txn_conf->srv_enabled) { | |
| char d[MAXDNAME]; | |
| // Look at the next_hop_scheme to determine what scheme to put in the SRV lookup | |
| unsigned int scheme_len = snprintf(d, sizeof(d), "_%s._tcp.", hdrtoken_index_to_wks(t_state.next_hop_scheme)); | |
| ink_strlcpy(d + scheme_len, t_state.server_info.name, sizeof(d) - scheme_len); | |
| SMDbg(dbg_ctl_dns_srv, "Beginning lookup of SRV records for origin %s", d); | |
| HostDBProcessor::Options opt; | |
| if (t_state.api_txn_dns_timeout_value != -1) { | |
| opt.timeout = t_state.api_txn_dns_timeout_value; | |
| } | |
| pending_action = hostDBProcessor.getSRVbyname_imm(this, (cb_process_result_pfn)&HttpSM::process_srv_info, d, 0, opt); | |
| if (pending_action.empty()) { | |
| char const *host_name = t_state.dns_info.is_srv() ? t_state.dns_info.srv_hostname : t_state.dns_info.lookup_name; | |
| opt.port = t_state.dns_info.is_srv() ? t_state.dns_info.srv_port : | |
| t_state.server_info.dst_addr.isValid() ? t_state.server_info.dst_addr.host_order_port() : | |
| t_state.hdr_info.client_request.port_get(); | |
| opt.flags = (t_state.cache_info.directives.does_client_permit_dns_storing) ? HostDBProcessor::HOSTDB_DO_NOT_FORCE_DNS : | |
| HostDBProcessor::HOSTDB_FORCE_DNS_RELOAD; | |
| opt.timeout = (t_state.api_txn_dns_timeout_value != -1) ? t_state.api_txn_dns_timeout_value : 0; | |
| opt.host_res_style = | |
| ats_host_res_from(_ua.get_txn()->get_netvc()->get_local_addr()->sa_family, t_state.txn_conf->host_res_data.order); | |
| pending_action = hostDBProcessor.getbyname_imm(this, (cb_process_result_pfn)&HttpSM::process_hostdb_info, host_name, 0, opt); | |
| if (pending_action.empty()) { | |
| call_transact_and_set_next_state(nullptr); | |
| } | |
| } | |
| return; | |
| } else { /* we aren't using SRV stuff... */ | |
| SMDbg(dbg_ctl_http_seq, "Doing DNS Lookup"); | |
| // If there is not a current server, we must be looking up the origin | |
| // server at the beginning of the transaction | |
| int server_port = 0; | |
| if (t_state.current.server && t_state.current.server->dst_addr.isValid()) { | |
| server_port = t_state.current.server->dst_addr.host_order_port(); | |
| } else if (t_state.server_info.dst_addr.isValid()) { | |
| server_port = t_state.server_info.dst_addr.host_order_port(); | |
| } else { | |
| server_port = t_state.hdr_info.client_request.port_get(); | |
| } | |
| if (t_state.api_txn_dns_timeout_value != -1) { | |
| SMDbg(dbg_ctl_http_timeout, "beginning DNS lookup. allowing %d mseconds for DNS lookup", t_state.api_txn_dns_timeout_value); | |
| } | |
| HostDBProcessor::Options opt; | |
| opt.port = server_port; | |
| opt.flags = (t_state.cache_info.directives.does_client_permit_dns_storing) ? HostDBProcessor::HOSTDB_DO_NOT_FORCE_DNS : | |
| HostDBProcessor::HOSTDB_FORCE_DNS_RELOAD; | |
| opt.timeout = (t_state.api_txn_dns_timeout_value != -1) ? t_state.api_txn_dns_timeout_value : 0; | |
| opt.host_res_style = | |
| ats_host_res_from(_ua.get_txn()->get_netvc()->get_local_addr()->sa_family, t_state.txn_conf->host_res_data.order); | |
| pending_action = hostDBProcessor.getbyname_imm(this, (cb_process_result_pfn)&HttpSM::process_hostdb_info, | |
| t_state.dns_info.lookup_name, 0, opt); | |
| if (pending_action.empty()) { | |
| call_transact_and_set_next_state(nullptr); | |
| } | |
| return; | |
| } | |
| ink_assert(!"not reached"); | |
| return; | |
| } | |
| void | |
| HttpSM::do_hostdb_reverse_lookup() | |
| { | |
| ink_assert(t_state.dns_info.lookup_name != nullptr); | |
| ink_assert(pending_action.empty()); | |
| SMDbg(dbg_ctl_http_seq, "Doing reverse DNS Lookup"); | |
| IpEndpoint addr; | |
| ats_ip_pton(t_state.dns_info.lookup_name, &addr.sa); | |
| pending_action = hostDBProcessor.getbyaddr_re(this, &addr.sa); | |
| return; | |
| } | |
| bool | |
| HttpSM::track_connect_fail() const | |
| { | |
| int const policy = t_state.txn_conf->connect_down_policy; | |
| // Policy 1: any TCP-level connect error (excluding TLS handshake failures). | |
| if (policy == 1 && t_state.current.server->had_connect_fail()) { | |
| return t_state.current.server->connect_result != -ENET_SSL_CONNECT_FAILED; | |
| } | |
| // Policy 2+: any connect error including TLS handshake failures. | |
| if (policy >= 2 && t_state.current.server->had_connect_fail()) { | |
| return true; | |
| } | |
| // Policy 3+: inactive timeout (connect_result was cleared at CONNECTION_ALIVE). | |
| if (policy >= 3 && t_state.current.server->state == HttpTransact::INACTIVE_TIMEOUT) { | |
| return true; | |
| } | |
| // Policy 4+: origin closed a fresh connection before sending any response bytes. | |
| // Excludes two cases: | |
| // - Reused keep-alive connection: there is a known race between ATS reusing and the origin closing it. | |
| // - Multiplexed origins (HTTP/2): stream-level failure does not indicate a connection failure. | |
| if (policy >= 4) { | |
| bool multiplexed = false; | |
| auto ssn = server_txn->get_proxy_ssn(); | |
| if (ssn != nullptr) { | |
| multiplexed = static_cast<PoolableSession *>(ssn)->is_multiplexing(); | |
| } | |
| if (!multiplexed && server_txn->is_first_transaction() && server_response_hdr_bytes == 0) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| void | |
| HttpSM::do_hostdb_update_if_necessary() | |
| { | |
| if (t_state.current.server == nullptr || plugin_tunnel_type != HttpPluginTunnel_t::NONE || t_state.dns_info.active == nullptr) { | |
| // No server, so update is not necessary | |
| return; | |
| } | |
| if (t_state.updated_server_version != HTTP_INVALID) { | |
| // we may have incorrectly assumed that the hostdb had the wrong version of | |
| // http for the server because our first few connect attempts to the server | |
| // failed, causing us to downgrade our requests to a lower version and changing | |
| // our information about the server version. | |
| // | |
| // This test therefore just issues the update only if the hostdb version is | |
| // in fact different from the version we want the value to be updated to. | |
| t_state.updated_server_version = HTTP_INVALID; | |
| t_state.dns_info.active->http_version = t_state.updated_server_version; | |
| } | |
| char addrbuf[INET6_ADDRPORTSTRLEN]; | |
| SMDbg(dbg_ctl_http, "update hostdb info: %s", ats_ip_nptop(&t_state.current.server->dst_addr.sa, addrbuf, sizeof(addrbuf))); | |
| // Check to see if we need to report or clear a connection failure | |
| if (track_connect_fail()) { | |
| this->mark_host_failure(&t_state.dns_info, ts_clock::now()); | |
| } else { | |
| if (t_state.dns_info.mark_active_server_up()) { | |
| char addrbuf[INET6_ADDRPORTSTRLEN]; | |
| ats_ip_nptop(&t_state.current.server->dst_addr.sa, addrbuf, sizeof(addrbuf)); | |
| ATS_PROBE2(mark_active_server_up, sm_id, addrbuf); | |
| if (t_state.dns_info.record->is_srv()) { | |
| SMDbg(dbg_ctl_http, "[%" PRId64 "] hostdb update marking SRV: %s(%s) as up", sm_id, t_state.dns_info.record->name(), | |
| addrbuf); | |
| } else { | |
| SMDbg(dbg_ctl_http, "[%" PRId64 "] hostdb update marking IP: %s as up", sm_id, addrbuf); | |
| } | |
| } | |
| } | |
| return; | |
| } | |
| /* | |
| * range entry valid [a,b] (a >= 0 and b >= 0 and a <= b) | |
| * HttpTransact::RangeSetup_t::NONE if the content length of cached copy is zero or | |
| * no range entry | |
| * HttpTransact::RangeSetup_t::NOT_SATISFIABLE iff all range entries are valid but | |
| * none overlap the current extent of the cached copy | |
| * HttpTransact::RangeSetup_t::NOT_HANDLED if out-of-order Range entries or | |
| * the cached copy`s content_length is INT64_MAX (e.g. read_from_writer and trunked) | |
| * HttpTransact::RangeSetup_t::REQUESTED if all sub range entries are valid and | |
| * in order (remove the entries that not overlap the extent of cache copy) | |
| */ | |
| void | |
| HttpSM::parse_range_and_compare(MIMEField *field, int64_t content_length) | |
| { | |
| int prev_good_range = -1; | |
| const char *value; | |
| int value_len; | |
| int n_values; | |
| int nr = 0; // number of valid ranges, also index to range array. | |
| int not_satisfy = 0; | |
| HdrCsvIter csv; | |
| const char *s, *e, *tmp; | |
| RangeRecord *ranges = nullptr; | |
| int64_t start, end; | |
| ink_assert(field != nullptr && t_state.range_setup == HttpTransact::RangeSetup_t::NONE && t_state.ranges == nullptr); | |
| if (content_length <= 0) { | |
| return; | |
| } | |
| // ToDo: Can this really happen? | |
| if (content_length == INT64_MAX) { | |
| t_state.range_setup = HttpTransact::RangeSetup_t::NOT_HANDLED; | |
| return; | |
| } | |
| if (parse_range_done) { | |
| SMDbg(dbg_ctl_http_range, "parse_range already done, t_state.range_setup %d", static_cast<int>(t_state.range_setup)); | |
| return; | |
| } | |
| parse_range_done = true; | |
| n_values = 0; | |
| value = csv.get_first(field, &value_len); | |
| while (value) { | |
| ++n_values; | |
| value = csv.get_next(&value_len); | |
| } | |
| value = csv.get_first(field, &value_len); | |
| if (n_values <= 0 || ptr_len_ncmp(value, value_len, "bytes=", 6)) { | |
| return; | |
| } | |
| ranges = new RangeRecord[n_values]; | |
| value += 6; // skip leading 'bytes=' | |
| value_len -= 6; | |
| // assume range_in_cache | |
| t_state.range_in_cache = true; | |
| for (; value; value = csv.get_next(&value_len)) { | |
| if (!(tmp = static_cast<const char *>(memchr(value, '-', value_len)))) { | |
| t_state.range_setup = HttpTransact::RangeSetup_t::NONE; | |
| goto Lfaild; | |
| } | |
| // process start value | |
| s = value; | |
| e = tmp; | |
| // skip leading white spaces | |
| for (; s < e && ParseRules::is_ws(*s); ++s) { | |
| ; | |
| } | |
| if (s >= e) { | |
| start = -1; | |
| } else { | |
| for (start = 0; s < e && *s >= '0' && *s <= '9'; ++s) { | |
| // check the int64 overflow in case of high gcc with O3 option | |
| // thinking the start is always positive | |
| int64_t new_start = start * 10 + (*s - '0'); | |
| if (new_start < start) { // Overflow | |
| t_state.range_setup = HttpTransact::RangeSetup_t::NONE; | |
| goto Lfaild; | |
| } | |
| start = new_start; | |
| } | |
| // skip last white spaces | |
| for (; s < e && ParseRules::is_ws(*s); ++s) { | |
| ; | |
| } | |
| if (s < e) { | |
| t_state.range_setup = HttpTransact::RangeSetup_t::NONE; | |
| goto Lfaild; | |
| } | |
| } | |
| // process end value | |
| s = tmp + 1; | |
| e = value + value_len; | |
| // skip leading white spaces | |
| for (; s < e && ParseRules::is_ws(*s); ++s) { | |
| ; | |
| } | |
| if (s >= e) { | |
| if (start < 0) { | |
| t_state.range_setup = HttpTransact::RangeSetup_t::NONE; | |
| goto Lfaild; | |
| } else if (start >= content_length) { | |
| not_satisfy++; | |
| continue; | |
| } | |
| end = content_length - 1; | |
| } else { | |
| for (end = 0; s < e && *s >= '0' && *s <= '9'; ++s) { | |
| // check the int64 overflow in case of high gcc with O3 option | |
| // thinking the start is always positive | |
| int64_t new_end = end * 10 + (*s - '0'); | |
| if (new_end < end) { // Overflow | |
| t_state.range_setup = HttpTransact::RangeSetup_t::NONE; | |
| goto Lfaild; | |
| } | |
| end = new_end; | |
| } | |
| // skip last white spaces | |
| for (; s < e && ParseRules::is_ws(*s); ++s) { | |
| ; | |
| } | |
| if (s < e) { | |
| t_state.range_setup = HttpTransact::RangeSetup_t::NONE; | |
| goto Lfaild; | |
| } | |
| if (start < 0) { | |
| if (end >= content_length) { | |
| end = content_length; | |
| } | |
| start = content_length - end; | |
| end = content_length - 1; | |
| } else if (start >= content_length && start <= end) { | |
| not_satisfy++; | |
| continue; | |
| } | |
| if (end >= content_length) { | |
| end = content_length - 1; | |
| } | |
| } | |
| if (start > end) { | |
| t_state.range_setup = HttpTransact::RangeSetup_t::NONE; | |
| goto Lfaild; | |
| } | |
| if (prev_good_range >= 0 && start <= ranges[prev_good_range]._end) { | |
| t_state.range_setup = HttpTransact::RangeSetup_t::NOT_HANDLED; | |
| goto Lfaild; | |
| } | |
| ink_assert(start >= 0 && end >= 0 && start < content_length && end < content_length); | |
| prev_good_range = nr; | |
| ranges[nr]._start = start; | |
| ranges[nr]._end = end; | |
| ++nr; | |
| if (cache_sm.cache_read_vc && t_state.cache_info.object_read) { | |
| if (!cache_sm.cache_read_vc->is_pread_capable() && cache_config_read_while_writer == 2) { | |
| // write in progress, check if request range not in cache yet | |
| HTTPInfo::FragOffset *frag_offset_tbl = t_state.cache_info.object_read->get_frag_table(); | |
| int frag_offset_cnt = t_state.cache_info.object_read->get_frag_offset_count(); | |
| if (!frag_offset_tbl || !frag_offset_cnt || (frag_offset_tbl[frag_offset_cnt - 1] < static_cast<uint64_t>(end))) { | |
| SMDbg(dbg_ctl_http_range, "request range in cache, end %" PRId64 ", frg_offset_cnt %d" PRId64, end, frag_offset_cnt); | |
| t_state.range_in_cache = false; | |
| } | |
| } | |
| } else { | |
| t_state.range_in_cache = false; | |
| } | |
| } | |
| if (nr > 0) { | |
| t_state.range_setup = HttpTransact::RangeSetup_t::REQUESTED; | |
| t_state.ranges = ranges; | |
| t_state.num_range_fields = nr; | |
| return; | |
| } | |
| if (not_satisfy) { | |
| t_state.range_setup = HttpTransact::RangeSetup_t::NOT_SATISFIABLE; | |
| } | |
| Lfaild: | |
| t_state.range_in_cache = false; | |
| t_state.num_range_fields = -1; | |
| delete[] ranges; | |
| return; | |
| } | |
| void | |
| HttpSM::calculate_output_cl(int64_t num_chars_for_ct, int64_t num_chars_for_cl) | |
| { | |
| if (t_state.range_setup != HttpTransact::RangeSetup_t::REQUESTED && | |
| t_state.range_setup != HttpTransact::RangeSetup_t::NOT_TRANSFORM_REQUESTED) { | |
| return; | |
| } | |
| ink_assert(t_state.ranges); | |
| if (t_state.num_range_fields == 1) { | |
| t_state.range_output_cl = t_state.ranges[0]._end - t_state.ranges[0]._start + 1; | |
| } else { | |
| for (int i = 0; i < t_state.num_range_fields; i++) { | |
| if (t_state.ranges[i]._start >= 0) { | |
| t_state.range_output_cl += boundary_size; | |
| t_state.range_output_cl += sub_header_size + num_chars_for_ct; | |
| t_state.range_output_cl += | |
| num_chars_for_int(t_state.ranges[i]._start) + num_chars_for_int(t_state.ranges[i]._end) + num_chars_for_cl + 2; | |
| t_state.range_output_cl += t_state.ranges[i]._end - t_state.ranges[i]._start + 1; | |
| t_state.range_output_cl += 2; | |
| } | |
| } | |
| t_state.range_output_cl += boundary_size + 2; | |
| } | |
| SMDbg(dbg_ctl_http_range, "Pre-calculated Content-Length for Range response is %" PRId64, t_state.range_output_cl); | |
| } | |
| void | |
| HttpSM::do_range_parse(MIMEField *range_field) | |
| { | |
| std::string_view content_type{}; | |
| int64_t content_length = 0; | |
| if (t_state.cache_info.object_read != nullptr) { | |
| content_type = | |
| t_state.cache_info.object_read->response_get()->value_get(static_cast<std::string_view>(MIME_FIELD_CONTENT_TYPE)); | |
| content_length = t_state.cache_info.object_read->object_size_get(); | |
| } else { | |
| content_length = t_state.hdr_info.server_response.get_content_length(); | |
| content_type = t_state.hdr_info.server_response.value_get(static_cast<std::string_view>(MIME_FIELD_CONTENT_TYPE)); | |
| } | |
| int64_t num_chars_for_cl = num_chars_for_int(content_length); | |
| parse_range_and_compare(range_field, content_length); | |
| calculate_output_cl(content_type.length(), num_chars_for_cl); | |
| } | |
| // this function looks for any Range: headers, parses them and either | |
| // sets up a transform processor to handle the request OR defers to the | |
| // HttpTunnel | |
| void | |
| HttpSM::do_range_setup_if_necessary() | |
| { | |
| MIMEField *field; | |
| field = t_state.hdr_info.client_request.field_find(static_cast<std::string_view>(MIME_FIELD_RANGE)); | |
| ink_assert(field != nullptr); | |
| t_state.range_setup = HttpTransact::RangeSetup_t::NONE; | |
| if (t_state.method == HTTP_WKSIDX_GET && t_state.hdr_info.client_request.version_get() == HTTP_1_1) { | |
| do_range_parse(field); | |
| if (t_state.range_setup == HttpTransact::RangeSetup_t::REQUESTED) { | |
| bool do_transform = false; | |
| if (!t_state.range_in_cache && t_state.cache_info.object_read) { | |
| SMDbg(dbg_ctl_http_range, "range can't be satisfied from cache, force origin request"); | |
| t_state.cache_lookup_result = HttpTransact::CacheLookupResult_t::MISS; | |
| return; | |
| } | |
| if (t_state.num_range_fields > 1) { | |
| if (0 == t_state.txn_conf->allow_multi_range) { | |
| t_state.range_setup = HttpTransact::RangeSetup_t::NONE; // No Range required (not allowed) | |
| t_state.hdr_info.client_request.field_delete( | |
| static_cast<std::string_view>(MIME_FIELD_RANGE)); // ... and nuke the Range header too | |
| t_state.num_range_fields = 0; | |
| } else if (1 == t_state.txn_conf->allow_multi_range) { | |
| do_transform = true; | |
| } else { | |
| t_state.num_range_fields = 0; | |
| t_state.range_setup = HttpTransact::RangeSetup_t::NOT_SATISFIABLE; | |
| } | |
| } else { | |
| // if revalidating and cache is stale we want to transform | |
| if (t_state.cache_info.action == HttpTransact::CacheAction_t::REPLACE) { | |
| if (t_state.hdr_info.server_response.status_get() == HTTPStatus::OK) { | |
| Dbg(dbg_ctl_http_range, "Serving transform after stale cache re-serve"); | |
| // Ranges and range_output_cl were computed against the stale cached object size. If the fresh origin Content-Length | |
| // differs, re-parse the Range against the fresh value so the outgoing Content-Length/Content-Range match the body | |
| // actually being sent. Without this, Content-Length/Content-Range advertise the stale cached size. | |
| const int64_t fresh_cl = t_state.hdr_info.server_response.get_content_length(); | |
| if (fresh_cl == 0) { | |
| // Re-parse yielded e.g. RANGE_NOT_SATISFIABLE (entire range past fresh body); let downstream handling take over | |
| // without installing the transform. | |
| Dbg(dbg_ctl_http_range, "Not transforming: fresh response body is empty"); | |
| return; | |
| } | |
| const int64_t cached_cl = t_state.cache_info.object_read ? t_state.cache_info.object_read->object_size_get() : -1; | |
| if (fresh_cl != cached_cl) { | |
| SMDbg(dbg_ctl_http_range, "Re-parsing range against fresh origin Content-Length %" PRId64 " (was %" PRId64 ")", | |
| fresh_cl, cached_cl); | |
| delete[] t_state.ranges; | |
| t_state.ranges = nullptr; | |
| t_state.num_range_fields = 0; | |
| t_state.range_setup = HttpTransact::RangeSetup_t::NONE; | |
| t_state.range_output_cl = 0; | |
| parse_range_done = false; | |
| std::string_view content_type = | |
| t_state.hdr_info.server_response.value_get(static_cast<std::string_view>(MIME_FIELD_CONTENT_TYPE)); | |
| parse_range_and_compare(field, fresh_cl); | |
| calculate_output_cl(content_type.length(), num_chars_for_int(fresh_cl)); | |
| if (t_state.range_setup != HttpTransact::RangeSetup_t::REQUESTED) { | |
| // Re-parse yielded e.g. RANGE_NOT_SATISFIABLE (entire range past fresh body); let downstream handling take over | |
| // without installing the transform. | |
| Dbg(dbg_ctl_http_range, "Not transforming: parse_range_and_compare set t_state.range_setup=%d", | |
| static_cast<int>(HttpTransact::RangeSetup_t::REQUESTED)); | |
| return; | |
| } | |
| } | |
| do_transform = true; | |
| } else { | |
| Dbg(dbg_ctl_http_range, "Not transforming after revalidate"); | |
| } | |
| } else if (cache_sm.cache_read_vc && cache_sm.cache_read_vc->is_pread_capable()) { | |
| // If only one range entry and pread is capable, no need transform range | |
| t_state.range_setup = HttpTransact::RangeSetup_t::NOT_TRANSFORM_REQUESTED; | |
| } else { | |
| do_transform = true; | |
| } | |
| } | |
| // We have to do the transform on (allowed) multi-range request, *or* if the VC is not pread capable | |
| if (do_transform) { | |
| if (api_hooks.get(TS_HTTP_RESPONSE_TRANSFORM_HOOK) == nullptr) { | |
| std::string_view content_type{}; | |
| int64_t content_length = 0; | |
| if (t_state.cache_info.object_read && t_state.cache_info.action != HttpTransact::CacheAction_t::REPLACE) { | |
| content_type = | |
| t_state.cache_info.object_read->response_get()->value_get(static_cast<std::string_view>(MIME_FIELD_CONTENT_TYPE)); | |
| content_length = t_state.cache_info.object_read->object_size_get(); | |
| } else { | |
| // We don't want to transform a range request if the server response has a content encoding. | |
| if (t_state.hdr_info.server_response.presence(MIME_PRESENCE_CONTENT_ENCODING)) { | |
| Dbg(dbg_ctl_http_trans, "Cannot setup range transform for server response with content encoding"); | |
| t_state.range_setup = HttpTransact::RangeSetup_t::NONE; | |
| return; | |
| } | |
| // Since we are transforming the range from the server, we want to cache the original response | |
| t_state.api_info.cache_untransformed = true; | |
| content_type = t_state.hdr_info.server_response.value_get(static_cast<std::string_view>(MIME_FIELD_CONTENT_TYPE)); | |
| content_length = t_state.hdr_info.server_response.get_content_length(); | |
| } | |
| SMDbg(dbg_ctl_http_trans, "Unable to accelerate range request, fallback to transform"); | |
| // create a Range: transform processor for requests of type Range: bytes=1-2,4-5,10-100 (eg. multiple ranges) | |
| INKVConnInternal *range_trans = transformProcessor.range_transform( | |
| mutex.get(), t_state.ranges, t_state.num_range_fields, &t_state.hdr_info.transform_response, content_type.data(), | |
| static_cast<int>(content_type.length()), content_length); | |
| api_hooks.append(TS_HTTP_RESPONSE_TRANSFORM_HOOK, range_trans); | |
| } else { | |
| // ToDo: Do we do something here? The theory is that multiple transforms do not behave well with | |
| // the range transform needed here. | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // The URL this transaction looks up in the cache, and that every delete which | |
| // has to match that lookup uses too. | |
| // | |
| // hdr_info.client_request's URL always names the *current* request: | |
| // redirect_request() rewrites it in place, so after a redirect follow it is the | |
| // Location target. cache_info.lookup_url is set once, before the first lookup | |
| // (HttpTransact::DecideCacheLookup), and names the *original* request. Whether | |
| // it also moves with a redirect is an accident of how it was set: with | |
| // pristine_host_hdr off it aliases client_request's URL and follows it, with | |
| // pristine_host_hdr on it is a private copy and stays put. It is therefore not | |
| // a reliable way to reach the redirected URL, which is why the redirect case | |
| // below reads client_request instead. | |
| URL * | |
| HttpSM::cache_lookup_url() | |
| { | |
| // Follow the redirect unless configured to keep the original cache key. | |
| if (t_state.redirect_info.redirect_in_process && !t_state.txn_conf->redirect_use_orig_cache_key) { | |
| return t_state.hdr_info.client_request.url_get(); | |
| } | |
| return t_state.cache_info.lookup_url; | |
| } | |
| void | |
| HttpSM::do_cache_lookup_and_read() | |
| { | |
| // TODO decide whether to uncomment after finish testing redirect | |
| // ink_assert(server_txn == NULL); | |
| ink_assert(pending_action.empty()); | |
| t_state.request_sent_time = UNDEFINED_TIME; | |
| t_state.response_received_time = UNDEFINED_TIME; | |
| // A compatibility retry continues the lookup that just missed rather than | |
| // starting a new one, so the counter, the probe, and the milestone all stay | |
| // with the canonical-key lookup that began it. | |
| if (!CompatCacheKey::is_legacy(compatibility_cache_lookup)) { | |
| Metrics::Counter::increment(http_rsb.cache_lookups); | |
| ATS_PROBE1(milestone_cache_open_read_begin, sm_id); | |
| milestones[TS_MILESTONE_CACHE_OPEN_READ_BEGIN] = ink_get_hrtime(); | |
| t_state.cache_info.lookup_count++; | |
| } | |
| t_state.cache_lookup_result = HttpTransact::CacheLookupResult_t::NONE; | |
| URL *c_url = cache_lookup_url(); | |
| SMDbg(dbg_ctl_http_seq, "Issuing cache lookup for URL %s", c_url->string_get(&t_state.arena)); | |
| HttpCacheKey key; | |
| if (CompatCacheKey::is_legacy(compatibility_cache_lookup)) { | |
| generate_cache_key92(&key, c_url); | |
| } else { | |
| generate_cache_key(&key, c_url); | |
| } | |
| t_state.hdr_info.cache_request.copy(&t_state.hdr_info.client_request); | |
| HttpTransactHeaders::normalize_accept_encoding(t_state.txn_conf, &t_state.hdr_info.cache_request); | |
| pending_action = cache_sm.open_read( | |
| &key, c_url, &t_state.hdr_info.cache_request, t_state.txn_conf, | |
| static_cast<time_t>((t_state.cache_control.pin_in_cache_for < 0) ? 0 : t_state.cache_control.pin_in_cache_for)); | |
| // | |
| // pin_in_cache value is an open_write parameter. | |
| // It is passed in open_read to allow the cluster to | |
| // optimize the typical open_read/open_read failed/open_write | |
| // sequence. | |
| // | |
| REMEMBER((long)pending_action.get(), reentrancy_count); | |
| return; | |
| } | |
| void | |
| HttpSM::do_cache_delete_all_alts() | |
| { | |
| // Do not delete a non-existent object. | |
| ink_assert(t_state.cache_info.object_read); | |
| // Address the object that was looked up. A redirect follow can look up a | |
| // different URL than cache_info.lookup_url, which is set once and does not | |
| // track the redirect when the pristine host header is maintained. | |
| URL *url = cache_lookup_url(); | |
| SMDbg(dbg_ctl_http_seq, "Issuing cache delete for %s", url->string_get_ref()); | |
| HttpCacheKey key; | |
| generate_cache_key(&key, url); | |
| cacheProcessor.remove(nullptr, &key); | |
| // A migration leaves the legacy copy in place, so the object can live under | |
| // both keys. Removing only one of them would let the other be served after | |
| // the purge. | |
| do_cache_delete_compat_alts(); | |
| } | |
| // Remove the object stored under the legacy key. | |
| // | |
| // Only for the delete paths, which have to reach both keys. A successful | |
| // migration deliberately leaves the legacy copy alone: VC_EVENT_WRITE_COMPLETE | |
| // means the tunnel handed the last byte to the cache VC, not that the object | |
| // reached disk, so deleting on that signal loses the object outright whenever | |
| // the write later fails. The copy ages out on its own, and it stops being read | |
| // as soon as the canonical key resolves, so compat_key_reads still decays to | |
| // zero. | |
| void | |
| HttpSM::do_cache_delete_compat_alts() | |
| { | |
| // Same URL the lookup used; see do_cache_delete_all_alts(). | |
| URL *url = cache_lookup_url(); | |
| // A path that carries its own ";params" segment hashes to the same key | |
| // under both schemes, so the canonical delete already reached it. | |
| if (!t_state.http_config_param->cache_try_compat_key_read || url->has_path_params()) { | |
| return; | |
| } | |
| SMDbg(dbg_ctl_http_seq, "Issuing compatibility cache delete for %s", url->string_get_ref()); | |
| HttpCacheKey key; | |
| generate_cache_key92(&key, url); | |
| cacheProcessor.remove(nullptr, &key); | |
| } | |
| inline void | |
| HttpSM::do_cache_prepare_write() | |
| { | |
| ATS_PROBE1(milestone_cache_open_write_begin, sm_id); | |
| milestones[TS_MILESTONE_CACHE_OPEN_WRITE_BEGIN] = ink_get_hrtime(); | |
| do_cache_prepare_action(&cache_sm, t_state.cache_info.object_read, true); | |
| } | |
| inline void | |
| HttpSM::do_cache_prepare_write_transform() | |
| { | |
| if (cache_sm.cache_write_vc != nullptr || tunnel.has_cache_writer()) { | |
| do_cache_prepare_action(&transform_cache_sm, nullptr, false, true); | |
| } else { | |
| do_cache_prepare_action(&transform_cache_sm, nullptr, false); | |
| } | |
| } | |
| void | |
| HttpSM::do_cache_prepare_update() | |
| { | |
| if (t_state.cache_info.object_read != nullptr && t_state.cache_info.object_read->valid() && | |
| t_state.cache_info.object_store.valid() && t_state.cache_info.object_store.response_get() != nullptr && | |
| t_state.cache_info.object_store.response_get()->valid() && | |
| t_state.hdr_info.client_request.method_get_wksidx() == HTTP_WKSIDX_GET) { | |
| // An object found under the 9.2 key cannot be updated in place: the write | |
| // would be a create on the current key, and the cache turns a header-only | |
| // close of a create into an abort. Take the failed-update path instead, | |
| // which serves the updated headers without storing them. | |
| if (CompatCacheKey::is_legacy(compatibility_cache_lookup)) { | |
| ink_assert(t_state.cache_info.write_lock_state != HttpTransact::CacheWriteLock_t::SUCCESS); | |
| call_transact_and_set_next_state(HttpTransact::HandleUpdateCachedObject); | |
| return; | |
| } | |
| t_state.cache_info.object_store.request_set(t_state.cache_info.object_read->request_get()); | |
| // t_state.cache_info.object_read = NULL; | |
| // cache_sm.close_read(); | |
| t_state.transact_return_point = HttpTransact::HandleUpdateCachedObject; | |
| ink_assert(cache_sm.cache_write_vc == nullptr); | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::state_cache_open_write); | |
| // don't retry read for update | |
| do_cache_prepare_action(&cache_sm, t_state.cache_info.object_read, false); | |
| } else { | |
| t_state.api_modifiable_cached_resp = false; | |
| call_transact_and_set_next_state(HttpTransact::HandleApiErrorJump); | |
| } | |
| } | |
| void | |
| HttpSM::do_cache_prepare_action(HttpCacheSM *c_sm, CacheHTTPInfo *object_read_info, bool retry, bool allow_multiple) | |
| { | |
| URL *o_url, *s_url; | |
| bool restore_client_request = false; | |
| ink_assert(pending_action.empty()); | |
| if (t_state.redirect_info.redirect_in_process) { | |
| o_url = &(t_state.redirect_info.original_url); | |
| ink_assert(o_url->valid()); | |
| restore_client_request = true; | |
| s_url = o_url; | |
| } else { | |
| o_url = &(t_state.cache_info.original_url); | |
| if (o_url->valid()) { | |
| s_url = o_url; | |
| } else { | |
| s_url = t_state.cache_info.lookup_url; | |
| } | |
| } | |
| // modify client request to make it have the url we are going to | |
| // store into the cache | |
| if (restore_client_request) { | |
| URL *c_url = t_state.hdr_info.client_request.url_get(); | |
| s_url->copy(c_url); | |
| } | |
| ink_assert(s_url != nullptr && s_url->valid()); | |
| SMDbg(dbg_ctl_http_cache_write, "writing to cache with URL %s", s_url->string_get(&t_state.arena)); | |
| HttpCacheKey key; | |
| generate_cache_key(&key, s_url); | |
| // A compatibility read returns an object stored under the legacy key. Passing | |
| // that object to a write using the canonical key turns the write into an | |
| // update, but the canonical-key vector does not contain the legacy alternate. | |
| // Cache::open_write then fails with ECACHE_NO_DOC instead of creating the | |
| // migrated object. Create a new canonical-key object for compatibility reads. | |
| CacheHTTPInfo *write_object_read_info = CompatCacheKey::write_info(compatibility_cache_lookup, object_read_info); | |
| pending_action = | |
| c_sm->open_write(&key, s_url, &t_state.hdr_info.cache_request, write_object_read_info, | |
| static_cast<time_t>((t_state.cache_control.pin_in_cache_for < 0) ? 0 : t_state.cache_control.pin_in_cache_for), | |
| retry, allow_multiple); | |
| } | |
| void | |
| HttpSM::send_origin_throttled_response() | |
| { | |
| // if the request is to a parent proxy, do not reset | |
| // t_state.current.retry_attempts so that another parent or | |
| // NextHop may be tried. | |
| if (t_state.dns_info.looking_up != ResolveInfo::PARENT_PROXY) { | |
| t_state.current.retry_attempts.maximize(t_state.configured_connect_attempts_max_retries()); | |
| } | |
| if (t_state.cause_of_death_errno == -UNKNOWN_INTERNAL_ERROR) { | |
| t_state.set_connect_fail(EUSERS); // Too many users. | |
| } | |
| t_state.current.state = HttpTransact::OUTBOUND_CONGESTION; | |
| call_transact_and_set_next_state(HttpTransact::HandleResponse); | |
| } | |
| static void | |
| set_tls_options(NetVCOptions &opt, const OverridableHttpConfigParams *txn_conf) | |
| { | |
| char *verify_server = nullptr; | |
| if (txn_conf->ssl_client_verify_server_policy == nullptr) { | |
| opt.verifyServerPolicy = YamlSNIConfig::Policy::UNSET; | |
| } else { | |
| verify_server = txn_conf->ssl_client_verify_server_policy; | |
| if (strcmp(verify_server, "DISABLED") == 0) { | |
| opt.verifyServerPolicy = YamlSNIConfig::Policy::DISABLED; | |
| } else if (strcmp(verify_server, "PERMISSIVE") == 0) { | |
| opt.verifyServerPolicy = YamlSNIConfig::Policy::PERMISSIVE; | |
| } else if (strcmp(verify_server, "ENFORCED") == 0) { | |
| opt.verifyServerPolicy = YamlSNIConfig::Policy::ENFORCED; | |
| } else { | |
| Warning("%s is invalid for proxy.config.ssl.client.verify.server.policy. Should be one of DISABLED, PERMISSIVE, or ENFORCED", | |
| verify_server); | |
| opt.verifyServerPolicy = YamlSNIConfig::Policy::UNSET; | |
| } | |
| } | |
| if (txn_conf->ssl_client_verify_server_properties == nullptr) { | |
| opt.verifyServerProperties = YamlSNIConfig::Property::UNSET; | |
| } else { | |
| verify_server = txn_conf->ssl_client_verify_server_properties; | |
| if (strcmp(verify_server, "SIGNATURE") == 0) { | |
| opt.verifyServerProperties = YamlSNIConfig::Property::SIGNATURE_MASK; | |
| } else if (strcmp(verify_server, "NAME") == 0) { | |
| opt.verifyServerProperties = YamlSNIConfig::Property::NAME_MASK; | |
| } else if (strcmp(verify_server, "ALL") == 0) { | |
| opt.verifyServerProperties = YamlSNIConfig::Property::ALL_MASK; | |
| } else if (strcmp(verify_server, "NONE") == 0) { | |
| opt.verifyServerProperties = YamlSNIConfig::Property::NONE; | |
| } else { | |
| Warning("%s is invalid for proxy.config.ssl.client.verify.server.properties. Should be one of SIGNATURE, NAME, or ALL", | |
| verify_server); | |
| opt.verifyServerProperties = YamlSNIConfig::Property::NONE; | |
| } | |
| } | |
| } | |
| std::string_view | |
| HttpSM::get_outbound_cert() const | |
| { | |
| const char *cert_name = t_state.txn_conf->ssl_client_cert_filename; | |
| if (cert_name == nullptr) { | |
| cert_name = ""; | |
| } | |
| return std::string_view(cert_name); | |
| } | |
| std::string_view | |
| HttpSM::get_outbound_sni() const | |
| { | |
| using namespace swoc::literals; | |
| std::string_view zret; | |
| swoc::TextView policy{t_state.txn_conf->ssl_client_sni_policy, swoc::TextView::npos}; | |
| TLSSNISupport *snis = nullptr; | |
| if (_ua.get_txn()) { | |
| if (auto *netvc = _ua.get_txn()->get_netvc(); netvc) { | |
| snis = netvc->get_service<TLSSNISupport>(); | |
| if (snis && snis->hints_from_sni.outbound_sni_policy.has_value()) { | |
| policy.assign(snis->hints_from_sni.outbound_sni_policy->data(), snis->hints_from_sni.outbound_sni_policy->size()); | |
| } | |
| } | |
| } | |
| if (policy.empty() || policy == "host"_tv) { | |
| // By default the host header field value is used for the SNI. | |
| zret = t_state.hdr_info.server_request.host_get(); | |
| } else if (_ua.get_txn() && snis && policy == "server_name"_tv) { | |
| const char *const server_name = snis->get_sni_server_name(); | |
| if (nullptr == server_name || server_name[0] == '\0') { | |
| zret = {}; | |
| } else { | |
| zret = server_name; | |
| } | |
| } else if (policy.front() == '@') { // guaranteed non-empty from previous clause | |
| zret = policy.remove_prefix(1); | |
| } else { | |
| // If other is specified, like "remap" and "verify_with_name_source", the remapped origin name is used for the SNI value | |
| zret = t_state.server_info.name; | |
| } | |
| return zret; | |
| } | |
| std::string_view | |
| HttpSM::get_outbound_sni_for_cert_verification() const | |
| { | |
| auto zret = this->get_outbound_sni(); | |
| if (zret.empty()) { | |
| zret = t_state.hdr_info.server_request.host_get(); | |
| } | |
| if (zret.empty()) { | |
| if (t_state.current.server != nullptr && t_state.current.server->name != nullptr) { | |
| zret = t_state.current.server->name; | |
| } | |
| } | |
| return zret; | |
| } | |
| bool | |
| HttpSM::apply_ip_allow_filter() | |
| { | |
| // Method allowed on dest IP address check | |
| IpAllow::ACL acl = IpAllow::match(this->get_server_remote_addr(), IpAllow::match_key_t::DST_ADDR); | |
| if (ip_allow_is_request_forbidden(acl)) { | |
| ip_allow_deny_request(acl); | |
| return false; | |
| } | |
| return true; | |
| } | |
| bool | |
| HttpSM::ip_allow_is_request_forbidden(const IpAllow::ACL &acl) | |
| { | |
| bool result{false}; | |
| if (acl.isValid()) { | |
| if (acl.isDenyAll()) { | |
| result = true; | |
| } else if (!acl.isAllowAll()) { | |
| if (this->get_request_method_wksidx() != -1) { | |
| result = !acl.isMethodAllowed(this->get_request_method_wksidx()); | |
| } else { | |
| auto method{t_state.hdr_info.server_request.method_get()}; | |
| result = !acl.isNonstandardMethodAllowed(method); | |
| } | |
| } | |
| } | |
| return result; | |
| } | |
| void | |
| HttpSM::ip_allow_deny_request(const IpAllow::ACL &acl) | |
| { | |
| if (dbg_ctl_ip_allow.on()) { | |
| ip_text_buffer ipb; | |
| auto method{t_state.hdr_info.client_request.method_get()}; | |
| const char *ntop_formatted = ats_ip_ntop(this->get_server_remote_addr(), ipb, sizeof(ipb)); | |
| Warning("server '%s' prohibited by ip-allow policy at line %d", ntop_formatted, acl.source_line()); | |
| SMDbg(dbg_ctl_ip_allow, "Line %d denial for '%.*s' from %s", acl.source_line(), static_cast<int>(method.length()), | |
| method.data(), ntop_formatted); | |
| } | |
| t_state.http_return_code_setter_name = "ip_allow"; | |
| t_state.current.retry_attempts.maximize( | |
| t_state.configured_connect_attempts_max_retries()); // prevent any more retries with this IP | |
| call_transact_and_set_next_state(HttpTransact::Forbidden); | |
| } | |
| bool | |
| HttpSM::grab_pre_warmed_net_v_connection_if_possible(const TLSTunnelSupport &tts, int pid) | |
| { | |
| bool result{false}; | |
| if (is_prewarm_enabled_or_sni_overridden(tts)) { | |
| EThread *ethread = this_ethread(); | |
| _prewarm_sm = ethread->prewarm_queue->dequeue(tts.create_dst(pid)); | |
| if (_prewarm_sm != nullptr) { | |
| open_prewarmed_connection(); | |
| result = true; | |
| } else { | |
| SMDbg(dbg_ctl_http_ss, "no pre-warmed tunnel"); | |
| } | |
| } | |
| return result; | |
| } | |
| bool | |
| HttpSM::is_prewarm_enabled_or_sni_overridden(const TLSTunnelSupport &tts) const | |
| { | |
| PreWarmConfig::scoped_config prewarm_conf; | |
| bool result = prewarm_conf->enabled; | |
| if (YamlSNIConfig::TunnelPreWarm sni_use_prewarm = tts.get_tunnel_prewarm_configuration(); | |
| sni_use_prewarm != YamlSNIConfig::TunnelPreWarm::UNSET) { | |
| result = static_cast<bool>(sni_use_prewarm); | |
| } | |
| return result; | |
| } | |
| void | |
| HttpSM::open_prewarmed_connection() | |
| { | |
| NetVConnection *netvc = _prewarm_sm->move_netvc(); | |
| ink_release_assert(_prewarm_sm->handler.is<&PreWarmSM::state_closed>()); | |
| SMDbg(dbg_ctl_http_ss, "using pre-warmed tunnel netvc=%p", netvc); | |
| t_state.current.retry_attempts.clear(); | |
| ink_release_assert(default_handler == HttpSM::default_handler); | |
| handleEvent(NET_EVENT_OPEN, netvc); | |
| } | |
| ////////////////////////////////////////////////////////////////////////// | |
| // | |
| // HttpSM::do_http_server_open() | |
| // | |
| ////////////////////////////////////////////////////////////////////////// | |
| void | |
| HttpSM::do_http_server_open(bool raw, bool only_direct) | |
| { | |
| int ip_family = t_state.current.server->dst_addr.sa.sa_family; | |
| auto fam_name = ats_ip_family_name(ip_family); | |
| SMDbg(dbg_ctl_http_track, "[%.*s]", static_cast<int>(fam_name.size()), fam_name.data()); | |
| NetVConnection *vc = _ua.get_txn()->get_netvc(); | |
| ink_release_assert(vc && vc->thread == this_ethread()); | |
| pending_action = nullptr; | |
| // Clean up connection tracking info if any. Need to do it now so the selected group | |
| // is consistent with the actual upstream in case of retry. | |
| t_state.outbound_conn_track_state.clear(); | |
| // Make sure any previous attempts are cleaned out | |
| if (server_txn) { | |
| tunnel.reset(); | |
| server_txn->transaction_done(); | |
| server_txn = nullptr; | |
| } | |
| // _ua.get_entry() can be null if a scheduled update is also a reverse proxy | |
| // request. Added REVPROXY to the assert below, and then changed checks | |
| // to be based on _ua.get_txn() != NULL instead of req_flavor value. | |
| ink_assert(_ua.get_entry() != nullptr || t_state.req_flavor == HttpTransact::HttpRequestFlavor_t::SCHEDULED_UPDATE || | |
| t_state.req_flavor == HttpTransact::HttpRequestFlavor_t::REVPROXY); | |
| ink_assert(pending_action.empty()); | |
| ink_assert(t_state.current.server->dst_addr.network_order_port() != 0); | |
| char addrbuf[INET6_ADDRPORTSTRLEN]; | |
| SMDbg(dbg_ctl_http, "open connection to %s: %s", t_state.current.server->name, | |
| ats_ip_nptop(&t_state.current.server->dst_addr.sa, addrbuf, sizeof(addrbuf))); | |
| SMDbg(dbg_ctl_http_seq, "Sending request to server"); | |
| // set the server first connect milestone here in case we return in the plugin_tunnel case that follows | |
| ATS_PROBE1(milestone_server_connect, sm_id); | |
| milestones[TS_MILESTONE_SERVER_CONNECT] = ink_get_hrtime(); | |
| if (milestones[TS_MILESTONE_SERVER_FIRST_CONNECT] == 0) { | |
| ATS_PROBE1(milestone_server_first_connect, sm_id); | |
| milestones[TS_MILESTONE_SERVER_FIRST_CONNECT] = milestones[TS_MILESTONE_SERVER_CONNECT]; | |
| } | |
| if (plugin_tunnel) { | |
| PluginVCCore *t = plugin_tunnel; | |
| plugin_tunnel = nullptr; | |
| Action *pvc_action_handle = t->connect_re(this); | |
| // This connect call is always reentrant | |
| ink_release_assert(pvc_action_handle == ACTION_RESULT_DONE); | |
| return; | |
| } | |
| // Check for remap rule. If so, only apply ip_allow filter if it is activated (ip_allow_check_enabled_p set). | |
| // Otherwise, if no remap rule is defined, apply the ip_allow filter. | |
| if (!t_state.url_remap_success || t_state.url_map.getMapping()->ip_allow_check_enabled_p) { | |
| if (!apply_ip_allow_filter()) { | |
| return; | |
| } | |
| } | |
| // Check for self loop. In outbound-transparent mode the client's original | |
| // destination is the legitimate upstream, so the client_info.dst_addr match | |
| // inside the loop check is skipped. The self-IP check (when | |
| // max_proxy_cycles == 0) and the Via-header multi-hop check are still | |
| // enforced so a client targeting ATS's own listening address or looping | |
| // through its own Via UUID is still caught. | |
| if (HttpTransact::will_this_request_self_loop(&t_state, _ua.get_txn()->is_outbound_transparent())) { | |
| call_transact_and_set_next_state(HttpTransact::SelfLoop); | |
| return; | |
| } | |
| // If this is not a raw connection, we try to get a session from the | |
| // shared session pool. Raw connections are for SSLs tunnel and | |
| // require a new connection | |
| // | |
| // This problem with POST requests is a bug. Because of the issue of the | |
| // race with us sending a request after server has closed but before the FIN | |
| // gets to us, we should open a new connection for POST. I believe TS used | |
| // to do this but as far I can tell the code that prevented keep-alive if | |
| // there is a request body has been removed. | |
| // If we are sending authorizations headers, mark the connection private | |
| // | |
| // We do this here because it means that we will not waste a connection from the pool if we already | |
| // know that the session will be private. This is overridable meaning that if a plugin later decides | |
| // it shouldn't be private it can still be returned to a shared pool. | |
| if (t_state.txn_conf->auth_server_session_private == 1 && | |
| t_state.hdr_info.server_request.presence(MIME_PRESENCE_AUTHORIZATION | MIME_PRESENCE_PROXY_AUTHORIZATION | | |
| MIME_PRESENCE_WWW_AUTHENTICATE)) { | |
| SMDbg(dbg_ctl_http_ss_auth, "Setting server session to private for authorization headers"); | |
| will_be_private_ss = true; | |
| } else if (t_state.txn_conf->auth_server_session_private == 2 && | |
| t_state.hdr_info.server_request.presence(MIME_PRESENCE_PROXY_AUTHORIZATION | MIME_PRESENCE_WWW_AUTHENTICATE)) { | |
| SMDbg(dbg_ctl_http_ss_auth, "Setting server session to private for Proxy-Authorization or WWW-Authenticate header"); | |
| will_be_private_ss = true; | |
| } | |
| if (t_state.method == HTTP_WKSIDX_POST || t_state.method == HTTP_WKSIDX_PUT) { | |
| // don't share the session if keep-alive for post is not on | |
| if (t_state.txn_conf->keep_alive_post_out == 0) { | |
| SMDbg(dbg_ctl_http_ss, "Setting server session to private because of keep-alive post out"); | |
| will_be_private_ss = true; | |
| } | |
| } | |
| bool try_reuse = false; | |
| if ((raw == false) && TS_SERVER_SESSION_SHARING_MATCH_NONE != t_state.txn_conf->server_session_sharing_match && | |
| (t_state.txn_conf->keep_alive_post_out == 1 || t_state.hdr_info.request_content_length <= 0) && !is_private() && | |
| _ua.get_txn() != nullptr) { | |
| HSMresult_t shared_result; | |
| SMDbg(dbg_ctl_http_ss, "Try to acquire_session for %s", t_state.current.server->name); | |
| shared_result = httpSessionManager.acquire_session(this, // state machine | |
| &t_state.current.server->dst_addr.sa, // ip + port | |
| t_state.current.server->name, // hostname | |
| _ua.get_txn() // has ptr to bound ua sessions | |
| ); | |
| try_reuse = true; | |
| switch (shared_result) { | |
| case HSMresult_t::DONE: | |
| Metrics::Counter::increment(http_rsb.origin_reuse); | |
| hsm_release_assert(server_txn != nullptr); | |
| handle_http_server_open(); | |
| return; | |
| case HSMresult_t::NOT_FOUND: | |
| Metrics::Counter::increment(http_rsb.origin_not_found); | |
| hsm_release_assert(server_txn == nullptr); | |
| break; | |
| case HSMresult_t::RETRY: | |
| Metrics::Counter::increment(http_rsb.origin_reuse_fail); | |
| // Pool locking or transaction allocation failed; open a new connection. | |
| break; | |
| default: | |
| hsm_release_assert(0); | |
| } | |
| } | |
| // Avoid a problem where server session sharing is disabled and we have keep-alive, we are trying to open a new server | |
| // session when we already have an attached server session. | |
| else if ((TS_SERVER_SESSION_SHARING_MATCH_NONE == t_state.txn_conf->server_session_sharing_match || is_private()) && | |
| (_ua.get_txn() != nullptr)) { | |
| PoolableSession *existing_ss = _ua.get_txn()->get_server_session(); | |
| if (existing_ss) { | |
| // [amc] Not sure if this is the best option, but we don't get here unless session sharing is disabled | |
| // so there's no point in further checking on the match or pool values. But why check anything? The | |
| // client has already exchanged a request with this specific origin server and has sent another one | |
| // shouldn't we just automatically keep the association? | |
| if (ats_ip_addr_port_eq(existing_ss->get_remote_addr(), &t_state.current.server->dst_addr.sa)) { | |
| _ua.get_txn()->attach_server_session(nullptr); | |
| existing_ss->set_active(); | |
| this->create_server_txn(existing_ss); | |
| hsm_release_assert(server_txn != nullptr); | |
| handle_http_server_open(); | |
| return; | |
| } else { | |
| // As this is in the non-sharing configuration, we want to close | |
| // the existing connection and call connect_re to get a new one | |
| existing_ss->set_inactivity_timeout(HRTIME_SECONDS(t_state.txn_conf->keep_alive_no_activity_timeout_out)); | |
| existing_ss->release(server_txn); | |
| _ua.get_txn()->attach_server_session(nullptr); | |
| } | |
| } | |
| } | |
| // Otherwise, we release the existing connection and call connect_re | |
| // to get a new one. | |
| // _ua.get_txn() is null when t_state.req_flavor == HttpRequestFlavor_t::SCHEDULED_UPDATE | |
| else if (_ua.get_txn() != nullptr) { | |
| PoolableSession *existing_ss = _ua.get_txn()->get_server_session(); | |
| if (existing_ss) { | |
| existing_ss->set_inactivity_timeout(HRTIME_SECONDS(t_state.txn_conf->keep_alive_no_activity_timeout_out)); | |
| existing_ss->release(server_txn); | |
| _ua.get_txn()->attach_server_session(nullptr); | |
| } | |
| } | |
| if (!try_reuse) { | |
| Metrics::Counter::increment(http_rsb.origin_make_new); | |
| if (TS_SERVER_SESSION_SHARING_MATCH_NONE == t_state.txn_conf->server_session_sharing_match) { | |
| Metrics::Counter::increment(http_rsb.origin_no_sharing); | |
| } else if ((t_state.txn_conf->keep_alive_post_out != 1 && t_state.hdr_info.request_content_length > 0)) { | |
| Metrics::Counter::increment(http_rsb.origin_body); | |
| } else if (is_private()) { | |
| Metrics::Counter::increment(http_rsb.origin_private); | |
| } else if (raw) { | |
| Metrics::Counter::increment(http_rsb.origin_raw); | |
| } else { | |
| ink_release_assert(_ua.get_txn() == nullptr); | |
| } | |
| } | |
| bool multiplexed_origin = !only_direct && !raw && this->origin_multiplexed() && !is_private(); | |
| if (multiplexed_origin) { | |
| SMDbg(dbg_ctl_http_ss, "Check for existing connect request"); | |
| if (this->add_to_existing_request()) { | |
| SMDbg(dbg_ctl_http_ss, "Queue behind existing request"); | |
| // We are queued up behind an existing connect request | |
| // Go away and wait. | |
| return; | |
| } | |
| } | |
| // Check to see if we have reached the max number of connections. | |
| // Atomically read the current number of connections and check to see | |
| // if we have gone above the max allowed. | |
| if (t_state.http_config_param->server_max_connections > 0) { | |
| if (Metrics::Gauge::load(http_rsb.current_server_connections) >= t_state.http_config_param->server_max_connections) { | |
| httpSessionManager.purge_keepalives(); | |
| // Eventually may want to have a queue as the origin_max_connection does to allow for a combination | |
| // of retries and errors. But at this point, we are just going to allow the error case. | |
| if (t_state.cause_of_death_errno == -UNKNOWN_INTERNAL_ERROR) { | |
| t_state.set_connect_fail(ENFILE); // Too many open files in system. | |
| } | |
| t_state.current.state = HttpTransact::CONNECTION_ERROR; | |
| call_transact_and_set_next_state(HttpTransact::HandleResponse); | |
| return; | |
| } | |
| } | |
| // See if the outbound connection tracker data is needed. If so, get it here for consistency. | |
| if (t_state.txn_conf->connection_tracker_config.server_max > 0 || t_state.txn_conf->connection_tracker_config.server_min > 0 || | |
| t_state.txn_conf->connection_tracker_config.metric_enabled) { | |
| t_state.outbound_conn_track_state = | |
| ConnectionTracker::obtain_outbound(t_state.txn_conf->connection_tracker_config, | |
| std::string_view{t_state.current.server->name}, t_state.current.server->dst_addr); | |
| } | |
| // Check to see if we have reached the max number of connections on this upstream host. | |
| if (t_state.txn_conf->connection_tracker_config.server_max > 0) { | |
| auto &ct_state = t_state.outbound_conn_track_state; | |
| auto ccount = ct_state.reserve(); | |
| int const server_max = t_state.txn_conf->connection_tracker_config.server_max; | |
| if (ccount > server_max) { | |
| ct_state.release(); | |
| ink_assert(pending_action.empty()); // in case of reschedule must not have already pending. | |
| ct_state.blocked(); | |
| Metrics::Counter::increment(http_rsb.origin_connections_throttled); | |
| ct_state.Warn_Blocked(server_max, sm_id, ccount - 1, &t_state.current.server->dst_addr.sa, | |
| debug_on && dbg_ctl_http.on() ? &dbg_ctl_http : nullptr); | |
| send_origin_throttled_response(); | |
| return; | |
| } else { | |
| ct_state.Note_Unblocked(&t_state.txn_conf->connection_tracker_config, ccount, &t_state.current.server->dst_addr.sa); | |
| } | |
| ct_state.update_max_count(ccount); | |
| } else if (t_state.txn_conf->connection_tracker_config.server_min > 0 || | |
| t_state.txn_conf->connection_tracker_config.metric_enabled) { | |
| auto &ct_state = t_state.outbound_conn_track_state; | |
| // Feed the count through as well, otherwise the group's peak stays at zero whenever metrics | |
| // are enabled without a configured maximum. | |
| ct_state.update_max_count(ct_state.reserve()); | |
| } | |
| // We did not manage to get an existing session and need to open a new connection | |
| NetVCOptions opt; | |
| opt.f_blocking_connect = false; | |
| opt.set_sock_param(t_state.txn_conf->sock_recv_buffer_size_out, t_state.txn_conf->sock_send_buffer_size_out, | |
| t_state.txn_conf->sock_option_flag_out, t_state.txn_conf->sock_packet_mark_out, | |
| t_state.txn_conf->sock_packet_tos_out, t_state.txn_conf->sock_packet_notsent_lowat); | |
| set_tls_options(opt, t_state.txn_conf); | |
| opt.ip_family = ip_family; | |
| int scheme_to_use = t_state.scheme; // get initial scheme | |
| bool tls_upstream = scheme_to_use == URL_WKSIDX_HTTPS; | |
| if (_ua.get_txn()) { | |
| auto tts = _ua.get_txn()->get_netvc()->get_service<TLSTunnelSupport>(); | |
| if (tts && raw) { | |
| tls_upstream = tts->is_upstream_tls(); | |
| _tunnel_type = tts->get_tunnel_type(); | |
| // ALPN on TLS Partial Blind Tunnel - set negotiated ALPN id | |
| int pid = SessionProtocolNameRegistry::INVALID; | |
| if (tts->get_tunnel_type() == SNIRoutingType::PARTIAL_BLIND) { | |
| auto alpns = _ua.get_txn()->get_netvc()->get_service<ALPNSupport>(); | |
| ink_assert(alpns); | |
| pid = alpns->get_negotiated_protocol_id(); | |
| if (pid != SessionProtocolNameRegistry::INVALID) { | |
| opt.alpn_protos = SessionProtocolNameRegistry::convert_openssl_alpn_wire_format(pid); | |
| } | |
| } | |
| if (grab_pre_warmed_net_v_connection_if_possible(*tts, pid)) { | |
| return; | |
| } | |
| } | |
| opt.local_port = _ua.get_txn()->get_outbound_port(); | |
| const IpAddr &outbound_ip = AF_INET6 == opt.ip_family ? _ua.get_txn()->get_outbound_ip6() : _ua.get_txn()->get_outbound_ip4(); | |
| if (outbound_ip.isValid()) { | |
| opt.addr_binding = NetVCOptions::INTF_ADDR; | |
| opt.local_ip = outbound_ip; | |
| } else if (_ua.get_txn()->is_outbound_transparent()) { | |
| opt.addr_binding = NetVCOptions::FOREIGN_ADDR; | |
| // Use effective_client_addr so that listeners with :pp-clnt bind the | |
| // outbound socket to the real client IP rather than the immediate TCP | |
| // peer; without :pp-clnt, effective_client_addr is the TCP peer, so | |
| // existing transparent deployments are unaffected. | |
| opt.local_ip = t_state.effective_client_addr; | |
| /* If the connection is server side transparent, we can bind to the | |
| port that the client chose instead of randomly assigning one at | |
| the proxy. This is controlled by the 'use_client_source_port' | |
| configuration parameter. | |
| */ | |
| NetVConnection *client_vc = _ua.get_txn()->get_netvc(); | |
| if (t_state.http_config_param->use_client_source_port && nullptr != client_vc) { | |
| opt.local_port = client_vc->get_remote_port(); | |
| } | |
| } | |
| } | |
| if (!t_state.is_websocket) { // if not websocket, then get scheme from server request | |
| int new_scheme_to_use = t_state.hdr_info.server_request.url_get()->scheme_get_wksidx(); | |
| // if the server_request url scheme was never set, try the client_request | |
| if (new_scheme_to_use < 0) { | |
| new_scheme_to_use = t_state.hdr_info.client_request.url_get()->scheme_get_wksidx(); | |
| } | |
| if (new_scheme_to_use >= 0) { // found a new scheme, use it | |
| scheme_to_use = new_scheme_to_use; | |
| } | |
| if (!raw || !tls_upstream) { | |
| tls_upstream = scheme_to_use == URL_WKSIDX_HTTPS; | |
| } | |
| } | |
| // draft-stenberg-httpbis-tcp recommends only enabling TFO on idempotent methods or | |
| // those with intervening protocol layers (eg. TLS). | |
| if (tls_upstream || HttpTransactHeaders::is_method_idempotent(t_state.method)) { | |
| opt.f_tcp_fastopen = (t_state.txn_conf->sock_option_flag_out & NetVCOptions::SOCK_OPT_TCP_FAST_OPEN); | |
| } | |
| opt.set_ssl_client_cert_name(t_state.txn_conf->ssl_client_cert_filename); | |
| opt.ssl_client_private_key_name = t_state.txn_conf->ssl_client_private_key_filename; | |
| opt.ssl_client_ca_cert_name = t_state.txn_conf->ssl_client_ca_cert_filename; | |
| opt.ssl_client_ca_cert_path = t_state.txn_conf->ssl_client_ca_cert_path; | |
| if (is_private()) { | |
| // If the connection to origin is private, don't try to negotiate the higher overhead H2 | |
| opt.alpn_protocols_array_size = -1; | |
| SMDbg(dbg_ctl_ssl_alpn, "Clear ALPN for private session"); | |
| } else if (t_state.txn_conf->ssl_client_alpn_protocols != nullptr) { | |
| opt.alpn_protocols_array_size = MAX_ALPN_STRING; | |
| SMDbg(dbg_ctl_ssl_alpn, "Setting ALPN to: %s", t_state.txn_conf->ssl_client_alpn_protocols); | |
| convert_alpn_to_wire_format(t_state.txn_conf->ssl_client_alpn_protocols, opt.alpn_protocols_array, | |
| opt.alpn_protocols_array_size); | |
| } | |
| ConnectingEntry *new_entry = nullptr; | |
| if (multiplexed_origin) { | |
| EThread *ethread = this_ethread(); | |
| if (nullptr != ethread->connecting_pool) { | |
| SMDbg(dbg_ctl_http_ss, "Queue multiplexed request"); | |
| new_entry = new ConnectingEntry(); | |
| new_entry->mutex = this->mutex; | |
| new_entry->ua_txn = _ua.get_txn(); | |
| SET_CONTINUATION_HANDLER(new_entry, &ConnectingEntry::state_http_server_open); | |
| new_entry->ipaddr.assign(&t_state.current.server->dst_addr.sa); | |
| new_entry->hostname = t_state.current.server->name; | |
| new_entry->sni = this->get_outbound_sni(); | |
| new_entry->cert_name = this->get_outbound_cert(); | |
| new_entry->is_no_plugin_tunnel = plugin_tunnel_type == HttpPluginTunnel_t::NONE; | |
| new_entry->enable_outbound_connection_tracking(t_state.outbound_conn_track_state.drop()); | |
| this->t_state.set_connect_fail(EIO); | |
| new_entry->connect_sms.insert(this); | |
| ethread->connecting_pool->m_ip_pool.insert(std::make_pair(new_entry->ipaddr, new_entry)); | |
| } | |
| } | |
| Continuation *cont = new_entry; | |
| if (!cont) { | |
| cont = this; | |
| } | |
| if (tls_upstream) { | |
| SMDbg(dbg_ctl_http, "calling sslNetProcessor.connect_re"); | |
| std::string_view sni_name = this->get_outbound_sni(); | |
| if (sni_name.length() > 0) { | |
| opt.set_sni_servername(sni_name.data(), sni_name.length()); | |
| } | |
| if (t_state.txn_conf->ssl_client_sni_policy != nullptr && | |
| !strcmp(t_state.txn_conf->ssl_client_sni_policy, "verify_with_name_source")) { | |
| // also set sni_hostname with host header from server request in this policy | |
| auto host{t_state.hdr_info.server_request.host_get()}; | |
| if (!host.empty()) { | |
| opt.set_sni_hostname(host.data(), static_cast<int>(host.length())); | |
| } | |
| } | |
| if (t_state.server_info.name) { | |
| opt.set_ssl_servername(t_state.server_info.name); | |
| } | |
| pending_action = sslNetProcessor.connect_re(cont, // state machine or ConnectingEntry | |
| &t_state.current.server->dst_addr.sa, // addr + port | |
| opt); | |
| } else { | |
| SMDbg(dbg_ctl_http, "calling netProcessor.connect_re"); | |
| pending_action = netProcessor.connect_re(cont, // state machine or ConnectingEntry | |
| &t_state.current.server->dst_addr.sa, // addr + port | |
| opt); | |
| } | |
| return; | |
| } | |
| int | |
| HttpSM::do_api_callout_internal() | |
| { | |
| switch (t_state.api_next_action) { | |
| case HttpTransact::StateMachineAction_t::API_SM_START: | |
| cur_hook_id = TS_HTTP_TXN_START_HOOK; | |
| break; | |
| case HttpTransact::StateMachineAction_t::API_PRE_REMAP: | |
| cur_hook_id = TS_HTTP_PRE_REMAP_HOOK; | |
| break; | |
| case HttpTransact::StateMachineAction_t::API_POST_REMAP: | |
| cur_hook_id = TS_HTTP_POST_REMAP_HOOK; | |
| break; | |
| case HttpTransact::StateMachineAction_t::API_TUNNEL_START: | |
| cur_hook_id = TS_HTTP_TUNNEL_START_HOOK; | |
| break; | |
| case HttpTransact::StateMachineAction_t::API_READ_REQUEST_HDR: | |
| cur_hook_id = TS_HTTP_READ_REQUEST_HDR_HOOK; | |
| break; | |
| case HttpTransact::StateMachineAction_t::REQUEST_BUFFER_READ_COMPLETE: | |
| cur_hook_id = TS_HTTP_REQUEST_BUFFER_READ_COMPLETE_HOOK; | |
| break; | |
| case HttpTransact::StateMachineAction_t::API_OS_DNS: | |
| cur_hook_id = TS_HTTP_OS_DNS_HOOK; | |
| break; | |
| case HttpTransact::StateMachineAction_t::API_SEND_REQUEST_HDR: | |
| cur_hook_id = TS_HTTP_SEND_REQUEST_HDR_HOOK; | |
| break; | |
| case HttpTransact::StateMachineAction_t::API_READ_CACHE_HDR: | |
| cur_hook_id = TS_HTTP_READ_CACHE_HDR_HOOK; | |
| break; | |
| case HttpTransact::StateMachineAction_t::API_CACHE_LOOKUP_COMPLETE: | |
| cur_hook_id = TS_HTTP_CACHE_LOOKUP_COMPLETE_HOOK; | |
| break; | |
| case HttpTransact::StateMachineAction_t::API_READ_RESPONSE_HDR: | |
| cur_hook_id = TS_HTTP_READ_RESPONSE_HDR_HOOK; | |
| break; | |
| case HttpTransact::StateMachineAction_t::API_SEND_RESPONSE_HDR: | |
| cur_hook_id = TS_HTTP_SEND_RESPONSE_HDR_HOOK; | |
| ATS_PROBE1(milestone_ua_begin_write, sm_id); | |
| milestones[TS_MILESTONE_UA_BEGIN_WRITE] = ink_get_hrtime(); | |
| break; | |
| case HttpTransact::StateMachineAction_t::API_SM_SHUTDOWN: | |
| if (callout_state == HttpApiState_t::IN_CALLOUT || callout_state == HttpApiState_t::DEFERED_SERVER_ERROR) { | |
| callout_state = HttpApiState_t::DEFERED_CLOSE; | |
| return 0; | |
| } else { | |
| cur_hook_id = TS_HTTP_TXN_CLOSE_HOOK; | |
| } | |
| break; | |
| default: | |
| cur_hook_id = static_cast<TSHttpHookID>(-1); | |
| ink_assert(!"not reached"); | |
| } | |
| hook_state.init(cur_hook_id, http_global_hooks, _ua.get_txn() ? _ua.get_txn()->feature_hooks() : nullptr, &api_hooks); | |
| cur_hook = nullptr; | |
| return state_api_callout(0, nullptr); | |
| } | |
| VConnection * | |
| HttpSM::do_post_transform_open() | |
| { | |
| ink_assert(post_transform_info.vc == nullptr); | |
| if (is_action_tag_set("http_post_nullt")) { | |
| txn_hook_add(TS_HTTP_REQUEST_TRANSFORM_HOOK, transformProcessor.null_transform(mutex.get())); | |
| } | |
| post_transform_info.vc = transformProcessor.open(this, api_hooks.get(TS_HTTP_REQUEST_TRANSFORM_HOOK)); | |
| if (post_transform_info.vc) { | |
| // Record the transform VC in our table | |
| post_transform_info.entry = vc_table.new_entry(); | |
| post_transform_info.entry->vc = post_transform_info.vc; | |
| post_transform_info.entry->vc_type = HttpVC_t::TRANSFORM_VC; | |
| } | |
| return post_transform_info.vc; | |
| } | |
| VConnection * | |
| HttpSM::do_transform_open() | |
| { | |
| ink_assert(transform_info.vc == nullptr); | |
| APIHook *hooks; | |
| if (is_action_tag_set("http_nullt")) { | |
| txn_hook_add(TS_HTTP_RESPONSE_TRANSFORM_HOOK, transformProcessor.null_transform(mutex.get())); | |
| } | |
| hooks = api_hooks.get(TS_HTTP_RESPONSE_TRANSFORM_HOOK); | |
| if (hooks) { | |
| transform_info.vc = transformProcessor.open(this, hooks); | |
| // Record the transform VC in our table | |
| transform_info.entry = vc_table.new_entry(); | |
| transform_info.entry->vc = transform_info.vc; | |
| transform_info.entry->vc_type = HttpVC_t::TRANSFORM_VC; | |
| } else { | |
| transform_info.vc = nullptr; | |
| } | |
| return transform_info.vc; | |
| } | |
| void | |
| HttpSM::mark_host_failure(ResolveInfo *info, ts_time time_down) | |
| { | |
| ink_assert(time_down != TS_TIME_ZERO); | |
| if (info->active == nullptr) { | |
| return; | |
| } | |
| char addrbuf[INET6_ADDRPORTSTRLEN]; | |
| ats_ip_nptop(&t_state.current.server->dst_addr.sa, addrbuf, sizeof(addrbuf)); | |
| const uint8_t max_connect_retries = HttpTransact::origin_server_connect_attempts_max_retries(&t_state); | |
| const ts_seconds fail_window = t_state.txn_conf->down_server_timeout; | |
| // Mark the host DOWN only after every attempt has failed. `max_connect_retries` counts only "retries", so the total attempt | |
| // budget is `max_connect_retries + 1` (the initial connect plus each retry). | |
| auto [down, fail_count] = info->active->increment_fail_count(time_down, max_connect_retries + 1, fail_window); | |
| if (down) { | |
| Metrics::Counter::increment(http_rsb.down_server_no_requests); | |
| char *url_str = t_state.hdr_info.client_request.url_string_get_ref(nullptr); | |
| std::string_view host_name{t_state.unmapped_url.host_get()}; | |
| swoc::bwprint(error_bw_buffer, "CONNECT : {::s} connecting to {} for host='{}' url='{}' fail_count='{}' marking down", | |
| swoc::bwf::Errno(t_state.current.server->connect_result), t_state.current.server->dst_addr, host_name, | |
| swoc::bwf::FirstOf(url_str, "<none>"), fail_count); | |
| Log::error("%s", error_bw_buffer.c_str()); | |
| SMDbg(dbg_ctl_http, "hostdb update marking IP: %s as down", addrbuf); | |
| ATS_PROBE2(hostdb_mark_ip_as_down, sm_id, addrbuf); | |
| } else { | |
| ATS_PROBE3(hostdb_inc_ip_failcount, sm_id, addrbuf, fail_count); | |
| SMDbg(dbg_ctl_http, "hostdb increment IP failcount %s to %d", addrbuf, fail_count); | |
| } | |
| } | |
| void | |
| HttpSM::set_ua_abort(HttpTransact::AbortState_t ua_abort, int event) | |
| { | |
| t_state.client_info.abort = ua_abort; | |
| switch (ua_abort) { | |
| case HttpTransact::ABORTED: | |
| // More detailed client side abort logging based on event | |
| switch (event) { | |
| case VC_EVENT_ERROR: | |
| t_state.squid_codes.log_code = SquidLogCode::ERR_CLIENT_READ_ERROR; | |
| break; | |
| case VC_EVENT_EOS: | |
| case VC_EVENT_ACTIVE_TIMEOUT: // Won't matter. Server will hangup | |
| case VC_EVENT_INACTIVITY_TIMEOUT: // Won't matter. Send back 408 | |
| // Fall-through | |
| default: | |
| t_state.squid_codes.log_code = SquidLogCode::ERR_CLIENT_ABORT; | |
| break; | |
| } | |
| break; | |
| default: | |
| // Handled here: | |
| // HttpTransact::ABORT_UNDEFINED, HttpTransact::DIDNOT_ABORT | |
| break; | |
| } | |
| // Set the connection attribute code for the client so that | |
| // we log the client finish code correctly | |
| switch (event) { | |
| case VC_EVENT_ACTIVE_TIMEOUT: | |
| t_state.client_info.state = HttpTransact::ACTIVE_TIMEOUT; | |
| break; | |
| case VC_EVENT_INACTIVITY_TIMEOUT: | |
| t_state.client_info.state = HttpTransact::INACTIVE_TIMEOUT; | |
| break; | |
| case VC_EVENT_ERROR: | |
| t_state.client_info.state = HttpTransact::CONNECTION_ERROR; | |
| break; | |
| case HTTP_TUNNEL_EVENT_PARSE_ERROR: | |
| t_state.client_info.state = HttpTransact::PARSE_ERROR; | |
| break; | |
| } | |
| ATS_PROBE4(http_ua_abort, sm_id, event, static_cast<int>(ua_abort), static_cast<int>(t_state.client_info.state)); | |
| } | |
| // void HttpSM::release_server_session() | |
| // | |
| // Called when we are not tunneling a response from the | |
| // server. If the session is keep alive, release it back to the | |
| // shared pool, otherwise close it | |
| // | |
| void | |
| HttpSM::release_server_session(bool serve_from_cache) | |
| { | |
| if (server_txn == nullptr) { | |
| return; | |
| } | |
| if (TS_SERVER_SESSION_SHARING_MATCH_NONE != t_state.txn_conf->server_session_sharing_match && t_state.current.server != nullptr && | |
| t_state.current.server->keep_alive == HTTPKeepAlive::KEEPALIVE && t_state.hdr_info.server_response.valid() && | |
| t_state.hdr_info.server_request.valid() && | |
| (t_state.hdr_info.server_response.status_get() == HTTPStatus::NOT_MODIFIED || | |
| (t_state.hdr_info.server_request.method_get_wksidx() == HTTP_WKSIDX_HEAD && | |
| t_state.www_auth_content != HttpTransact::CacheAuth_t::NONE)) && | |
| plugin_tunnel_type == HttpPluginTunnel_t::NONE && (!server_entry || !server_entry->eos)) { | |
| if (t_state.www_auth_content == HttpTransact::CacheAuth_t::NONE || serve_from_cache == false) { | |
| // Must explicitly set the keep_alive_no_activity time before doing the release | |
| server_txn->set_inactivity_timeout(HRTIME_SECONDS(t_state.txn_conf->keep_alive_no_activity_timeout_out)); | |
| server_txn->release(); | |
| } else { | |
| // an authenticated server connection - attach to the local client | |
| // we are serving from cache for the current transaction | |
| t_state.www_auth_content = HttpTransact::CacheAuth_t::SERVE; | |
| _ua.get_txn()->attach_server_session(static_cast<PoolableSession *>(server_txn->get_proxy_ssn()), false); | |
| } | |
| } else { | |
| server_txn->do_io_close(); | |
| if (TS_SERVER_SESSION_SHARING_MATCH_NONE == t_state.txn_conf->server_session_sharing_match) { | |
| Metrics::Counter::increment(http_rsb.origin_shutdown_release_no_sharing); | |
| } else if (t_state.current.server == nullptr) { | |
| Metrics::Counter::increment(http_rsb.origin_shutdown_release_no_server); | |
| } else if (t_state.current.server->keep_alive != HTTPKeepAlive::KEEPALIVE) { | |
| Metrics::Counter::increment(http_rsb.origin_shutdown_release_no_keep_alive); | |
| } else if (!t_state.hdr_info.server_response.valid()) { | |
| Metrics::Counter::increment(http_rsb.origin_shutdown_release_invalid_response); | |
| } else if (!t_state.hdr_info.server_request.valid()) { | |
| Metrics::Counter::increment(http_rsb.origin_shutdown_release_invalid_request); | |
| } else if (t_state.hdr_info.server_response.status_get() != HTTPStatus::NOT_MODIFIED && | |
| (t_state.hdr_info.server_request.method_get_wksidx() != HTTP_WKSIDX_HEAD || | |
| t_state.www_auth_content == HttpTransact::CacheAuth_t::NONE)) { | |
| Metrics::Counter::increment(http_rsb.origin_shutdown_release_modified); | |
| } else { | |
| Metrics::Counter::increment(http_rsb.origin_shutdown_release_misc); | |
| } | |
| } | |
| if (server_entry) { | |
| server_entry->vc = nullptr; | |
| server_entry->read_vio = nullptr; | |
| server_entry->write_vio = nullptr; | |
| server_entry = nullptr; | |
| } | |
| } | |
| // A peer's not-processed assertion permits a retry only if we can reproduce | |
| // the request. This includes safe methods with bodies: method safety does not | |
| // make an exhausted body reader replayable. Unbuffered, incomplete, unknown-length, | |
| // chunked and transformed bodies are excluded. Admission rearms a complete copy; | |
| // body rejection latches origin_retry_body_unavailable to block later retries. | |
| // Both outcomes increment their corresponding origin retry counters. | |
| bool | |
| HttpSM::prepare_for_origin_retry() | |
| { | |
| if (server_txn == nullptr || !server_txn->is_safe_to_retry()) { | |
| return false; | |
| } | |
| bool const has_body = server_request_body_bytes > 0 || | |
| (_ua.get_txn() != nullptr && _ua.get_txn()->has_request_body(t_state.hdr_info.request_content_length, | |
| t_state.client_info.transfer_encoding == | |
| HttpTransact::TransferEncoding_t::CHUNKED)); | |
| if (has_body) { | |
| int64_t const length = t_state.hdr_info.request_content_length; | |
| // A done buffer is not proof of completeness: HttpTunnel also marks it done | |
| // on EOS. Keep the exact-length check even when upstream paths reject early. | |
| if (post_transform_info.vc || t_state.client_info.transfer_encoding == HttpTransact::TransferEncoding_t::CHUNKED || | |
| length < 0 || length == HTTP_UNDEFINED_CL || !this->is_postbuf_valid() || !this->get_postbuf_done() || | |
| this->postbuf_buffer_avail() != length) { | |
| origin_retry_body_unavailable = true; | |
| Metrics::Counter::increment(http_rsb.origin_retry_body_unavailable); | |
| SMDbg(dbg_ctl_http, "Origin retry denied: complete replayable request body unavailable"); | |
| return false; | |
| } | |
| is_buffering_request_body = true; | |
| } | |
| Metrics::Counter::increment(http_rsb.origin_retry_admitted); | |
| SMDbg(dbg_ctl_http, "Origin not-processed retry admitted: method=%d body=%d", t_state.method, has_body); | |
| return true; | |
| } | |
| // void HttpSM::handle_post_failure() | |
| // | |
| // We failed in our attempt post (or put) a document | |
| // to the server. Two cases happen here. The normal | |
| // one is the server is down, in which case we ought to | |
| // return an error to the client. The second one is | |
| // stupid. The server returned a response without reading | |
| // all the post data. In order to be as transparent as | |
| // possible process the server's response | |
| void | |
| HttpSM::handle_post_failure() | |
| { | |
| STATE_ENTER(handle_post_failure, VC_EVENT_NONE); | |
| ink_assert(_ua.get_entry()->vc == _ua.get_txn()); | |
| ink_assert(is_waiting_for_full_body || server_entry->eos == true); | |
| if (is_waiting_for_full_body) { | |
| call_transact_and_set_next_state(HttpTransact::Forbidden); | |
| return; | |
| } | |
| // First order of business is to clean up from | |
| // the tunnel | |
| // note: since the tunnel is providing the buffer for a lingering | |
| // client read (for abort watching purposes), we need to stop | |
| // the read | |
| if (false == t_state.redirect_info.redirect_in_process) { | |
| _ua.get_entry()->read_vio = _ua.get_txn()->do_io_read(this, 0, nullptr); | |
| } | |
| _ua.get_entry()->in_tunnel = false; | |
| server_entry->in_tunnel = false; | |
| // Preserve a complete buffered body only when the origin has not responded | |
| // and explicitly reports that this request was not processed. Otherwise the | |
| // copy may be incomplete, so discard it along with redirect eligibility. | |
| bool const retry = this->prepare_for_origin_retry(); | |
| if (!retry) { | |
| this->disable_redirect(); | |
| } | |
| // Don't even think about doing keep-alive after this debacle | |
| t_state.client_info.keep_alive = HTTPKeepAlive::NO_KEEPALIVE; | |
| t_state.current.server->keep_alive = HTTPKeepAlive::NO_KEEPALIVE; | |
| tunnel.deallocate_buffers(); | |
| tunnel.reset(); | |
| // Server is down | |
| if (t_state.current.state == HttpTransact::STATE_UNDEFINED || t_state.current.state == HttpTransact::CONNECTION_ALIVE) { | |
| t_state.set_connect_fail(server_txn->get_netvc()->lerrno); | |
| t_state.current.state = retry ? HttpTransact::CONNECTION_ERROR : HttpTransact::CONNECTION_CLOSED; | |
| } else if (retry && t_state.current.state == HttpTransact::CONNECTION_CLOSED) { | |
| // The upload tunnel already recorded EPIPE; a clean FIN can have lerrno == 0. | |
| t_state.current.state = HttpTransact::CONNECTION_ERROR; | |
| } | |
| call_transact_and_set_next_state(HttpTransact::HandleResponse); | |
| } | |
| // void HttpSM::handle_http_server_open() | |
| // | |
| // The server connection is now open. If there is a POST or PUT, | |
| // we need setup a transform is there is one otherwise we need | |
| // to send the request header | |
| // | |
| void | |
| HttpSM::handle_http_server_open() | |
| { | |
| // [bwyatt] applying per-transaction OS netVC options here | |
| // IFF they differ from the netVC's current options. | |
| // This should keep this from being redundant on a | |
| // server session's first transaction. | |
| if (nullptr != server_txn) { | |
| NetVConnection *vc = server_txn->get_netvc(); | |
| if (vc) { | |
| server_connection_provided_cert = vc->provided_cert(); | |
| if (vc->options.sockopt_flags != t_state.txn_conf->sock_option_flag_out || | |
| vc->options.packet_mark != t_state.txn_conf->sock_packet_mark_out || | |
| vc->options.packet_tos != t_state.txn_conf->sock_packet_tos_out || | |
| vc->options.packet_notsent_lowat != t_state.txn_conf->sock_packet_notsent_lowat) { | |
| vc->options.sockopt_flags = t_state.txn_conf->sock_option_flag_out; | |
| vc->options.packet_mark = t_state.txn_conf->sock_packet_mark_out; | |
| vc->options.packet_tos = t_state.txn_conf->sock_packet_tos_out; | |
| vc->options.packet_notsent_lowat = t_state.txn_conf->sock_packet_notsent_lowat; | |
| vc->apply_options(); | |
| } | |
| } | |
| server_txn->set_inactivity_timeout(get_server_inactivity_timeout()); | |
| int method = t_state.hdr_info.server_request.method_get_wksidx(); | |
| if (method != HTTP_WKSIDX_TRACE && | |
| server_txn->has_request_body(t_state.hdr_info.request_content_length, | |
| t_state.client_info.transfer_encoding == HttpTransact::TransferEncoding_t::CHUNKED) && | |
| do_post_transform_open()) { | |
| do_setup_client_request_body_tunnel( | |
| HttpVC_t::TRANSFORM_VC); /* This doesn't seem quite right. Should be sending the request header */ | |
| } else { | |
| setup_server_send_request_api(); | |
| } | |
| } else { | |
| ink_release_assert(!"No server_txn"); | |
| } | |
| } | |
| // void HttpSM::handle_server_setup_error(int event, void* data) | |
| // | |
| // Handles setting t_state.current.state and calling | |
| // Transact in between opening an origin server connection | |
| // and receiving the response header (in the case of the | |
| // POST, a post tunnel happens in between the sending | |
| // request header and reading the response header | |
| // | |
| void | |
| HttpSM::handle_server_setup_error(int event, void *data) | |
| { | |
| VIO *vio = static_cast<VIO *>(data); | |
| ink_assert(vio != nullptr); | |
| STATE_ENTER(handle_server_setup_error, event); | |
| // If there is POST or PUT tunnel wait for the tunnel | |
| // to figure out that things have gone to hell | |
| if (tunnel.is_tunnel_active()) { | |
| ink_assert(server_entry->read_vio == data || server_entry->write_vio == data); | |
| SMDbg(dbg_ctl_http, "forwarding event %s to post tunnel", HttpDebugNames::get_event_name(event)); | |
| HttpTunnelConsumer *c = tunnel.get_consumer(server_entry->vc); | |
| // it is possible only user agent post->post transform is set up | |
| // this happened for Linux iocore where NET_EVENT_OPEN was returned | |
| // for a non-existing listening port. the hack is to pass the error | |
| // event for server connection to post_transform_info | |
| if (c == nullptr && post_transform_info.vc) { | |
| c = tunnel.get_consumer(post_transform_info.vc); | |
| // c->handler_state = HTTP_SM_TRANSFORM_FAIL; | |
| // No point in proceeding if there is no consumer | |
| // Do we need to do additional clean up in the c == NULL case? | |
| if (c != nullptr) { | |
| HttpTunnelProducer *ua_producer = c->producer; | |
| ink_assert(_ua.get_entry()->vc == ua_producer->vc); | |
| _ua.get_entry()->vc_read_handler = &HttpSM::state_watch_for_client_abort; | |
| _ua.get_entry()->vc_write_handler = &HttpSM::state_watch_for_client_abort; | |
| _ua.get_entry()->read_vio = ua_producer->vc->do_io_read(this, INT64_MAX, c->producer->read_buffer); | |
| ua_producer->vc->do_io_shutdown(IO_SHUTDOWN_READ); | |
| ua_producer->alive = false; | |
| ua_producer->handler_state = static_cast<int>(HttpSmPost_t::SERVER_FAIL); | |
| tunnel.handleEvent(VC_EVENT_ERROR, c->write_vio); | |
| return; | |
| } | |
| } else { | |
| // c could be null here as well | |
| if (c != nullptr) { | |
| tunnel.handleEvent(event, c->write_vio); | |
| return; | |
| } | |
| } | |
| // If there is no consumer, let the event pass through to shutdown | |
| } else { | |
| if (post_transform_info.vc) { | |
| HttpTunnelConsumer *c = tunnel.get_consumer(post_transform_info.vc); | |
| if (c && c->handler_state == HTTP_SM_TRANSFORM_OPEN) { | |
| vc_table.cleanup_entry(post_transform_info.entry); | |
| post_transform_info.entry = nullptr; | |
| tunnel.deallocate_buffers(); | |
| tunnel.reset(); | |
| } | |
| } | |
| } | |
| [[maybe_unused]] UnixNetVConnection *dbg_vc = nullptr; | |
| switch (event) { | |
| case VC_EVENT_EOS: | |
| t_state.current.state = this->prepare_for_origin_retry() ? HttpTransact::CONNECTION_ERROR : HttpTransact::CONNECTION_CLOSED; | |
| t_state.set_connect_fail(EPIPE); | |
| break; | |
| case VC_EVENT_ERROR: | |
| t_state.current.state = HttpTransact::CONNECTION_ERROR; | |
| t_state.set_connect_fail(server_txn->get_netvc()->lerrno); | |
| break; | |
| case VC_EVENT_ACTIVE_TIMEOUT: | |
| t_state.set_connect_fail(ETIMEDOUT); | |
| t_state.current.state = HttpTransact::ACTIVE_TIMEOUT; | |
| break; | |
| case VC_EVENT_INACTIVITY_TIMEOUT: | |
| // If we're writing the request and get an inactivity timeout | |
| // before any bytes are written, the connection to the | |
| // server failed | |
| // In case of TIMEOUT, the iocore sends back | |
| // server_entry->read_vio instead of the write_vio | |
| t_state.set_connect_fail(ETIMEDOUT); | |
| if (server_entry->write_vio && server_entry->write_vio->nbytes > 0 && server_entry->write_vio->ndone == 0) { | |
| t_state.current.state = HttpTransact::CONNECTION_ERROR; | |
| } else { | |
| t_state.current.state = HttpTransact::INACTIVE_TIMEOUT; | |
| } | |
| break; | |
| default: | |
| ink_release_assert(0); | |
| } | |
| ATS_PROBE3(http_server_setup_error, sm_id, event, static_cast<int>(t_state.current.state)); | |
| if (event == VC_EVENT_INACTIVITY_TIMEOUT || event == VC_EVENT_ERROR || event == VC_EVENT_EOS) { | |
| // Clean up the vc_table entry so any events in play to the timed out server vio | |
| // don't get handled. The connection isn't there. | |
| if (server_entry) { | |
| ink_assert(server_entry->vc_type == HttpVC_t::SERVER_VC); | |
| vc_table.cleanup_entry(server_entry); | |
| server_entry = nullptr; | |
| } | |
| } | |
| // Closedown server connection and deallocate buffers | |
| ink_assert(!server_entry || server_entry->in_tunnel == false); | |
| // if we are waiting on a plugin callout for | |
| // HTTP_API_SEND_REQUEST_HDR defer calling transact until | |
| // after we've finished processing the plugin callout | |
| switch (callout_state) { | |
| case HttpApiState_t::NO_CALLOUT: | |
| // Normal fast path case, no api callouts in progress | |
| break; | |
| case HttpApiState_t::IN_CALLOUT: | |
| case HttpApiState_t::DEFERED_SERVER_ERROR: | |
| // Callout in progress note that we are in deferring | |
| // the server error | |
| callout_state = HttpApiState_t::DEFERED_SERVER_ERROR; | |
| return; | |
| case HttpApiState_t::DEFERED_CLOSE: | |
| // The user agent has shutdown killing the sm | |
| // but we are stuck waiting for the server callout | |
| // to finish so do nothing here. We don't care | |
| // about the server connection at this and are | |
| // just waiting till we can execute the close hook | |
| return; | |
| default: | |
| ink_release_assert(0); | |
| } | |
| call_transact_and_set_next_state(HttpTransact::HandleResponse); | |
| } | |
| void | |
| HttpSM::setup_transform_to_server_transfer() | |
| { | |
| ink_assert(post_transform_info.vc != nullptr); | |
| ink_assert(post_transform_info.entry->vc == post_transform_info.vc); | |
| int64_t nbytes = t_state.hdr_info.transform_request_cl; | |
| int64_t alloc_index = buffer_size_to_index(nbytes, t_state.http_config_param->max_payload_iobuf_index); | |
| MIOBuffer *post_buffer = new_MIOBuffer(alloc_index); | |
| IOBufferReader *buf_start = post_buffer->alloc_reader(); | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::tunnel_handler_post); | |
| HttpTunnelConsumer *c = tunnel.get_consumer(post_transform_info.vc); | |
| HttpTunnelProducer *p = tunnel.add_producer(post_transform_info.vc, nbytes, buf_start, &HttpSM::tunnel_handler_transform_read, | |
| HttpTunnelType_t::TRANSFORM, "post transform"); | |
| tunnel.chain(c, p); | |
| post_transform_info.entry->in_tunnel = true; | |
| tunnel.add_consumer(server_entry->vc, post_transform_info.vc, &HttpSM::tunnel_handler_post_server, HttpTunnelType_t::HTTP_SERVER, | |
| "http server post"); | |
| server_entry->in_tunnel = true; | |
| tunnel.tunnel_run(p); | |
| } | |
| void | |
| HttpSM::do_drain_request_body(HTTPHdr &response) | |
| { | |
| int64_t content_length = t_state.hdr_info.client_request.get_content_length(); | |
| int64_t avail = _ua.get_txn()->get_remote_reader()->read_avail(); | |
| // Consuming the client request buffer is only safe when no request-body tunnel is active: a live | |
| // producer could still be reading from it, so consuming here would race and desync the connection. | |
| // Callers today reach this after the request-body tunnel is done, so the guard is normally a no-op; | |
| // keep it so a future path with a live tunnel drops keep-alive instead of double-consuming the reader. | |
| if (tunnel.is_tunnel_active()) { | |
| SMDbg(dbg_ctl_http, "request-body tunnel still active, setting the response to non-keepalive"); | |
| goto close_connection; | |
| } | |
| if (t_state.client_info.transfer_encoding == HttpTransact::TransferEncoding_t::CHUNKED) { | |
| SMDbg(dbg_ctl_http, "Chunked body, setting the response to non-keepalive"); | |
| goto close_connection; | |
| } | |
| if (content_length > 0) { | |
| if (avail >= content_length) { | |
| SMDbg(dbg_ctl_http, "entire body is in the buffer, consuming"); | |
| int64_t act_on = (avail < content_length) ? avail : content_length; | |
| client_request_body_bytes = act_on; | |
| _ua.get_txn()->get_remote_reader()->consume(act_on); | |
| } else { | |
| SMDbg(dbg_ctl_http, "entire body is not in the buffer, setting the response to non-keepalive"); | |
| goto close_connection; | |
| } | |
| } | |
| return; | |
| close_connection: | |
| t_state.client_info.keep_alive = HTTPKeepAlive::NO_KEEPALIVE; | |
| _ua.get_txn()->set_close_connection(response); | |
| } | |
| void | |
| HttpSM::do_setup_client_request_body_tunnel(HttpVC_t to_vc_type) | |
| { | |
| if (!_ua.get_txn()->has_request_body(t_state.hdr_info.request_content_length, | |
| t_state.client_info.transfer_encoding == HttpTransact::TransferEncoding_t::CHUNKED)) { | |
| // No tunnel is needed to transfer 0 bytes or when no request body is present. | |
| // Simply return without setting up a tunnel nor any of the other related logic around request bodies. | |
| return; | |
| } | |
| bool chunked = t_state.client_info.transfer_encoding == HttpTransact::TransferEncoding_t::CHUNKED || | |
| t_state.hdr_info.request_content_length == HTTP_UNDEFINED_CL; | |
| bool post_redirect = false; | |
| HttpTunnelProducer *p = nullptr; | |
| // YTS Team, yamsat Plugin | |
| // if redirect_in_process and redirection is enabled add static producer | |
| if ((is_buffering_request_body && this->_postbuf.is_valid()) || // Make sure we have a valid buffer in case is buffering. | |
| (t_state.redirect_info.redirect_in_process && enable_redirection && this->_postbuf.is_valid())) { | |
| post_redirect = true; | |
| // copy the post data into a new producer buffer for static producer | |
| MIOBuffer *postdata_producer_buffer = new_empty_MIOBuffer(t_state.http_config_param->max_payload_iobuf_index); | |
| IOBufferReader *postdata_producer_reader = postdata_producer_buffer->alloc_reader(); | |
| postdata_producer_buffer->write(this->_postbuf.postdata_copy_buffer_start); | |
| int64_t post_bytes = chunked ? INT64_MAX : t_state.hdr_info.request_content_length; | |
| transferred_bytes = post_bytes; | |
| p = tunnel.add_producer(HTTP_TUNNEL_STATIC_PRODUCER, post_bytes, postdata_producer_reader, (HttpProducerHandler) nullptr, | |
| HttpTunnelType_t::STATIC, "redirect static agent post"); | |
| } else { | |
| int64_t alloc_index; | |
| // content length is undefined, use default buffer size | |
| if (t_state.hdr_info.request_content_length == HTTP_UNDEFINED_CL) { | |
| alloc_index = static_cast<int>(t_state.txn_conf->default_buffer_size_index); | |
| if (alloc_index < MIN_CONFIG_BUFFER_SIZE_INDEX || alloc_index > MAX_BUFFER_SIZE_INDEX) { | |
| alloc_index = DEFAULT_REQUEST_BUFFER_SIZE_INDEX; | |
| } | |
| } else { | |
| alloc_index = | |
| buffer_size_to_index(t_state.hdr_info.request_content_length, t_state.http_config_param->max_payload_iobuf_index); | |
| } | |
| MIOBuffer *post_buffer = new_MIOBuffer(alloc_index); | |
| IOBufferReader *buf_start = post_buffer->alloc_reader(); | |
| int64_t post_bytes = chunked ? INT64_MAX : t_state.hdr_info.request_content_length; | |
| if (enable_redirection) { | |
| this->_postbuf.init(post_buffer->clone_reader(buf_start)); | |
| } | |
| // Note: Many browsers, Netscape and IE included send two extra | |
| // bytes (CRLF) at the end of the post. We just ignore those | |
| // bytes since the sending them is not spec | |
| // Next order of business if copy the remaining data from the | |
| // header buffer into new buffer | |
| int64_t num_body_bytes = 0; | |
| // If is_using_post_buffer has been used, then client_request_body_bytes | |
| // will have already been sent in wait_for_full_body and there will be | |
| // zero bytes in this user agent buffer. We don't want to clobber | |
| // client_request_body_bytes with a zero value here in those cases. | |
| if (client_request_body_bytes > 0) { | |
| num_body_bytes = client_request_body_bytes; | |
| } else { | |
| num_body_bytes = post_buffer->write(_ua.get_txn()->get_remote_reader(), | |
| chunked ? _ua.get_txn()->get_remote_reader()->read_avail() : post_bytes); | |
| } | |
| // Don't consume post_bytes here from _ua.get_txn()->get_remote_reader() since | |
| // we are not sure how many bytes the tunnel will use yet. Wait until | |
| // HttpSM::tunnel_handler_post_ua to consume the bytes. | |
| // The user agent has already sent all it has | |
| if (_ua.get_txn()->is_read_closed()) { | |
| post_bytes = num_body_bytes; | |
| } | |
| p = tunnel.add_producer(_ua.get_entry()->vc, post_bytes - transferred_bytes, buf_start, &HttpSM::tunnel_handler_post_ua, | |
| HttpTunnelType_t::HTTP_CLIENT, "user agent post"); | |
| } | |
| _ua.get_entry()->in_tunnel = true; | |
| switch (to_vc_type) { | |
| case HttpVC_t::TRANSFORM_VC: | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::state_request_wait_for_transform_read); | |
| ink_assert(post_transform_info.entry != nullptr); | |
| ink_assert(post_transform_info.entry->vc == post_transform_info.vc); | |
| tunnel.add_consumer(post_transform_info.entry->vc, _ua.get_entry()->vc, &HttpSM::tunnel_handler_transform_write, | |
| HttpTunnelType_t::TRANSFORM, "post transform"); | |
| post_transform_info.entry->in_tunnel = true; | |
| break; | |
| case HttpVC_t::SERVER_VC: | |
| // YTS Team, yamsat Plugin | |
| // When redirect in process is true and redirection is enabled | |
| // add http server as the consumer | |
| if (post_redirect) { | |
| chunked = false; | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::tunnel_handler_for_partial_post); | |
| tunnel.add_consumer(server_entry->vc, HTTP_TUNNEL_STATIC_PRODUCER, &HttpSM::tunnel_handler_post_server, | |
| HttpTunnelType_t::HTTP_SERVER, "redirect http server post"); | |
| } else { | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::tunnel_handler_post); | |
| tunnel.add_consumer(server_entry->vc, _ua.get_entry()->vc, &HttpSM::tunnel_handler_post_server, HttpTunnelType_t::HTTP_SERVER, | |
| "http server post"); | |
| } | |
| server_entry->in_tunnel = true; | |
| break; | |
| default: | |
| ink_release_assert(0); | |
| break; | |
| } | |
| this->setup_client_request_plugin_agents(p); | |
| // The user agent and origin may support chunked (HTTP/1.1) or not (HTTP/2) | |
| if (chunked) { | |
| bool const drop_chunked_trailers = t_state.http_config_param->oride.http_drop_chunked_trailers == 1; | |
| bool const parse_chunk_strictly = t_state.http_config_param->oride.http_strict_chunk_parsing == 1; | |
| if (_ua.get_txn()->is_chunked_encoding_supported()) { | |
| if (server_txn->is_chunked_encoding_supported()) { | |
| tunnel.set_producer_chunking_action(p, 0, TunnelChunkingAction_t::PASSTHRU_CHUNKED_CONTENT, drop_chunked_trailers, | |
| parse_chunk_strictly); | |
| } else { | |
| tunnel.set_producer_chunking_action(p, 0, TunnelChunkingAction_t::DECHUNK_CONTENT, drop_chunked_trailers, | |
| parse_chunk_strictly); | |
| tunnel.set_producer_chunking_size(p, 0); | |
| } | |
| } else { | |
| if (server_txn->is_chunked_encoding_supported()) { | |
| tunnel.set_producer_chunking_action(p, 0, TunnelChunkingAction_t::CHUNK_CONTENT, drop_chunked_trailers, | |
| parse_chunk_strictly); | |
| tunnel.set_producer_chunking_size(p, 0); | |
| } else { | |
| tunnel.set_producer_chunking_action(p, 0, TunnelChunkingAction_t::PASSTHRU_DECHUNKED_CONTENT, drop_chunked_trailers, | |
| parse_chunk_strictly); | |
| } | |
| } | |
| } | |
| _ua.get_txn()->set_inactivity_timeout(HRTIME_SECONDS(t_state.txn_conf->transaction_no_activity_timeout_in)); | |
| server_txn->set_inactivity_timeout(get_server_inactivity_timeout()); | |
| tunnel.tunnel_run(p); | |
| // If we're half closed, we got a FIN from the client. Forward it on to the origin server | |
| // now that we have the tunnel operational. | |
| // HttpTunnel could broken due to bad chunked data and close all vc by chain_abort_all(). | |
| if (static_cast<HttpSmPost_t>(p->handler_state) != HttpSmPost_t::UA_FAIL && _ua.get_txn()->get_half_close_flag()) { | |
| p->vc->do_io_shutdown(IO_SHUTDOWN_READ); | |
| } | |
| } | |
| // void HttpSM::perform_transform_cache_write_action() | |
| // | |
| // Called to do cache write from the transform | |
| // | |
| void | |
| HttpSM::perform_transform_cache_write_action() | |
| { | |
| SMDbg(dbg_ctl_http, "%s", HttpDebugNames::get_cache_action_name(t_state.cache_info.action)); | |
| if (t_state.range_setup != HttpTransact::RangeSetup_t::NONE) { | |
| SMDbg(dbg_ctl_http, "perform_transform_cache_write_action %s (with range setup)", | |
| HttpDebugNames::get_cache_action_name(t_state.cache_info.action)); | |
| } | |
| switch (t_state.cache_info.transform_action) { | |
| case HttpTransact::CacheAction_t::NO_ACTION: { | |
| // Nothing to do | |
| transform_cache_sm.end_both(); | |
| break; | |
| } | |
| case HttpTransact::CacheAction_t::WRITE: { | |
| if (t_state.api_info.cache_untransformed == false) { | |
| transform_cache_sm.close_read(); | |
| t_state.cache_info.transform_write_status = HttpTransact::CacheWriteStatus_t::IN_PROGRESS; | |
| setup_cache_write_transfer(&transform_cache_sm, transform_info.entry->vc, &t_state.cache_info.transform_store, | |
| client_response_hdr_bytes, "cache write t"); | |
| } | |
| break; | |
| } | |
| default: | |
| ink_release_assert(0); | |
| break; | |
| } | |
| } | |
| // void HttpSM::perform_cache_write_action() | |
| // | |
| // Called to do cache write, delete and updates based | |
| // on s->cache_info.action. Does not setup cache | |
| // read tunnels | |
| // | |
| void | |
| HttpSM::perform_cache_write_action() | |
| { | |
| SMDbg(dbg_ctl_http, "%s", HttpDebugNames::get_cache_action_name(t_state.cache_info.action)); | |
| switch (t_state.cache_info.action) { | |
| case HttpTransact::CacheAction_t::NO_ACTION: | |
| { | |
| // Nothing to do | |
| cache_sm.end_both(); | |
| break; | |
| } | |
| case HttpTransact::CacheAction_t::SERVE: { | |
| cache_sm.abort_write(); | |
| break; | |
| } | |
| case HttpTransact::CacheAction_t::DELETE: { | |
| // Write close deletes the old alternate | |
| cache_sm.close_write(); | |
| // That reached only one of the two keys the object can live under while | |
| // the compatibility lookup is enabled. | |
| do_cache_delete_compat_alts(); | |
| cache_sm.close_read(); | |
| t_state.cache_info.write_lock_state = HttpTransact::CacheWriteLock_t::INIT; | |
| break; | |
| } | |
| case HttpTransact::CacheAction_t::SERVE_AND_DELETE: { | |
| // FIX ME: need to set up delete for after cache write has | |
| // completed | |
| break; | |
| } | |
| case HttpTransact::CacheAction_t::SERVE_AND_UPDATE: { | |
| issue_cache_update(); | |
| break; | |
| } | |
| case HttpTransact::CacheAction_t::UPDATE: { | |
| cache_sm.close_read(); | |
| issue_cache_update(); | |
| break; | |
| } | |
| case HttpTransact::CacheAction_t::WRITE: | |
| case HttpTransact::CacheAction_t::REPLACE: | |
| // Fix need to set up delete for after cache write has | |
| // completed | |
| if (transform_info.entry == nullptr || t_state.api_info.cache_untransformed == true) { | |
| cache_sm.close_read(); | |
| t_state.cache_info.write_status = HttpTransact::CacheWriteStatus_t::IN_PROGRESS; | |
| // Decide how many header bytes the cache-write consumer should skip. | |
| // When a transform is present, setup_server_transfer_to_transform() | |
| // creates the tunnel buffer with hdr_size=0, so the buffer contains | |
| // only body data - no response headers to skip. When no transform is | |
| // present, setup_server_transfer() writes the response header into the | |
| // buffer, so skip_bytes must equal client_response_hdr_bytes. | |
| int64_t cache_write_skip = (transform_info.entry == nullptr) ? client_response_hdr_bytes : 0; | |
| setup_cache_write_transfer(&cache_sm, server_entry->vc, &t_state.cache_info.object_store, cache_write_skip, "cache write"); | |
| } else { | |
| // We are not caching the untransformed. We might want to | |
| // use the cache writevc to cache the transformed copy | |
| ink_assert(transform_cache_sm.cache_write_vc == nullptr); | |
| transform_cache_sm.cache_write_vc = cache_sm.cache_write_vc; | |
| cache_sm.cache_write_vc = nullptr; | |
| } | |
| break; | |
| default: | |
| ink_release_assert(0); | |
| break; | |
| } | |
| } | |
| void | |
| HttpSM::issue_cache_update() | |
| { | |
| ink_assert(cache_sm.cache_write_vc != nullptr); | |
| if (cache_sm.cache_write_vc) { | |
| t_state.cache_info.object_store.request_sent_time_set(t_state.request_sent_time); | |
| t_state.cache_info.object_store.response_received_time_set(t_state.response_received_time); | |
| ink_assert(t_state.cache_info.object_store.request_sent_time_get() > 0); | |
| ink_assert(t_state.cache_info.object_store.response_received_time_get() > 0); | |
| cache_sm.cache_write_vc->set_http_info(&t_state.cache_info.object_store); | |
| t_state.cache_info.object_store.clear(); | |
| } | |
| // Now close the write which commits the update | |
| cache_sm.close_write(); | |
| t_state.cache_info.write_lock_state = HttpTransact::CacheWriteLock_t::INIT; | |
| } | |
| int | |
| HttpSM::write_header_into_buffer(HTTPHdr *h, MIOBuffer *b) | |
| { | |
| int dumpoffset; | |
| int done; | |
| dumpoffset = 0; | |
| do { | |
| IOBufferBlock *block = b->get_current_block(); | |
| int bufindex = 0; | |
| int tmp = dumpoffset; | |
| ink_assert(block->write_avail() > 0); | |
| done = h->print(block->start(), block->write_avail(), &bufindex, &tmp); | |
| dumpoffset += bufindex; | |
| ink_assert(bufindex > 0); | |
| b->fill(bufindex); | |
| if (!done) { | |
| b->add_block(); | |
| } | |
| } while (!done); | |
| return dumpoffset; | |
| } | |
| void | |
| HttpSM::attach_server_session() | |
| { | |
| hsm_release_assert(server_entry == nullptr); | |
| // In the h1 only origin version, the transact_count was updated after making this assignment. | |
| // The SSN-TXN-COUNT option in header rewrite relies on this fact, so we decrement here so the | |
| // plugin API interface is consistent as we move to more protocols to origin | |
| server_transact_count = server_txn->get_proxy_ssn()->get_transact_count() - 1; | |
| // update the dst_addr when using an existing session | |
| // for e.g using Host based session pools may ignore the DNS IP | |
| IpEndpoint addr; | |
| addr.assign(server_txn->get_remote_addr()); | |
| if (!ats_ip_addr_eq(&t_state.current.server->dst_addr, &addr)) { | |
| ip_port_text_buffer ipb1, ipb2; | |
| SMDbg(dbg_ctl_http_ss, "updating ip when attaching server session from %s to %s", | |
| ats_ip_ntop(&t_state.current.server->dst_addr.sa, ipb1, sizeof(ipb1)), | |
| ats_ip_ntop(server_txn->get_remote_addr(), ipb2, sizeof(ipb2))); | |
| ats_ip_copy(&t_state.current.server->dst_addr, server_txn->get_remote_addr()); | |
| } | |
| // Propagate the per client IP debugging | |
| if (_ua.get_txn()) { | |
| server_txn->get_netvc()->control_flags.set_flags(get_cont_flags().get_flags()); | |
| ATS_PROBE2(http_attach_server_session, this->sm_id, server_txn->get_netvc()->get_socket()); | |
| } else { // If there is no _ua.get_txn() no sense in continuing to attach the server session | |
| return; | |
| } | |
| // Set the mutex so that we have something to update | |
| // stats with | |
| server_txn->mutex = this->mutex; | |
| server_txn->increment_transactions_stat(); | |
| // Record the VC in our table | |
| server_entry = vc_table.new_entry(); | |
| server_entry->vc = server_txn; | |
| server_entry->vc_type = HttpVC_t::SERVER_VC; | |
| server_entry->vc_write_handler = &HttpSM::state_send_server_request_header; | |
| NetVConnection *server_vc = server_txn->get_netvc(); | |
| // set flag for server session is SSL | |
| if (server_vc->get_service<TLSBasicSupport>()) { | |
| server_connection_is_ssl = true; | |
| } | |
| if (auto tsrs = server_vc->get_service<TLSSessionResumptionSupport>(); tsrs) { | |
| server_ssl_reused = tsrs->getIsResumedOriginSSLSession(); | |
| } | |
| server_protocol = server_txn->get_protocol_string(); | |
| // Initiate a read on the session so that the SM and not | |
| // session manager will get called back if the timeout occurs | |
| // or the server closes on us. The IO Core now requires us to | |
| // do the read with a buffer and a size so preallocate the | |
| // buffer | |
| // ts-3189 We are only setting up an empty read at this point. This | |
| // is sufficient to have the timeout errors directed to the appropriate | |
| // SM handler, but we don't want to read any data until the tunnel has | |
| // been set up. This isn't such a big deal with GET results, since | |
| // if no tunnels are set up, there is no danger of data being delivered | |
| // to the wrong tunnel's consumer handler. But for post and other | |
| // methods that send data after the request, two tunnels are created in | |
| // series, and with a full read set up at this point, the EOS from the | |
| // first tunnel was sometimes behind handled by the consumer of the | |
| // first tunnel instead of the producer of the second tunnel. | |
| // The real read is setup in setup_server_read_response_header() | |
| server_entry->read_vio = server_txn->do_io_read(this, 0, server_txn->get_remote_reader()->mbuf); | |
| // Transfer control of the write side as well | |
| server_entry->write_vio = server_txn->do_io_write(this, 0, nullptr); | |
| // Setup the timeouts | |
| // Set the inactivity timeout to the connect timeout so that we | |
| // we fail this server if it doesn't start sending the response | |
| // header | |
| server_txn->set_inactivity_timeout(get_server_connect_timeout()); | |
| server_txn->set_active_timeout(get_server_active_timeout()); | |
| // Do we need Transfer_Encoding? | |
| if (_ua.get_txn()->has_request_body(t_state.hdr_info.request_content_length, | |
| t_state.client_info.transfer_encoding == HttpTransact::TransferEncoding_t::CHUNKED)) { | |
| if (server_txn->is_chunked_encoding_supported()) { | |
| // See if we need to insert a chunked header | |
| if (!t_state.hdr_info.server_request.presence(MIME_PRESENCE_CONTENT_LENGTH) && | |
| !t_state.hdr_info.server_request.presence(MIME_PRESENCE_TRANSFER_ENCODING)) { | |
| // Stuff in a TE setting so we treat this as chunked, sort of. | |
| t_state.server_info.transfer_encoding = HttpTransact::TransferEncoding_t::CHUNKED; | |
| t_state.hdr_info.server_request.value_append(static_cast<std::string_view>(MIME_FIELD_TRANSFER_ENCODING), | |
| static_cast<std::string_view>(HTTP_VALUE_CHUNKED), true); | |
| } | |
| } | |
| } | |
| if (plugin_tunnel_type != HttpPluginTunnel_t::NONE || is_private()) { | |
| this->set_server_session_private(true); | |
| } | |
| } | |
| void | |
| HttpSM::setup_server_send_request_api() | |
| { | |
| // Make sure the VC is on the correct timeout | |
| server_txn->set_inactivity_timeout(get_server_inactivity_timeout()); | |
| t_state.api_next_action = HttpTransact::StateMachineAction_t::API_SEND_REQUEST_HDR; | |
| do_api_callout(); | |
| } | |
| void | |
| HttpSM::setup_server_send_request() | |
| { | |
| int hdr_length; | |
| int64_t msg_len = 0; /* lv: just make gcc happy */ | |
| hsm_release_assert(server_entry != nullptr); | |
| hsm_release_assert(server_txn != nullptr); | |
| hsm_release_assert(server_entry->vc == server_txn); | |
| // Send the request header | |
| server_entry->vc_write_handler = &HttpSM::state_send_server_request_header; | |
| server_entry->write_buffer = new_MIOBuffer(HTTP_HEADER_BUFFER_SIZE_INDEX); | |
| if (t_state.api_server_request_body_set) { | |
| msg_len = t_state.internal_msg_buffer_size; | |
| t_state.hdr_info.server_request.value_set_int64(static_cast<std::string_view>(MIME_FIELD_CONTENT_LENGTH), msg_len); | |
| } | |
| dump_header(dbg_ctl_http_hdrs, &(t_state.hdr_info.server_request), sm_id, "Proxy's Request after hooks"); | |
| // We need a reader so bytes don't fall off the end of | |
| // the buffer | |
| IOBufferReader *buf_start = server_entry->write_buffer->alloc_reader(); | |
| server_request_hdr_bytes = hdr_length = write_header_into_buffer(&t_state.hdr_info.server_request, server_entry->write_buffer); | |
| // the plugin decided to append a message to the request | |
| if (t_state.api_server_request_body_set) { | |
| SMDbg(dbg_ctl_http, "appending msg of %" PRId64 " bytes to request %s", msg_len, t_state.internal_msg_buffer); | |
| hdr_length += server_entry->write_buffer->write(t_state.internal_msg_buffer, msg_len); | |
| server_request_body_bytes = msg_len; | |
| } | |
| ATS_PROBE1(milestone_server_begin_write, sm_id); | |
| milestones[TS_MILESTONE_SERVER_BEGIN_WRITE] = ink_get_hrtime(); | |
| server_entry->write_vio = server_entry->vc->do_io_write(this, hdr_length, buf_start); | |
| // Make sure the VC is using correct timeouts. We may be reusing a previously used server session | |
| server_txn->set_inactivity_timeout(get_server_inactivity_timeout()); | |
| // Go on and set up the read response header too | |
| setup_server_read_response_header(); | |
| } | |
| void | |
| HttpSM::setup_server_read_response_header() | |
| { | |
| ink_assert(server_txn != nullptr); | |
| ink_assert(server_entry != nullptr); | |
| // HttpRequestFlavor_t::SCHEDULED_UPDATE can be transformed in HttpRequestFlavor_t::REVPROXY | |
| ink_assert(_ua.get_txn() != nullptr || t_state.req_flavor == HttpTransact::HttpRequestFlavor_t::SCHEDULED_UPDATE || | |
| t_state.req_flavor == HttpTransact::HttpRequestFlavor_t::REVPROXY); | |
| ink_assert(server_txn != nullptr && server_txn->get_remote_reader() != nullptr); | |
| SMDbg(dbg_ctl_http, "Setting up the header read"); | |
| // Now that we've got the ability to read from the | |
| // server, setup to read the response header | |
| server_entry->vc_read_handler = &HttpSM::state_read_server_response_header; | |
| server_entry->vc = server_txn; | |
| t_state.current.state = HttpTransact::STATE_UNDEFINED; | |
| t_state.current.server->state = HttpTransact::STATE_UNDEFINED; | |
| // Note: we must use destroy() here since clear() | |
| // does not free the memory from the header | |
| t_state.hdr_info.server_response.destroy(); | |
| t_state.hdr_info.server_response.create(HTTPType::RESPONSE); | |
| http_parser_clear(&http_parser); | |
| server_response_hdr_bytes = 0; | |
| milestones[TS_MILESTONE_SERVER_READ_HEADER_DONE] = 0; | |
| // The tunnel from OS to UA is now setup. Ready to read the response | |
| server_entry->read_vio = server_txn->do_io_read(this, INT64_MAX, server_txn->get_remote_reader()->mbuf); | |
| // If there is anything in the buffer call the parsing routines | |
| // since if the response is finished, we won't get any | |
| // additional callbacks | |
| if (server_txn->get_remote_reader()->read_avail() > 0) { | |
| state_read_server_response_header((server_entry->eos) ? VC_EVENT_EOS : VC_EVENT_READ_READY, server_entry->read_vio); | |
| } | |
| } | |
| HttpTunnelProducer * | |
| HttpSM::setup_cache_read_transfer() | |
| { | |
| int64_t alloc_index, hdr_size; | |
| int64_t doc_size; | |
| ink_assert(cache_sm.cache_read_vc != nullptr); | |
| doc_size = t_state.cache_info.object_read->object_size_get(); | |
| alloc_index = buffer_size_to_index(doc_size + index_to_buffer_size(HTTP_HEADER_BUFFER_SIZE_INDEX), | |
| t_state.http_config_param->max_payload_iobuf_index); | |
| #ifndef USE_NEW_EMPTY_MIOBUFFER | |
| MIOBuffer *buf = new_MIOBuffer(alloc_index); | |
| #else | |
| MIOBuffer *buf = new_empty_MIOBuffer(alloc_index); | |
| buf->append_block(HTTP_HEADER_BUFFER_SIZE_INDEX); | |
| #endif | |
| buf->water_mark = static_cast<int>(t_state.txn_conf->default_buffer_water_mark); | |
| IOBufferReader *buf_start = buf->alloc_reader(); | |
| // Now dump the header into the buffer | |
| ink_assert(t_state.hdr_info.client_response.status_get() != HTTPStatus::NOT_MODIFIED); | |
| client_response_hdr_bytes = hdr_size = write_response_header_into_buffer(&t_state.hdr_info.client_response, buf); | |
| cache_response_hdr_bytes = client_response_hdr_bytes; | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::tunnel_handler); | |
| if (doc_size != INT64_MAX) { | |
| doc_size += hdr_size; | |
| } | |
| HttpTunnelProducer *p = tunnel.add_producer(cache_sm.cache_read_vc, doc_size, buf_start, &HttpSM::tunnel_handler_cache_read, | |
| HttpTunnelType_t::CACHE_READ, "cache read"); | |
| tunnel.add_consumer(_ua.get_entry()->vc, cache_sm.cache_read_vc, &HttpSM::tunnel_handler_ua, HttpTunnelType_t::HTTP_CLIENT, | |
| "user agent"); | |
| // if size of a cached item is not known, we'll do chunking for keep-alive HTTP/1.1 clients | |
| // this only applies to read-while-write cases where origin server sends a dynamically generated chunked content | |
| // w/o providing a Content-Length header | |
| if (t_state.client_info.receive_chunked_response) { | |
| bool const drop_chunked_trailers = t_state.http_config_param->oride.http_drop_chunked_trailers == 1; | |
| bool const parse_chunk_strictly = t_state.http_config_param->oride.http_strict_chunk_parsing == 1; | |
| tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, TunnelChunkingAction_t::CHUNK_CONTENT, drop_chunked_trailers, | |
| parse_chunk_strictly); | |
| tunnel.set_producer_chunking_size(p, t_state.txn_conf->http_chunking_size); | |
| } | |
| _ua.get_entry()->in_tunnel = true; | |
| cache_sm.cache_read_vc = nullptr; | |
| return p; | |
| } | |
| HttpTunnelProducer * | |
| HttpSM::setup_cache_transfer_to_transform() | |
| { | |
| int64_t alloc_index; | |
| int64_t doc_size; | |
| ink_assert(cache_sm.cache_read_vc != nullptr); | |
| ink_assert(transform_info.vc != nullptr); | |
| ink_assert(transform_info.entry->vc == transform_info.vc); | |
| // grab this here | |
| cache_response_hdr_bytes = t_state.hdr_info.cache_response.length_get(); | |
| doc_size = t_state.cache_info.object_read->object_size_get(); | |
| alloc_index = buffer_size_to_index(doc_size, t_state.http_config_param->max_payload_iobuf_index); | |
| MIOBuffer *buf = new_MIOBuffer(alloc_index); | |
| IOBufferReader *buf_start = buf->alloc_reader(); | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::state_response_wait_for_transform_read); | |
| HttpTunnelProducer *p = tunnel.add_producer(cache_sm.cache_read_vc, doc_size, buf_start, &HttpSM::tunnel_handler_cache_read, | |
| HttpTunnelType_t::CACHE_READ, "cache read"); | |
| tunnel.add_consumer(transform_info.vc, cache_sm.cache_read_vc, &HttpSM::tunnel_handler_transform_write, | |
| HttpTunnelType_t::TRANSFORM, "transform write"); | |
| transform_info.entry->in_tunnel = true; | |
| cache_sm.cache_read_vc = nullptr; | |
| return p; | |
| } | |
| void | |
| HttpSM::setup_cache_write_transfer(HttpCacheSM *c_sm, VConnection *source_vc, HTTPInfo *store_info, int64_t skip_bytes, | |
| const char *name) | |
| { | |
| ink_assert(c_sm->cache_write_vc != nullptr); | |
| ink_assert(t_state.request_sent_time > 0); | |
| ink_assert(t_state.response_received_time > 0); | |
| store_info->request_sent_time_set(t_state.request_sent_time); | |
| store_info->response_received_time_set(t_state.response_received_time); | |
| c_sm->cache_write_vc->set_http_info(store_info); | |
| store_info->clear(); | |
| tunnel.add_consumer(c_sm->cache_write_vc, source_vc, &HttpSM::tunnel_handler_cache_write, HttpTunnelType_t::CACHE_WRITE, name, | |
| skip_bytes); | |
| c_sm->cache_write_vc = nullptr; | |
| } | |
| void | |
| HttpSM::setup_100_continue_transfer() | |
| { | |
| MIOBuffer *buf = new_MIOBuffer(HTTP_HEADER_BUFFER_SIZE_INDEX); | |
| IOBufferReader *buf_start = buf->alloc_reader(); | |
| // First write the client response header into the buffer | |
| ink_assert(t_state.client_info.http_version != HTTP_0_9); | |
| client_response_hdr_bytes = write_header_into_buffer(&t_state.hdr_info.client_response, buf); | |
| ink_assert(client_response_hdr_bytes > 0); | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::tunnel_handler_100_continue); | |
| // Clear the decks before we set up new producers. As things stand, we cannot have two static operators | |
| // at once | |
| tunnel.reset(); | |
| // Setup the tunnel to the client | |
| HttpTunnelProducer *p = | |
| tunnel.add_producer(HTTP_TUNNEL_STATIC_PRODUCER, client_response_hdr_bytes, buf_start, (HttpProducerHandler) nullptr, | |
| HttpTunnelType_t::STATIC, "internal msg - 100 continue"); | |
| tunnel.add_consumer(_ua.get_entry()->vc, HTTP_TUNNEL_STATIC_PRODUCER, &HttpSM::tunnel_handler_100_continue_ua, | |
| HttpTunnelType_t::HTTP_CLIENT, "user agent"); | |
| // Make sure the half_close is not set. | |
| _ua.get_txn()->set_half_close_flag(false); | |
| _ua.get_entry()->in_tunnel = true; | |
| tunnel.tunnel_run(p); | |
| // Set up the header response read again. Already processed the 100 response | |
| setup_server_read_response_header(); | |
| } | |
| ////////////////////////////////////////////////////////////////////////// | |
| // | |
| // HttpSM::setup_error_transfer() | |
| // | |
| // The proxy has generated an error message which it | |
| // is sending to the client. For some cases, however, | |
| // such as when the proxy is transparent, returning | |
| // a proxy-generated error message exposes the proxy, | |
| // destroying transparency. The HttpBodyFactory code, | |
| // therefore, does not generate an error message body | |
| // in such cases. This function checks for the presence | |
| // of an error body. If its not present, it closes the | |
| // connection to the user, else it simply calls | |
| // setup_write_proxy_internal, which is the standard | |
| // routine for setting up proxy-generated responses. | |
| // | |
| ////////////////////////////////////////////////////////////////////////// | |
| void | |
| HttpSM::setup_error_transfer() | |
| { | |
| if (body_factory->is_response_suppressed(&t_state) || t_state.internal_msg_buffer || | |
| is_response_body_precluded(t_state.http_return_code)) { | |
| // Since we need to send the error message, call the API | |
| // function | |
| ink_assert(t_state.internal_msg_buffer_size > 0 || is_response_body_precluded(t_state.http_return_code)); | |
| t_state.api_next_action = HttpTransact::StateMachineAction_t::API_SEND_RESPONSE_HDR; | |
| do_api_callout(); | |
| } else { | |
| SMDbg(dbg_ctl_http, "Now closing connection ..."); | |
| vc_table.cleanup_entry(_ua.get_entry()); | |
| _ua.set_entry(nullptr); | |
| // _ua.get_txn() = NULL; | |
| terminate_sm = true; | |
| t_state.source = HttpTransact::Source_t::INTERNAL; | |
| } | |
| } | |
| void | |
| HttpSM::setup_internal_transfer(HttpSMHandler handler_arg) | |
| { | |
| bool is_msg_buf_present; | |
| if (t_state.internal_msg_buffer) { | |
| is_msg_buf_present = true; | |
| ink_assert(t_state.internal_msg_buffer_size > 0); | |
| // Set the content length here since a plugin | |
| // may have changed the error body | |
| t_state.hdr_info.client_response.set_content_length(t_state.internal_msg_buffer_size); | |
| t_state.hdr_info.client_response.field_delete(static_cast<std::string_view>(MIME_FIELD_TRANSFER_ENCODING)); | |
| // set internal_msg_buffer_type if available | |
| if (t_state.internal_msg_buffer_type) { | |
| int len = strlen(t_state.internal_msg_buffer_type); | |
| if (len > 0) { | |
| t_state.hdr_info.client_response.value_set( | |
| static_cast<std::string_view>(MIME_FIELD_CONTENT_TYPE), | |
| std::string_view{t_state.internal_msg_buffer_type, static_cast<std::string_view::size_type>(len)}); | |
| } | |
| ats_free(t_state.internal_msg_buffer_type); | |
| t_state.internal_msg_buffer_type = nullptr; | |
| } else { | |
| t_state.hdr_info.client_response.value_set(static_cast<std::string_view>(MIME_FIELD_CONTENT_TYPE), "text/html"sv); | |
| } | |
| } else { | |
| is_msg_buf_present = false; | |
| // If we are sending a response that can have a body | |
| // but doesn't have a body add a content-length of zero. | |
| // Needed for keep-alive on PURGE requests | |
| if (!is_response_body_precluded(t_state.hdr_info.client_response.status_get(), t_state.method)) { | |
| t_state.hdr_info.client_response.set_content_length(0); | |
| t_state.hdr_info.client_response.field_delete(static_cast<std::string_view>(MIME_FIELD_TRANSFER_ENCODING)); | |
| } | |
| } | |
| t_state.source = HttpTransact::Source_t::INTERNAL; | |
| int64_t buf_size = | |
| index_to_buffer_size(HTTP_HEADER_BUFFER_SIZE_INDEX) + (is_msg_buf_present ? t_state.internal_msg_buffer_size : 0); | |
| MIOBuffer *buf = new_MIOBuffer(buffer_size_to_index(buf_size, t_state.http_config_param->max_payload_iobuf_index)); | |
| IOBufferReader *buf_start = buf->alloc_reader(); | |
| // First write the client response header into the buffer | |
| client_response_hdr_bytes = write_response_header_into_buffer(&t_state.hdr_info.client_response, buf); | |
| int64_t nbytes = client_response_hdr_bytes; | |
| // Next append the message onto the MIOBuffer | |
| // From HTTP/1.1 RFC: | |
| // "The HEAD method is identical to GET except that the server | |
| // MUST NOT return a message-body in the response. The metainformation | |
| // in the HTTP headers in response to a HEAD request SHOULD be | |
| // identical to the information sent in response to a GET request." | |
| // --> do not append the message onto the MIOBuffer and keep our pointer | |
| // to it so that it can be freed. | |
| if (is_msg_buf_present && t_state.method != HTTP_WKSIDX_HEAD) { | |
| nbytes += t_state.internal_msg_buffer_size; | |
| if (t_state.internal_msg_buffer_fast_allocator_size < 0) { | |
| buf->append_xmalloced(t_state.internal_msg_buffer, t_state.internal_msg_buffer_size); | |
| } else { | |
| buf->append_fast_allocated(t_state.internal_msg_buffer, t_state.internal_msg_buffer_size, | |
| t_state.internal_msg_buffer_fast_allocator_size); | |
| } | |
| // The IOBufferBlock will free the msg buffer when necessary so | |
| // eliminate our pointer to it | |
| t_state.internal_msg_buffer = nullptr; | |
| t_state.internal_msg_buffer_size = 0; | |
| } | |
| HTTP_SM_SET_DEFAULT_HANDLER(handler_arg); | |
| if (_ua.get_entry() && _ua.get_entry()->vc) { | |
| // Clear the decks before we setup the new producers | |
| // As things stand, we cannot have two static producers operating at | |
| // once | |
| tunnel.reset(); | |
| // Setup the tunnel to the client | |
| HttpTunnelProducer *p = tunnel.add_producer(HTTP_TUNNEL_STATIC_PRODUCER, nbytes, buf_start, (HttpProducerHandler) nullptr, | |
| HttpTunnelType_t::STATIC, "internal msg"); | |
| tunnel.add_consumer(_ua.get_entry()->vc, HTTP_TUNNEL_STATIC_PRODUCER, &HttpSM::tunnel_handler_ua, HttpTunnelType_t::HTTP_CLIENT, | |
| "user agent"); | |
| _ua.get_entry()->in_tunnel = true; | |
| tunnel.tunnel_run(p); | |
| } else { | |
| (this->*default_handler)(HTTP_TUNNEL_EVENT_DONE, &tunnel); | |
| } | |
| } | |
| // int HttpSM::find_http_resp_buffer_size(int cl) | |
| // | |
| // Returns the allocation index for the buffer for | |
| // a response based on the content length | |
| // | |
| int | |
| HttpSM::find_http_resp_buffer_size(int64_t content_length) | |
| { | |
| int64_t alloc_index; | |
| if (content_length == HTTP_UNDEFINED_CL) { | |
| // Try use our configured default size. Otherwise pick | |
| // the default size | |
| alloc_index = static_cast<int>(t_state.txn_conf->default_buffer_size_index); | |
| if (alloc_index < MIN_CONFIG_BUFFER_SIZE_INDEX || alloc_index > DEFAULT_MAX_BUFFER_SIZE) { | |
| alloc_index = DEFAULT_RESPONSE_BUFFER_SIZE_INDEX; | |
| } | |
| } else { | |
| int64_t buf_size = index_to_buffer_size(HTTP_HEADER_BUFFER_SIZE_INDEX) + content_length; | |
| alloc_index = buffer_size_to_index(buf_size, t_state.http_config_param->max_payload_iobuf_index); | |
| } | |
| return alloc_index; | |
| } | |
| // int HttpSM::server_transfer_init() | |
| // | |
| // Moves data from the header buffer into the reply buffer | |
| // and return the number of bytes we should use for initiating the | |
| // tunnel | |
| // | |
| int64_t | |
| HttpSM::server_transfer_init(MIOBuffer *buf, int hdr_size) | |
| { | |
| int64_t nbytes; | |
| int64_t to_copy = INT64_MAX; | |
| ink_assert(t_state.current.server != nullptr); // should have been set up if we're doing a transfer. | |
| if (server_entry->eos == true) { | |
| // The server has shutdown on us already so the only data | |
| // we'll get is already in the buffer | |
| nbytes = server_txn->get_remote_reader()->read_avail() + hdr_size; | |
| } else if (t_state.hdr_info.response_content_length == HTTP_UNDEFINED_CL) { | |
| // Chunked or otherwise, no length is defined. Pass -1 to tell the | |
| // tunnel that the size is unknown. | |
| nbytes = -1; | |
| } else { | |
| // Set to copy to the number of bytes we want to write as | |
| // if the server is sending us a bogus response we have to | |
| // truncate it as we've already decided to trust the content | |
| // length | |
| to_copy = t_state.hdr_info.response_content_length; | |
| nbytes = t_state.hdr_info.response_content_length + hdr_size; | |
| } | |
| // Next order of business if copy the remaining data from the header buffer into new buffer. | |
| int64_t server_response_pre_read_bytes = buf->write(server_txn->get_remote_reader(), to_copy); | |
| server_txn->get_remote_reader()->consume(server_response_pre_read_bytes); | |
| // If we know the length & copied the entire body | |
| // of the document out of the header buffer make | |
| // sure the server isn't screwing us by having sent too | |
| // much. If it did, we want to close the server connection | |
| if (server_response_pre_read_bytes == to_copy && server_txn->get_remote_reader()->read_avail() > 0) { | |
| t_state.current.server->keep_alive = HTTPKeepAlive::NO_KEEPALIVE; | |
| } | |
| return nbytes; | |
| } | |
| HttpTunnelProducer * | |
| HttpSM::setup_server_transfer_to_transform() | |
| { | |
| int64_t alloc_index; | |
| int64_t nbytes; | |
| alloc_index = find_server_buffer_size(); | |
| MIOBuffer *buf = new_MIOBuffer(alloc_index); | |
| IOBufferReader *buf_start = buf->alloc_reader(); | |
| nbytes = server_transfer_init(buf, 0); | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::state_response_wait_for_transform_read); | |
| HttpTunnelProducer *p = tunnel.add_producer(server_entry->vc, nbytes, buf_start, &HttpSM::tunnel_handler_server, | |
| HttpTunnelType_t::HTTP_SERVER, "http server"); | |
| tunnel.add_consumer(transform_info.vc, server_entry->vc, &HttpSM::tunnel_handler_transform_write, HttpTunnelType_t::TRANSFORM, | |
| "transform write"); | |
| server_entry->in_tunnel = true; | |
| transform_info.entry->in_tunnel = true; | |
| if (t_state.current.server->transfer_encoding == HttpTransact::TransferEncoding_t::CHUNKED) { | |
| client_response_hdr_bytes = 0; // fixed by YTS Team, yamsat | |
| bool const parse_chunk_strictly = t_state.http_config_param->oride.http_strict_chunk_parsing == 1; | |
| tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, TunnelChunkingAction_t::DECHUNK_CONTENT, | |
| HttpTunnel::DROP_CHUNKED_TRAILERS, parse_chunk_strictly); | |
| } | |
| return p; | |
| } | |
| HttpTunnelProducer * | |
| HttpSM::setup_transfer_from_transform() | |
| { | |
| int64_t alloc_index = find_server_buffer_size(); | |
| // TODO change this call to new_empty_MIOBuffer() | |
| MIOBuffer *buf = new_MIOBuffer(alloc_index); | |
| buf->water_mark = static_cast<int>(t_state.txn_conf->default_buffer_water_mark); | |
| IOBufferReader *buf_start = buf->alloc_reader(); | |
| HttpTunnelConsumer *c = tunnel.get_consumer(transform_info.vc); | |
| ink_assert(c != nullptr); | |
| ink_assert(c->vc == transform_info.vc); | |
| ink_assert(c->vc_type == HttpTunnelType_t::TRANSFORM); | |
| // Now dump the header into the buffer | |
| ink_assert(t_state.hdr_info.client_response.status_get() != HTTPStatus::NOT_MODIFIED); | |
| client_response_hdr_bytes = write_response_header_into_buffer(&t_state.hdr_info.client_response, buf); | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::tunnel_handler); | |
| HttpTunnelProducer *p = tunnel.add_producer(transform_info.vc, INT64_MAX, buf_start, &HttpSM::tunnel_handler_transform_read, | |
| HttpTunnelType_t::TRANSFORM, "transform read"); | |
| tunnel.chain(c, p); | |
| tunnel.add_consumer(_ua.get_entry()->vc, transform_info.vc, &HttpSM::tunnel_handler_ua, HttpTunnelType_t::HTTP_CLIENT, | |
| "user agent"); | |
| transform_info.entry->in_tunnel = true; | |
| _ua.get_entry()->in_tunnel = true; | |
| this->setup_client_response_plugin_agents(p, client_response_hdr_bytes); | |
| if (t_state.client_info.receive_chunked_response) { | |
| bool const drop_chunked_trailers = t_state.http_config_param->oride.http_drop_chunked_trailers == 1; | |
| bool const parse_chunk_strictly = t_state.http_config_param->oride.http_strict_chunk_parsing == 1; | |
| tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, TunnelChunkingAction_t::CHUNK_CONTENT, drop_chunked_trailers, | |
| parse_chunk_strictly); | |
| tunnel.set_producer_chunking_size(p, t_state.txn_conf->http_chunking_size); | |
| } | |
| return p; | |
| } | |
| HttpTunnelProducer * | |
| HttpSM::setup_server_transfer() | |
| { | |
| SMDbg(dbg_ctl_http, "Setup Server Transfer"); | |
| int64_t alloc_index, hdr_size; | |
| int64_t nbytes; | |
| alloc_index = find_server_buffer_size(); | |
| #ifndef USE_NEW_EMPTY_MIOBUFFER | |
| MIOBuffer *buf = new_MIOBuffer(alloc_index); | |
| #else | |
| MIOBuffer *buf = new_empty_MIOBuffer(alloc_index); | |
| buf->append_block(HTTP_HEADER_BUFFER_SIZE_INDEX); | |
| #endif | |
| buf->water_mark = static_cast<int>(t_state.txn_conf->default_buffer_water_mark); | |
| IOBufferReader *buf_start = buf->alloc_reader(); | |
| // we need to know if we are going to chunk the response or not | |
| // before we write the response header into buffer | |
| TunnelChunkingAction_t action; | |
| if (t_state.client_info.receive_chunked_response == false) { | |
| if (t_state.current.server->transfer_encoding == HttpTransact::TransferEncoding_t::CHUNKED) { | |
| action = TunnelChunkingAction_t::DECHUNK_CONTENT; | |
| } else { | |
| action = TunnelChunkingAction_t::PASSTHRU_DECHUNKED_CONTENT; | |
| } | |
| } else { | |
| if (t_state.current.server->transfer_encoding != HttpTransact::TransferEncoding_t::CHUNKED) { | |
| if (t_state.client_info.http_version == HTTP_0_9) { | |
| action = TunnelChunkingAction_t::PASSTHRU_DECHUNKED_CONTENT; // send as-is | |
| } else { | |
| action = TunnelChunkingAction_t::CHUNK_CONTENT; | |
| } | |
| } else { | |
| action = TunnelChunkingAction_t::PASSTHRU_CHUNKED_CONTENT; | |
| } | |
| } | |
| if (action == TunnelChunkingAction_t::CHUNK_CONTENT || | |
| action == TunnelChunkingAction_t::PASSTHRU_CHUNKED_CONTENT) { // remove Content-Length | |
| t_state.hdr_info.client_response.field_delete(static_cast<std::string_view>(MIME_FIELD_CONTENT_LENGTH)); | |
| } | |
| // Now dump the header into the buffer | |
| ink_assert(t_state.hdr_info.client_response.status_get() != HTTPStatus::NOT_MODIFIED); | |
| client_response_hdr_bytes = hdr_size = write_response_header_into_buffer(&t_state.hdr_info.client_response, buf); | |
| nbytes = server_transfer_init(buf, hdr_size); | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::tunnel_handler); | |
| HttpTunnelProducer *p = tunnel.add_producer(server_entry->vc, nbytes, buf_start, &HttpSM::tunnel_handler_server, | |
| HttpTunnelType_t::HTTP_SERVER, "http server"); | |
| tunnel.add_consumer(_ua.get_entry()->vc, server_entry->vc, &HttpSM::tunnel_handler_ua, HttpTunnelType_t::HTTP_CLIENT, | |
| "user agent"); | |
| _ua.get_entry()->in_tunnel = true; | |
| server_entry->in_tunnel = true; | |
| this->setup_client_response_plugin_agents(p, client_response_hdr_bytes); | |
| bool const drop_chunked_trailers = t_state.http_config_param->oride.http_drop_chunked_trailers == 1; | |
| bool const parse_chunk_strictly = t_state.http_config_param->oride.http_strict_chunk_parsing == 1; | |
| tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, action, drop_chunked_trailers, parse_chunk_strictly); | |
| tunnel.set_producer_chunking_size(p, t_state.txn_conf->http_chunking_size); | |
| return p; | |
| } | |
| HttpTunnelProducer * | |
| HttpSM::setup_push_transfer_to_cache() | |
| { | |
| int64_t nbytes, alloc_index; | |
| alloc_index = find_http_resp_buffer_size(t_state.hdr_info.request_content_length); | |
| MIOBuffer *buf = new_MIOBuffer(alloc_index); | |
| IOBufferReader *buf_start = buf->alloc_reader(); | |
| ink_release_assert(t_state.hdr_info.request_content_length != HTTP_UNDEFINED_CL); | |
| nbytes = t_state.hdr_info.request_content_length - pushed_response_hdr_bytes; | |
| ink_release_assert(nbytes >= 0); | |
| if (_ua.get_entry()->eos == true) { | |
| // The ua has shutdown on us already so the only data | |
| // we'll get is already in the buffer. Make sure it | |
| // fulfills the stated length | |
| int64_t avail = _ua.get_txn()->get_remote_reader()->read_avail(); | |
| if (avail < nbytes) { | |
| // Client failed to send the body, it's gone. Kill the | |
| // state machine | |
| terminate_sm = true; | |
| return nullptr; | |
| } | |
| } | |
| // Next order of business is copy the remaining data from the | |
| // header buffer into new buffer. | |
| pushed_response_body_bytes = buf->write(_ua.get_txn()->get_remote_reader(), nbytes); | |
| _ua.get_txn()->get_remote_reader()->consume(pushed_response_body_bytes); | |
| client_request_body_bytes += pushed_response_body_bytes; | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::tunnel_handler_push); | |
| HttpTunnelProducer *p = tunnel.add_producer(_ua.get_entry()->vc, nbytes, buf_start, &HttpSM::tunnel_handler_ua_push, | |
| HttpTunnelType_t::HTTP_CLIENT, "user_agent"); | |
| setup_cache_write_transfer(&cache_sm, _ua.get_entry()->vc, &t_state.cache_info.object_store, 0, "cache write"); | |
| _ua.get_entry()->in_tunnel = true; | |
| return p; | |
| } | |
| void | |
| HttpSM::setup_blind_tunnel(bool send_response_hdr, IOBufferReader *initial) | |
| { | |
| ink_assert(server_entry->vc != nullptr); | |
| HttpTunnelConsumer *c_ua; | |
| HttpTunnelConsumer *c_os; | |
| HttpTunnelProducer *p_ua; | |
| HttpTunnelProducer *p_os; | |
| MIOBuffer *from_ua_buf = new_MIOBuffer(BUFFER_SIZE_INDEX_32K); | |
| MIOBuffer *to_ua_buf = new_MIOBuffer(BUFFER_SIZE_INDEX_32K); | |
| IOBufferReader *r_from = from_ua_buf->alloc_reader(); | |
| IOBufferReader *r_to = to_ua_buf->alloc_reader(); | |
| ATS_PROBE1(milestone_server_begin_write, sm_id); | |
| milestones[TS_MILESTONE_SERVER_BEGIN_WRITE] = ink_get_hrtime(); | |
| if (send_response_hdr) { | |
| client_response_hdr_bytes = write_response_header_into_buffer(&t_state.hdr_info.client_response, to_ua_buf); | |
| if (initial && initial->read_avail()) { | |
| int64_t avail = initial->read_avail(); | |
| to_ua_buf->write(initial, avail); | |
| initial->consume(avail); | |
| } | |
| } else { | |
| client_response_hdr_bytes = 0; | |
| } | |
| int64_t nbytes = 0; | |
| if (t_state.txn_conf->proxy_protocol_out >= 0) { | |
| nbytes = do_outbound_proxy_protocol(from_ua_buf, static_cast<NetVConnection *>(server_entry->vc), _ua.get_txn()->get_netvc(), | |
| t_state.txn_conf->proxy_protocol_out); | |
| } | |
| client_request_body_bytes = nbytes; | |
| if (_ua.get_raw_buffer_reader() != nullptr) { | |
| client_request_body_bytes += from_ua_buf->write(_ua.get_raw_buffer_reader(), client_request_hdr_bytes); | |
| _ua.get_raw_buffer_reader()->dealloc(); | |
| _ua.set_raw_buffer_reader(nullptr); | |
| } | |
| // if pre-warmed connection is used and it has data from origin server, forward it to ua | |
| if (_prewarm_sm && _prewarm_sm->has_data_from_origin_server()) { | |
| ink_release_assert(_prewarm_sm->handler.is<&PreWarmSM::state_closed>()); | |
| client_response_hdr_bytes += to_ua_buf->write(_prewarm_sm->server_buf_reader()); | |
| } | |
| // Next order of business if copy the remaining data from the | |
| // header buffer into new buffer | |
| client_request_body_bytes += from_ua_buf->write(_ua.get_txn()->get_remote_reader()); | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::tunnel_handler); | |
| this->do_transform_open(); | |
| this->do_post_transform_open(); | |
| p_os = tunnel.add_producer(server_entry->vc, -1, r_to, &HttpSM::tunnel_handler_ssl_producer, HttpTunnelType_t::HTTP_SERVER, | |
| "http server - tunnel"); | |
| if (this->transform_info.vc != nullptr) { | |
| HttpTunnelConsumer *c_trans = tunnel.add_consumer(transform_info.vc, server_entry->vc, &HttpSM::tunnel_handler_transform_write, | |
| HttpTunnelType_t::TRANSFORM, "server tunnel - transform"); | |
| MIOBuffer *trans_buf = new_MIOBuffer(BUFFER_SIZE_INDEX_32K); | |
| IOBufferReader *trans_to = trans_buf->alloc_reader(); | |
| HttpTunnelProducer *p_trans = tunnel.add_producer(transform_info.vc, -1, trans_to, &HttpSM::tunnel_handler_transform_read, | |
| HttpTunnelType_t::TRANSFORM, "server tunnel - transform"); | |
| c_ua = tunnel.add_consumer(_ua.get_entry()->vc, transform_info.vc, &HttpSM::tunnel_handler_ssl_consumer, | |
| HttpTunnelType_t::HTTP_CLIENT, "user agent - tunnel"); | |
| tunnel.chain(c_trans, p_trans); | |
| transform_info.entry->in_tunnel = true; | |
| } else { | |
| c_ua = tunnel.add_consumer(_ua.get_entry()->vc, server_entry->vc, &HttpSM::tunnel_handler_ssl_consumer, | |
| HttpTunnelType_t::HTTP_CLIENT, "user agent - tunnel"); | |
| } | |
| p_ua = tunnel.add_producer(_ua.get_entry()->vc, -1, r_from, &HttpSM::tunnel_handler_ssl_producer, HttpTunnelType_t::HTTP_CLIENT, | |
| "user agent - tunnel"); | |
| if (this->post_transform_info.vc != nullptr) { | |
| HttpTunnelConsumer *c_trans = | |
| tunnel.add_consumer(post_transform_info.vc, _ua.get_entry()->vc, &HttpSM::tunnel_handler_transform_write, | |
| HttpTunnelType_t::TRANSFORM, "ua tunnel - transform"); | |
| MIOBuffer *trans_buf = new_MIOBuffer(BUFFER_SIZE_INDEX_32K); | |
| IOBufferReader *trans_to = trans_buf->alloc_reader(); | |
| HttpTunnelProducer *p_trans = tunnel.add_producer(post_transform_info.vc, -1, trans_to, &HttpSM::tunnel_handler_transform_read, | |
| HttpTunnelType_t::TRANSFORM, "ua tunnel - transform"); | |
| c_os = tunnel.add_consumer(server_entry->vc, post_transform_info.vc, &HttpSM::tunnel_handler_ssl_consumer, | |
| HttpTunnelType_t::HTTP_SERVER, "http server - tunnel"); | |
| tunnel.chain(c_trans, p_trans); | |
| post_transform_info.entry->in_tunnel = true; | |
| } else { | |
| c_os = tunnel.add_consumer(server_entry->vc, _ua.get_entry()->vc, &HttpSM::tunnel_handler_ssl_consumer, | |
| HttpTunnelType_t::HTTP_SERVER, "http server - tunnel"); | |
| } | |
| _ua.get_entry()->vc->mark_as_tunnel_endpoint(); | |
| server_entry->vc->mark_as_tunnel_endpoint(); | |
| // Make the tunnel aware that the entries are bi-directional | |
| tunnel.chain(c_os, p_os); | |
| tunnel.chain(c_ua, p_ua); | |
| _ua.get_entry()->in_tunnel = true; | |
| server_entry->in_tunnel = true; | |
| tunnel.tunnel_run(); | |
| // If we're half closed, we got a FIN from the client. Forward it on to the origin server | |
| // now that we have the tunnel operational. | |
| if (_ua.get_txn() && _ua.get_txn()->get_half_close_flag()) { | |
| p_ua->vc->do_io_shutdown(IO_SHUTDOWN_READ); | |
| } | |
| } | |
| void | |
| HttpSM::setup_client_response_plugin_agents(HttpTunnelProducer *p, int num_header_bytes) | |
| { | |
| APIHook *agent = txn_hook_get(TS_HTTP_RESPONSE_CLIENT_HOOK); | |
| has_active_response_plugin_agents = agent != nullptr; | |
| while (agent) { | |
| INKVConnInternal *contp = static_cast<INKVConnInternal *>(agent->m_cont); | |
| tunnel.add_consumer(contp, p->vc, &HttpSM::tunnel_handler_plugin_agent, HttpTunnelType_t::HTTP_CLIENT, "response plugin agent", | |
| num_header_bytes); | |
| // We don't put these in the SM VC table because the tunnel | |
| // will clean them up in do_io_close(). | |
| agent = agent->next(); | |
| } | |
| } | |
| void | |
| HttpSM::setup_client_request_plugin_agents(HttpTunnelProducer *p, int num_header_bytes) | |
| { | |
| APIHook *agent = txn_hook_get(TS_HTTP_REQUEST_CLIENT_HOOK); | |
| has_active_request_plugin_agents = agent != nullptr; | |
| while (agent) { | |
| INKVConnInternal *contp = static_cast<INKVConnInternal *>(agent->m_cont); | |
| tunnel.add_consumer(contp, p->vc, &HttpSM::tunnel_handler_plugin_agent, HttpTunnelType_t::HTTP_CLIENT, "request plugin agent", | |
| num_header_bytes); | |
| // We don't put these in the SM VC table because the tunnel | |
| // will clean them up in do_io_close(). | |
| agent = agent->next(); | |
| } | |
| } | |
| inline void | |
| HttpSM::transform_cleanup(TSHttpHookID hook, HttpTransformInfo *info) | |
| { | |
| APIHook *t_hook = api_hooks.get(hook); | |
| if (t_hook && info->vc == nullptr) { | |
| do { | |
| VConnection *t_vcon = t_hook->m_cont; | |
| t_vcon->do_io_close(); | |
| t_hook = t_hook->m_link.next; | |
| } while (t_hook != nullptr); | |
| } | |
| } | |
| void | |
| HttpSM::plugin_agents_cleanup() | |
| { | |
| // If this is set then all of the plugin agent VCs were put in | |
| // the VC table and cleaned up there. This handles the case where | |
| // something went wrong early. | |
| if (!has_active_response_plugin_agents) { | |
| APIHook *agent = txn_hook_get(TS_HTTP_RESPONSE_CLIENT_HOOK); | |
| while (agent) { | |
| INKVConnInternal *contp = static_cast<INKVConnInternal *>(agent->m_cont); | |
| contp->do_io_close(); | |
| agent = agent->next(); | |
| } | |
| } | |
| if (!has_active_request_plugin_agents) { | |
| APIHook *agent = txn_hook_get(TS_HTTP_REQUEST_CLIENT_HOOK); | |
| while (agent) { | |
| INKVConnInternal *contp = static_cast<INKVConnInternal *>(agent->m_cont); | |
| contp->do_io_close(); | |
| agent = agent->next(); | |
| } | |
| } | |
| } | |
| ////////////////////////////////////////////////////////////////////////// | |
| // | |
| // HttpSM::kill_this() | |
| // | |
| // This function has two phases. One before we call the asynchronous | |
| // clean up routines (api and list removal) and one after. | |
| // The state about which phase we are in is kept in | |
| // HttpSM::kill_this_async_done | |
| // | |
| ////////////////////////////////////////////////////////////////////////// | |
| void | |
| HttpSM::kill_this() | |
| { | |
| ink_release_assert(reentrancy_count == 1); | |
| this->postbuf_clear(); | |
| enable_redirection = false; | |
| if (kill_this_async_done == false) { | |
| //////////////////////////////// | |
| // cancel uncompleted actions // | |
| //////////////////////////////// | |
| // The action should be cancelled only if | |
| // the state machine is in HttpApiState_t::NO_CALLOUT | |
| // state. This is because we are depending on the | |
| // callout to complete for the state machine to | |
| // get killed. | |
| if (callout_state == HttpApiState_t::NO_CALLOUT && !pending_action.empty()) { | |
| pending_action = nullptr; | |
| } else if (!pending_action.empty()) { | |
| ink_assert(pending_action.empty()); | |
| } | |
| cache_sm.end_both(); | |
| transform_cache_sm.end_both(); | |
| vc_table.cleanup_all(); | |
| // Clean up the tunnel resources. Take | |
| // it down if it is still active | |
| tunnel.kill_tunnel(); | |
| if (_netvc) { | |
| _netvc->do_io_close(); | |
| free_MIOBuffer(_netvc_read_buffer); | |
| } else if (server_txn == nullptr) { | |
| this->cancel_pending_server_connection(); | |
| } | |
| // It possible that a plugin added transform hook | |
| // but the hook never executed due to a client abort | |
| // In that case, we need to manually close all the | |
| // transforms to prevent memory leaks (INKqa06147) | |
| if (hooks_set) { | |
| transform_cleanup(TS_HTTP_RESPONSE_TRANSFORM_HOOK, &transform_info); | |
| transform_cleanup(TS_HTTP_REQUEST_TRANSFORM_HOOK, &post_transform_info); | |
| plugin_agents_cleanup(); | |
| } | |
| // It's also possible that the plugin_tunnel vc was never | |
| // executed due to not contacting the server | |
| if (plugin_tunnel) { | |
| plugin_tunnel->kill_no_connect(); | |
| plugin_tunnel = nullptr; | |
| } | |
| // So we don't try to nuke the state machine | |
| // if the plugin receives event we must reset | |
| // the terminate_flag | |
| terminate_sm = false; | |
| t_state.api_next_action = HttpTransact::StateMachineAction_t::API_SM_SHUTDOWN; | |
| if (do_api_callout() < 0) { // Failed to get a continuation lock | |
| // Need to hang out until we can complete the TXN_CLOSE hook | |
| terminate_sm = false; | |
| reentrancy_count--; | |
| return; | |
| } | |
| } | |
| // The reentrancy_count is still valid up to this point since | |
| // the api shutdown hook is asynchronous and double frees can | |
| // happen if the reentrancy count is not still valid until | |
| // after all asynch callouts have completed | |
| // | |
| // Once we get to this point, we could be waiting for async | |
| // completion in which case we need to decrement the reentrancy | |
| // count since the entry points can't do it for us since they | |
| // don't know if the state machine has been destroyed. In the | |
| // case we really are done with asynch callouts, decrement the | |
| // reentrancy count since it seems tacky to destruct a state | |
| // machine with non-zero count | |
| reentrancy_count--; | |
| ink_release_assert(reentrancy_count == 0); | |
| // If the api shutdown & list removal was synchronous | |
| // then the value of kill_this_async_done has changed so | |
| // we must check it again | |
| if (kill_this_async_done == true) { | |
| pending_action = nullptr; | |
| // This should only be a last-resort cleanup path. A background fill is | |
| // normally driven to COMPLETED or ABORTED by tunnel_handler_server, but | |
| // any unexpected teardown must still balance the active fill gauge before | |
| // optional stats/logging run. | |
| if (background_fill == BackgroundFill_t::STARTED) { | |
| background_fill = BackgroundFill_t::ABORTED; | |
| Metrics::Gauge::decrement(http_rsb.background_fill_current_count); | |
| } | |
| if (t_state.http_config_param->enable_http_stats) { | |
| update_stats(); | |
| } | |
| ////////////// | |
| // Log Data // | |
| ////////////// | |
| SMDbg(dbg_ctl_http_seq, "Logging transaction"); | |
| if (Log::transaction_logging_enabled() && t_state.api_info.logging_enabled) { | |
| TransactionLogData log_data(this); | |
| LogAccess accessor(log_data); | |
| int ret = Log::access(&accessor); | |
| if (ret & Log::FULL) { | |
| SMDbg(dbg_ctl_http, "Logging system indicates FULL."); | |
| } | |
| if (ret & Log::FAIL) { | |
| Log::error("failed to log transaction for at least one log object"); | |
| } | |
| } | |
| if (server_txn) { | |
| server_txn->transaction_done(); | |
| server_txn = nullptr; | |
| } | |
| if (_ua.get_txn()) { | |
| _ua.get_txn()->transaction_done(); | |
| } | |
| // In the async state, the plugin could have been | |
| // called resulting in the creation of a plugin_tunnel. | |
| // So it needs to be deleted now. | |
| if (plugin_tunnel) { | |
| plugin_tunnel->kill_no_connect(); | |
| plugin_tunnel = nullptr; | |
| } | |
| ink_assert(pending_action.empty()); | |
| ink_release_assert(vc_table.is_table_clear() == true); | |
| ink_release_assert(tunnel.is_tunnel_active() == false); | |
| HTTP_SM_SET_DEFAULT_HANDLER(nullptr); | |
| ats_free(redirect_url); | |
| redirect_url = nullptr; | |
| redirect_url_len = 0; | |
| #ifdef USE_HTTP_DEBUG_LISTS | |
| ink_mutex_acquire(&debug_sm_list_mutex); | |
| debug_sm_list.remove(this); | |
| ink_mutex_release(&debug_sm_list_mutex); | |
| #endif | |
| SMDbg(dbg_ctl_http, "deallocating sm"); | |
| destroy(); | |
| } | |
| } | |
| void | |
| HttpSM::update_stats() | |
| { | |
| const HTTPStatus status_code = | |
| t_state.hdr_info.client_response.valid() ? t_state.hdr_info.client_response.status_get() : HTTPStatus::NONE; | |
| ATS_PROBE2(milestone_sm_finish, sm_id, static_cast<int>(status_code)); | |
| milestones[TS_MILESTONE_SM_FINISH] = ink_get_hrtime(); | |
| if (is_action_tag_set("bad_length_state_dump")) { | |
| if (status_code == HTTPStatus::OK) { | |
| int64_t p_resp_cl = t_state.hdr_info.client_response.get_content_length(); | |
| int64_t resp_size = client_response_body_bytes; | |
| if (!((p_resp_cl == -1 || p_resp_cl == resp_size || resp_size == 0))) { | |
| Error("[%" PRId64 "] Truncated content detected", sm_id); | |
| dump_state_on_assert(); | |
| } | |
| } else if (client_request_hdr_bytes == 0) { | |
| Error("[%" PRId64 "] Zero length request header received", sm_id); | |
| dump_state_on_assert(); | |
| } | |
| } | |
| ink_hrtime total_time = milestones.elapsed(TS_MILESTONE_SM_START, TS_MILESTONE_SM_FINISH); | |
| // ua_close will not be assigned properly in some exceptional situation. | |
| // TODO: Assign ua_close with suitable value when HttpTunnel terminates abnormally. | |
| if (milestones[TS_MILESTONE_UA_CLOSE] == 0 && milestones[TS_MILESTONE_UA_READ_HEADER_DONE] > 0) { | |
| ATS_PROBE1(milestone_ua_close, sm_id); | |
| milestones[TS_MILESTONE_UA_CLOSE] = ink_get_hrtime(); | |
| } | |
| // request_process_time = The time after the header is parsed to the completion of the transaction | |
| ink_hrtime request_process_time = milestones[TS_MILESTONE_UA_CLOSE] - milestones[TS_MILESTONE_UA_READ_HEADER_DONE]; | |
| HttpTransact::client_result_stat(&t_state, total_time, request_process_time); | |
| ink_hrtime ua_write_time; | |
| if (milestones[TS_MILESTONE_UA_BEGIN_WRITE] != 0 && milestones[TS_MILESTONE_UA_CLOSE] != 0) { | |
| ua_write_time = milestones.elapsed(TS_MILESTONE_UA_BEGIN_WRITE, TS_MILESTONE_UA_CLOSE); | |
| } else { | |
| ua_write_time = -1; | |
| } | |
| ink_hrtime os_read_time; | |
| if (milestones[TS_MILESTONE_SERVER_READ_HEADER_DONE] != 0 && milestones[TS_MILESTONE_SERVER_CLOSE] != 0) { | |
| os_read_time = milestones.elapsed(TS_MILESTONE_SERVER_READ_HEADER_DONE, TS_MILESTONE_SERVER_CLOSE); | |
| } else { | |
| os_read_time = -1; | |
| } | |
| HttpTransact::update_size_and_time_stats( | |
| &t_state, total_time, ua_write_time, os_read_time, client_request_hdr_bytes, client_request_body_bytes, | |
| client_response_hdr_bytes, client_response_body_bytes, server_request_hdr_bytes, server_request_body_bytes, | |
| server_response_hdr_bytes, server_response_body_bytes, pushed_response_hdr_bytes, pushed_response_body_bytes, milestones); | |
| /* | |
| if (is_action_tag_set("http_handler_times")) { | |
| print_all_http_handler_times(); | |
| } | |
| */ | |
| // print slow requests if the threshold is set (> 0) and if we are over the time threshold | |
| if (t_state.txn_conf->slow_log_threshold != 0 && ink_hrtime_from_msec(t_state.txn_conf->slow_log_threshold) < total_time) { | |
| // Use the unmapped (pre-remap) URL so the slow log shows the incoming client URL. | |
| // unmapped_url may not be valid if the transaction ended before reaching the remap | |
| // stage (e.g. client timeout during header read, malformed request). In that case | |
| // fall back to client_request URL which hasn't been remapped yet either. | |
| char url_string[256] = ""; | |
| int url_length = 0; | |
| if (t_state.unmapped_url.valid()) { | |
| t_state.unmapped_url.string_get_buf(url_string, sizeof(url_string) - 1, &url_length); | |
| } else { | |
| int offset = 0; | |
| int skip = 0; | |
| t_state.hdr_info.client_request.url_print(url_string, sizeof(url_string) - 1, &offset, &skip); | |
| url_length = offset; | |
| } | |
| url_string[url_length] = 0; // NULL terminate the string | |
| // unique id | |
| char unique_id_string[128] = ""; | |
| if (auto field{t_state.hdr_info.client_request.value_get(static_cast<std::string_view>(MIME_FIELD_X_ID))}; !field.empty()) { | |
| auto length{std::min(field.length(), sizeof(unique_id_string) - 1)}; | |
| memcpy(unique_id_string, field.data(), length); | |
| unique_id_string[length] = 0; // NULL terminate the string | |
| } | |
| // set the fd for the request | |
| int fd = 0; | |
| NetVConnection *vc = nullptr; | |
| if (_ua.get_txn() != nullptr) { | |
| vc = _ua.get_txn()->get_netvc(); | |
| if (vc != nullptr) { | |
| fd = vc->get_socket(); | |
| } else { | |
| fd = -1; | |
| } | |
| } | |
| int status = static_cast<int>(status_code); | |
| char client_ip[INET6_ADDRSTRLEN]; | |
| // Log the operator-visible client IP (matches %<chi>) so the slow-request | |
| // line is consistent with squid logs when :pp-clnt is in effect. | |
| ats_ip_ntop(&t_state.effective_client_addr, client_ip, sizeof(client_ip)); | |
| Error("[%" PRId64 "] Slow Request: " | |
| "client_ip: %s:%u " | |
| "protocol: %s " | |
| "url: %s " | |
| "status: %d " | |
| "unique id: %s " | |
| "redirection_tries: %d " | |
| "bytes: %" PRId64 " " | |
| "fd: %d " | |
| "client state: %d " | |
| "server state: %d " | |
| "ua_tls_handshake: %.3f " | |
| "ua_begin: %.3f " | |
| "ua_first_read: %.3f " | |
| "ua_read_header_done: %.3f " | |
| "cache_open_read_begin: %.3f " | |
| "cache_open_read_end: %.3f " | |
| "cache_open_write_begin: %.3f " | |
| "cache_open_write_end: %.3f " | |
| "dns_lookup_begin: %.3f " | |
| "dns_lookup_end: %.3f " | |
| "server_connect: %.3f " | |
| "server_connect_end: %.3f " | |
| "server_tls_handshake: %.3f " | |
| "server_first_read: %.3f " | |
| "server_read_header_done: %.3f " | |
| "server_close: %.3f " | |
| "ua_write: %.3f " | |
| "ua_close: %.3f " | |
| "sm_finish: %.3f " | |
| "plugin_active: %.3f " | |
| "plugin_total: %.3f", | |
| sm_id, client_ip, t_state.effective_client_addr.host_order_port(), | |
| _ua.get_txn() ? _ua.get_txn()->get_protocol_string() : "-1", url_string, status, unique_id_string, redirection_tries, | |
| client_response_body_bytes, fd, t_state.client_info.state, t_state.server_info.state, | |
| milestones.difference_sec(TS_MILESTONE_TLS_HANDSHAKE_START, TS_MILESTONE_TLS_HANDSHAKE_END), | |
| milestones.difference_sec(TS_MILESTONE_SM_START, TS_MILESTONE_UA_BEGIN), | |
| milestones.difference_sec(TS_MILESTONE_SM_START, TS_MILESTONE_UA_FIRST_READ), | |
| milestones.difference_sec(TS_MILESTONE_SM_START, TS_MILESTONE_UA_READ_HEADER_DONE), | |
| milestones.difference_sec(TS_MILESTONE_SM_START, TS_MILESTONE_CACHE_OPEN_READ_BEGIN), | |
| milestones.difference_sec(TS_MILESTONE_SM_START, TS_MILESTONE_CACHE_OPEN_READ_END), | |
| milestones.difference_sec(TS_MILESTONE_SM_START, TS_MILESTONE_CACHE_OPEN_WRITE_BEGIN), | |
| milestones.difference_sec(TS_MILESTONE_SM_START, TS_MILESTONE_CACHE_OPEN_WRITE_END), | |
| milestones.difference_sec(TS_MILESTONE_SM_START, TS_MILESTONE_DNS_LOOKUP_BEGIN), | |
| milestones.difference_sec(TS_MILESTONE_SM_START, TS_MILESTONE_DNS_LOOKUP_END), | |
| milestones.difference_sec(TS_MILESTONE_SM_START, TS_MILESTONE_SERVER_CONNECT), | |
| milestones.difference_sec(TS_MILESTONE_SM_START, TS_MILESTONE_SERVER_CONNECT_END), | |
| milestones.difference_sec(TS_MILESTONE_SM_START, TS_MILESTONE_SERVER_TLS_HANDSHAKE_END), | |
| milestones.difference_sec(TS_MILESTONE_SM_START, TS_MILESTONE_SERVER_FIRST_READ), | |
| milestones.difference_sec(TS_MILESTONE_SM_START, TS_MILESTONE_SERVER_READ_HEADER_DONE), | |
| milestones.difference_sec(TS_MILESTONE_SM_START, TS_MILESTONE_SERVER_CLOSE), | |
| milestones.difference_sec(TS_MILESTONE_SM_START, TS_MILESTONE_UA_BEGIN_WRITE), | |
| milestones.difference_sec(TS_MILESTONE_SM_START, TS_MILESTONE_UA_CLOSE), | |
| milestones.difference_sec(TS_MILESTONE_SM_START, TS_MILESTONE_SM_FINISH), | |
| milestones.difference_sec(TS_MILESTONE_SM_START, TS_MILESTONE_PLUGIN_ACTIVE), | |
| milestones.difference_sec(TS_MILESTONE_SM_START, TS_MILESTONE_PLUGIN_TOTAL)); | |
| } | |
| } | |
| // | |
| // void HttpSM::dump_state_on_assert | |
| // Debugging routine to dump the state machine's history | |
| // and other state on an assertion failure | |
| // We use Diags::Status instead of stderr since | |
| // Diags works both on UNIX & NT | |
| // | |
| void | |
| HttpSM::dump_state_on_assert() | |
| { | |
| Error("[%" PRId64 "] ------- begin http state dump -------", sm_id); | |
| if (history.overflowed()) { | |
| Error(" History Wrap around. history size: %d", history.size()); | |
| } | |
| // Loop through the history and dump it | |
| for (unsigned int i = 0; i < history.size(); i++) { | |
| char buf[256]; | |
| int r = history[i].reentrancy; | |
| int e = history[i].event; | |
| Error("%d %d %s", e, r, history[i].location.str(buf, sizeof(buf))); | |
| } | |
| // Dump the via string | |
| Error("Via String: [%s]\n", t_state.via_string); | |
| // Dump header info | |
| dump_state_hdr(&t_state.hdr_info.client_request, "Client Request"); | |
| dump_state_hdr(&t_state.hdr_info.server_request, "Server Request"); | |
| dump_state_hdr(&t_state.hdr_info.server_response, "Server Response"); | |
| dump_state_hdr(&t_state.hdr_info.transform_response, "Transform Response"); | |
| dump_state_hdr(&t_state.hdr_info.client_response, "Client Response"); | |
| Error("[%" PRId64 "] ------- end http state dump ---------", sm_id); | |
| } | |
| void | |
| HttpSM::dump_state_hdr(HTTPHdr *h, const char *s) | |
| { | |
| // Dump the client request if available | |
| if (h->valid()) { | |
| int l = h->length_get(); | |
| char *hdr_buf = static_cast<char *>(ats_malloc(l + 1)); | |
| int index = 0; | |
| int offset = 0; | |
| h->print(hdr_buf, l, &index, &offset); | |
| hdr_buf[l] = '\0'; | |
| Error(" ---- %s [%" PRId64 "] ----\n%s\n", s, sm_id, hdr_buf); | |
| ats_free(hdr_buf); | |
| } | |
| } | |
| /***************************************************************************** | |
| ***************************************************************************** | |
| **** **** | |
| **** HttpTransact Interface **** | |
| **** **** | |
| ***************************************************************************** | |
| *****************************************************************************/ | |
| ////////////////////////////////////////////////////////////////////////// | |
| // | |
| // HttpSM::call_transact_and_set_next_state(f) | |
| // | |
| // This routine takes an HttpTransact function <f>, calls the function | |
| // to perform some actions on the current HttpTransact::State, and | |
| // then uses the HttpTransact return action code to set the next | |
| // handler (state) for the state machine. HttpTransact could have | |
| // returned the handler directly, but returns action codes in hopes of | |
| // making a cleaner separation between the state machine and the | |
| // HttpTransact logic. | |
| // | |
| ////////////////////////////////////////////////////////////////////////// | |
| // Where is the goatherd? | |
| void | |
| HttpSM::call_transact_and_set_next_state(TransactEntryFunc_t f) | |
| { | |
| last_action = t_state.next_action; // remember where we were | |
| // The callee can either specify a method to call in to Transact, | |
| // or call with NULL which indicates that Transact should use | |
| // its stored entry point. | |
| if (f == nullptr) { | |
| ink_release_assert(t_state.transact_return_point != nullptr); | |
| t_state.transact_return_point(&t_state); | |
| } else { | |
| f(&t_state); | |
| } | |
| SMDbg(dbg_ctl_http, "State Transition: %s -> %s", HttpDebugNames::get_action_name(last_action), | |
| HttpDebugNames::get_action_name(t_state.next_action)); | |
| set_next_state(); | |
| return; | |
| } | |
| ////////////////////////////////////////////////////////////////////////////// | |
| // | |
| // HttpSM::set_next_state() | |
| // | |
| // call_transact_and_set_next_state() was broken into two parts, one | |
| // which calls the HttpTransact method and the second which sets the | |
| // next state. In a case which set_next_state() was not completed, | |
| // the state function calls set_next_state() to retry setting the | |
| // state. | |
| // | |
| ////////////////////////////////////////////////////////////////////////////// | |
| void | |
| HttpSM::set_next_state() | |
| { | |
| /////////////////////////////////////////////////////////////////////// | |
| // Use the returned "next action" code to set the next state handler // | |
| /////////////////////////////////////////////////////////////////////// | |
| switch (t_state.next_action) { | |
| case HttpTransact::StateMachineAction_t::API_PRE_REMAP: | |
| case HttpTransact::StateMachineAction_t::API_POST_REMAP: | |
| case HttpTransact::StateMachineAction_t::API_READ_REQUEST_HDR: | |
| case HttpTransact::StateMachineAction_t::REQUEST_BUFFER_READ_COMPLETE: | |
| case HttpTransact::StateMachineAction_t::API_OS_DNS: | |
| case HttpTransact::StateMachineAction_t::API_SEND_REQUEST_HDR: | |
| case HttpTransact::StateMachineAction_t::API_READ_CACHE_HDR: | |
| case HttpTransact::StateMachineAction_t::API_READ_RESPONSE_HDR: | |
| case HttpTransact::StateMachineAction_t::API_SEND_RESPONSE_HDR: | |
| case HttpTransact::StateMachineAction_t::API_CACHE_LOOKUP_COMPLETE: { | |
| t_state.api_next_action = t_state.next_action; | |
| do_api_callout(); | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::POST_REMAP_SKIP: { | |
| call_transact_and_set_next_state(nullptr); | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::REMAP_REQUEST: { | |
| do_remap_request(true); /* run inline */ | |
| SMDbg(dbg_ctl_url_rewrite, "completed inline remapping request"); | |
| t_state.url_remap_success = remapProcessor.finish_remap(&t_state, m_remap.get()); | |
| if (t_state.next_action == HttpTransact::StateMachineAction_t::SEND_ERROR_CACHE_NOOP && | |
| t_state.transact_return_point == nullptr) { | |
| // It appears that we can now set the next_action to error and transact_return_point to nullptr when | |
| // going through do_remap_request presumably due to a plugin setting an error. In that case, it seems | |
| // that the error message has already been setup, so we can just return and avoid the further | |
| // call_transact_and_set_next_state | |
| } else { | |
| call_transact_and_set_next_state(nullptr); | |
| } | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::DNS_LOOKUP: { | |
| if (sockaddr const *addr; t_state.http_config_param->use_client_target_addr == 2 && // no CTA verification | |
| !t_state.url_remap_success && // wasn't remapped | |
| t_state.parent_result.result != ParentResultType::SPECIFIED && // no parent. | |
| t_state.client_info.is_transparent && // inbound transparent | |
| t_state.dns_info.os_addr_style == ResolveInfo::OS_Addr::TRY_DEFAULT && // haven't tried anything yet. | |
| ats_is_ip(addr = _ua.get_txn()->get_netvc()->get_local_addr())) // valid inbound remote address | |
| { | |
| /* If the connection is client side transparent and the URL was not remapped/directed to | |
| * parent proxy, we can use the client destination IP address instead of doing a DNS lookup. | |
| * This is controlled by the 'use_client_target_addr' configuration parameter. | |
| */ | |
| if (dbg_ctl_dns.on()) { | |
| ip_text_buffer ipb; | |
| SMDbg(dbg_ctl_dns, "Skipping DNS lookup for client supplied target %s.", ats_ip_ntop(addr, ipb, sizeof(ipb))); | |
| } | |
| t_state.dns_info.set_upstream_address(addr); | |
| // Make a note the CTA is being used - don't do this case again. | |
| t_state.dns_info.os_addr_style = ResolveInfo::OS_Addr::TRY_CLIENT; | |
| if (t_state.hdr_info.client_request.version_get() == HTTPVersion(1, 1)) { | |
| t_state.dns_info.http_version = HTTP_1_1; | |
| } else if (t_state.hdr_info.client_request.version_get() == HTTPVersion(1, 0)) { | |
| t_state.dns_info.http_version = HTTP_1_0; | |
| } else if (t_state.hdr_info.client_request.version_get() == HTTPVersion(0, 9)) { | |
| t_state.dns_info.http_version = HTTP_0_9; | |
| } else { | |
| t_state.dns_info.http_version = HTTP_1_1; | |
| } | |
| call_transact_and_set_next_state(nullptr); | |
| break; | |
| } else if (t_state.dns_info.looking_up == ResolveInfo::ORIGIN_SERVER && t_state.txn_conf->no_dns_forward_to_parent && | |
| t_state.parent_result.result != ParentResultType::UNDEFINED) { | |
| // We claim resolved here so the SM does not stall on origin DNS, but no | |
| // address has been obtained. Set addr to INADDR_ANY so the | |
| // ats_is_ip_any guard in HttpTransact::OSDNSLookup produces a clean | |
| // Bad Request rather than reusing leftover State data. | |
| ats_ip4_set(&t_state.dns_info.addr, INADDR_ANY, 0); | |
| t_state.dns_info.resolved_p = true; | |
| call_transact_and_set_next_state(nullptr); | |
| break; | |
| } else if (t_state.dns_info.resolved_p) { | |
| SMDbg(dbg_ctl_dns, "Skipping DNS lookup because the address is already set."); | |
| call_transact_and_set_next_state(nullptr); | |
| break; | |
| } | |
| // else have to do DNS. | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::state_hostdb_lookup); | |
| // We need to close the previous attempt | |
| // Because it could be a server side retry by DNS rr | |
| if (server_entry) { | |
| ink_assert(server_entry->vc_type == HttpVC_t::SERVER_VC); | |
| vc_table.cleanup_entry(server_entry); | |
| server_entry = nullptr; | |
| } else { | |
| // Now that we have gotten the user agent request, we can cancel | |
| // the inactivity timeout associated with it. Note, however, that | |
| // we must not cancel the inactivity timeout if the message | |
| // contains a body. This indicates that a POST operation is taking place and | |
| // that the client is still sending data to the origin server. The | |
| // origin server cannot reply until the entire request is received. In | |
| // light of this dependency, TS must ensure that the client finishes | |
| // sending its request and for this reason, the inactivity timeout | |
| // cannot be cancelled. | |
| if (_ua.get_txn() && | |
| !_ua.get_txn()->has_request_body(t_state.hdr_info.request_content_length, | |
| t_state.client_info.transfer_encoding == HttpTransact::TransferEncoding_t::CHUNKED)) { | |
| _ua.get_txn()->cancel_inactivity_timeout(); | |
| } else if (!_ua.get_txn() || _ua.get_txn()->get_netvc() == nullptr) { | |
| terminate_sm = true; | |
| return; // Give up if there is no session | |
| } | |
| } | |
| ink_assert(t_state.dns_info.looking_up != ResolveInfo::UNDEFINED_LOOKUP); | |
| do_hostdb_lookup(); | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::DNS_REVERSE_LOOKUP: { | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::state_hostdb_reverse_lookup); | |
| do_hostdb_reverse_lookup(); | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::CACHE_LOOKUP: { | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::state_cache_open_read); | |
| // Every lookup starts from the canonical key. A redirect follow or a read | |
| // retry looks up a different object than the one a previous compatibility | |
| // lookup found, so the flag must not carry over. | |
| compatibility_cache_lookup = CompatibilityCacheLookup::COMPAT_CACHE_LOOKUP_NORMAL; | |
| do_cache_lookup_and_read(); | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::ORIGIN_SERVER_OPEN: { | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::state_http_server_open); | |
| // We need to close the previous attempt | |
| if (server_entry) { | |
| ink_assert(server_entry->vc_type == HttpVC_t::SERVER_VC); | |
| vc_table.cleanup_entry(server_entry); | |
| server_entry = nullptr; | |
| } else { | |
| // Now that we have gotten the user agent request, we can cancel | |
| // the inactivity timeout associated with it. Note, however, that | |
| // we must not cancel the inactivity timeout if the message | |
| // contains a body. This indicates that a POST operation is taking place and | |
| // that the client is still sending data to the origin server. The | |
| // origin server cannot reply until the entire request is received. In | |
| // light of this dependency, TS must ensure that the client finishes | |
| // sending its request and for this reason, the inactivity timeout | |
| // cannot be cancelled. | |
| if (_ua.get_txn() && | |
| !_ua.get_txn()->has_request_body(t_state.hdr_info.request_content_length, | |
| t_state.client_info.transfer_encoding == HttpTransact::TransferEncoding_t::CHUNKED)) { | |
| _ua.get_txn()->cancel_inactivity_timeout(); | |
| } else if (!_ua.get_txn()) { | |
| terminate_sm = true; | |
| return; // Give up if there is no session | |
| } | |
| } | |
| ink_hrtime backoff_base = t_state.txn_conf->connect_attempts_retry_backoff_base; | |
| if (backoff_base > 0) { | |
| // Retry with exponential backoff | |
| ink_hrtime backoff = backoff_base << t_state.current.retry_attempts.get(); | |
| SMDbg(dbg_ctl_http_connect, "Retry connect with %" PRId64 " msec backoff", backoff); | |
| this->pending_action = nullptr; | |
| this->pending_action = this_ethread()->schedule_in(this, HRTIME_MSECONDS(backoff), CONNECT_EVENT_RETRY); | |
| } else { | |
| do_http_server_open(); | |
| } | |
| break; | |
| } | |
| // This is called in some case if the 100 continue header is from a HTTP/1.0 server | |
| // Likely an obsolete case now and should probably return an error | |
| case HttpTransact::StateMachineAction_t::SERVER_PARSE_NEXT_HDR: { | |
| setup_server_read_response_header(); | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::INTERNAL_100_RESPONSE: { | |
| setup_100_continue_transfer(); | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::SERVER_READ: { | |
| t_state.source = HttpTransact::Source_t::HTTP_ORIGIN_SERVER; | |
| // The origin may answer before it has read the request body, for instance a POST carrying | |
| // Expect: 100-continue that is answered immediately. Whatever the origin never read is still | |
| // queued on the client connection and would otherwise be framed as the next request, so drain | |
| // it here, or drop keep-alive when it cannot be drained. | |
| bool const is_chunked_body = t_state.client_info.transfer_encoding == HttpTransact::TransferEncoding_t::CHUNKED; | |
| int64_t const declared_body_bytes = t_state.hdr_info.client_request.get_content_length(); | |
| bool const was_body_forwarded = | |
| is_chunked_body ? client_request_body_bytes > 0 : client_request_body_bytes >= declared_body_bytes; | |
| if (_ua.get_txn() != nullptr && (is_chunked_body || declared_body_bytes > 0) && !was_body_forwarded) { | |
| do_drain_request_body(transform_info.vc ? t_state.hdr_info.transform_response : t_state.hdr_info.client_response); | |
| } | |
| if (transform_info.vc) { | |
| ink_assert(t_state.hdr_info.client_response.valid() == 0); | |
| ink_assert((t_state.hdr_info.transform_response.valid() ? true : false) == true); | |
| HttpTunnelProducer *p = setup_server_transfer_to_transform(); | |
| perform_cache_write_action(); | |
| tunnel.tunnel_run(p); | |
| } else { | |
| ink_assert((t_state.hdr_info.client_response.valid() ? true : false) == true); | |
| t_state.api_next_action = HttpTransact::StateMachineAction_t::API_SEND_RESPONSE_HDR; | |
| // check to see if we are going to handle the redirection from server response and if there is a plugin hook set | |
| if (hooks_set && is_redirect_required() == false) { | |
| do_api_callout_internal(); | |
| } else { | |
| do_redirect(); | |
| handle_api_return(); | |
| } | |
| } | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::SERVE_FROM_CACHE: { | |
| ink_assert(t_state.cache_info.action == HttpTransact::CacheAction_t::SERVE || | |
| t_state.cache_info.action == HttpTransact::CacheAction_t::SERVE_AND_DELETE || | |
| t_state.cache_info.action == HttpTransact::CacheAction_t::SERVE_AND_UPDATE); | |
| release_server_session(true); | |
| t_state.source = HttpTransact::Source_t::CACHE; | |
| if (transform_info.vc) { | |
| ink_assert(t_state.hdr_info.client_response.valid() == 0); | |
| ink_assert((t_state.hdr_info.transform_response.valid() ? true : false) == true); | |
| do_drain_request_body(t_state.hdr_info.transform_response); | |
| t_state.hdr_info.cache_response.create(HTTPType::RESPONSE); | |
| t_state.hdr_info.cache_response.copy(&t_state.hdr_info.transform_response); | |
| HttpTunnelProducer *p = setup_cache_transfer_to_transform(); | |
| perform_cache_write_action(); | |
| tunnel.tunnel_run(p); | |
| } else { | |
| ink_assert((t_state.hdr_info.client_response.valid() ? true : false) == true); | |
| do_drain_request_body(t_state.hdr_info.client_response); | |
| t_state.hdr_info.cache_response.create(HTTPType::RESPONSE); | |
| t_state.hdr_info.cache_response.copy(&t_state.hdr_info.client_response); | |
| perform_cache_write_action(); | |
| t_state.api_next_action = HttpTransact::StateMachineAction_t::API_SEND_RESPONSE_HDR; | |
| // check to see if there is a plugin hook set | |
| if (hooks_set) { | |
| do_api_callout_internal(); | |
| } else { | |
| handle_api_return(); | |
| } | |
| } | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::CACHE_ISSUE_WRITE: { | |
| ink_assert(cache_sm.cache_write_vc == nullptr); | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::state_cache_open_write); | |
| do_cache_prepare_write(); | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::INTERNAL_CACHE_WRITE: { | |
| t_state.api_next_action = HttpTransact::StateMachineAction_t::API_SEND_RESPONSE_HDR; | |
| do_api_callout(); | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::INTERNAL_CACHE_NOOP: { | |
| if (server_entry != nullptr && server_entry->in_tunnel == false) { | |
| release_server_session(); | |
| } | |
| // If we're in state SEND_API_RESPONSE_HDR, it means functions | |
| // registered to hook SEND_RESPONSE_HDR have already been called. So we do not | |
| // need to call do_api_callout. Otherwise TS loops infinitely in this state ! | |
| if (t_state.api_next_action == HttpTransact::StateMachineAction_t::API_SEND_RESPONSE_HDR) { | |
| handle_api_return(); | |
| } else { | |
| t_state.api_next_action = HttpTransact::StateMachineAction_t::API_SEND_RESPONSE_HDR; | |
| do_api_callout(); | |
| } | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::INTERNAL_CACHE_DELETE: { | |
| // Nuke all the alternates since this is mostly likely | |
| // the result of a delete method | |
| cache_sm.end_both(); | |
| do_cache_delete_all_alts(); | |
| release_server_session(); | |
| t_state.api_next_action = HttpTransact::StateMachineAction_t::API_SEND_RESPONSE_HDR; | |
| do_api_callout(); | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::INTERNAL_CACHE_UPDATE_HEADERS: { | |
| issue_cache_update(); | |
| cache_sm.close_read(); | |
| release_server_session(); | |
| t_state.api_next_action = HttpTransact::StateMachineAction_t::API_SEND_RESPONSE_HDR; | |
| do_api_callout(); | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::SEND_ERROR_CACHE_NOOP: { | |
| setup_error_transfer(); | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::SSL_TUNNEL: { | |
| t_state.api_next_action = HttpTransact::StateMachineAction_t::API_SEND_RESPONSE_HDR; | |
| do_api_callout(); | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::ORIGIN_SERVER_RAW_OPEN: { | |
| // Pre-emptively set a server connect failure that will be cleared once a WRITE_READY is received from origin or | |
| // bytes are received back | |
| t_state.set_connect_fail(EIO); | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::state_raw_http_server_open); | |
| ink_assert(server_entry == nullptr); | |
| do_http_server_open(true); | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::CACHE_ISSUE_WRITE_TRANSFORM: { | |
| ink_assert(t_state.cache_info.transform_action == HttpTransact::CacheAction_t::PREPARE_TO_WRITE); | |
| if (transform_cache_sm.cache_write_vc) { | |
| // We've already got the write_vc that | |
| // didn't use for the untransformed copy | |
| ink_assert(cache_sm.cache_write_vc == nullptr); | |
| ink_assert(t_state.api_info.cache_untransformed == false); | |
| t_state.cache_info.write_lock_state = HttpTransact::CacheWriteLock_t::SUCCESS; | |
| call_transact_and_set_next_state(nullptr); | |
| } else { | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::state_cache_open_write); | |
| do_cache_prepare_write_transform(); | |
| } | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::TRANSFORM_READ: { | |
| t_state.api_next_action = HttpTransact::StateMachineAction_t::API_SEND_RESPONSE_HDR; | |
| do_api_callout(); | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::READ_PUSH_HDR: { | |
| setup_push_read_response_header(); | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::STORE_PUSH_BODY: { | |
| // This can return NULL - do we really want to run the tunnel in that case? | |
| // But that's how it was before this change. | |
| HttpTunnelProducer *p = setup_push_transfer_to_cache(); | |
| tunnel.tunnel_run(p); | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::CACHE_PREPARE_UPDATE: { | |
| ink_assert(t_state.api_update_cached_object == HttpTransact::UpdateCachedObject_t::CONTINUE); | |
| do_cache_prepare_update(); | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::CACHE_ISSUE_UPDATE: { | |
| if (t_state.api_update_cached_object == HttpTransact::UpdateCachedObject_t::ERROR) { | |
| t_state.cache_info.object_read = nullptr; | |
| cache_sm.close_read(); | |
| } | |
| issue_cache_update(); | |
| call_transact_and_set_next_state(nullptr); | |
| break; | |
| } | |
| case HttpTransact::StateMachineAction_t::WAIT_FOR_FULL_BODY: { | |
| wait_for_full_body(); | |
| break; | |
| } | |
| default: { | |
| ink_release_assert(!"Unknown next action"); | |
| } | |
| } | |
| } | |
| void | |
| HttpSM::do_redirect() | |
| { | |
| SMDbg(dbg_ctl_http_redirect, "enable_redirection %u", enable_redirection); | |
| if (!enable_redirection || redirection_tries >= t_state.txn_conf->number_of_redirections) { | |
| this->postbuf_clear(); | |
| if (enable_redirection && redirection_tries >= t_state.txn_conf->number_of_redirections) { | |
| t_state.squid_codes.subcode = SquidSubcode::NUM_REDIRECTIONS_EXCEEDED; | |
| } | |
| return; | |
| } | |
| // if redirect_url is set by an user's plugin, yts will redirect to this url anyway. | |
| if (is_redirect_required()) { | |
| if (redirect_url != nullptr || | |
| t_state.hdr_info.client_response.field_find(static_cast<std::string_view>(MIME_FIELD_LOCATION))) { | |
| if (Log::transaction_logging_enabled() && t_state.api_info.logging_enabled) { | |
| TransactionLogData log_data(this); | |
| LogAccess accessor(log_data); | |
| if (redirect_url == nullptr) { | |
| if (t_state.squid_codes.log_code == SquidLogCode::TCP_HIT) { | |
| t_state.squid_codes.log_code = SquidLogCode::TCP_HIT_REDIRECT; | |
| } else { | |
| t_state.squid_codes.log_code = SquidLogCode::TCP_MISS_REDIRECT; | |
| } | |
| } else { | |
| if (t_state.squid_codes.log_code == SquidLogCode::TCP_HIT) { | |
| t_state.squid_codes.log_code = SquidLogCode::TCP_HIT_X_REDIRECT; | |
| } else { | |
| t_state.squid_codes.log_code = SquidLogCode::TCP_MISS_X_REDIRECT; | |
| } | |
| } | |
| int ret = Log::access(&accessor); | |
| if (ret & Log::FULL) { | |
| SMDbg(dbg_ctl_http, "Logging system indicates FULL."); | |
| } | |
| if (ret & Log::FAIL) { | |
| Log::error("failed to log transaction for at least one log object"); | |
| } | |
| } | |
| ++redirection_tries; | |
| if (redirect_url != nullptr) { | |
| redirect_request(redirect_url, redirect_url_len); | |
| ats_free((void *)redirect_url); | |
| redirect_url = nullptr; | |
| redirect_url_len = 0; | |
| Metrics::Counter::increment(http_rsb.total_x_redirect); | |
| } else { | |
| // get the location header and setup the redirect | |
| auto redir_url{t_state.hdr_info.client_response.value_get(static_cast<std::string_view>(MIME_FIELD_LOCATION))}; | |
| redirect_request(redir_url.data(), static_cast<int>(redir_url.length())); | |
| } | |
| } else { | |
| enable_redirection = false; | |
| } | |
| } else { | |
| enable_redirection = false; | |
| } | |
| } | |
| void | |
| HttpSM::redirect_request(const char *arg_redirect_url, const int arg_redirect_len) | |
| { | |
| SMDbg(dbg_ctl_http_redirect, "redirect url: %.*s", arg_redirect_len, arg_redirect_url); | |
| // get a reference to the client request header and client url and check to see if the url is valid | |
| HTTPHdr &clientRequestHeader = t_state.hdr_info.client_request; | |
| URL &clientUrl = *clientRequestHeader.url_get(); | |
| if (!clientUrl.valid()) { | |
| return; | |
| } | |
| bool valid_origHost = true; | |
| int origMethod_len{0}; | |
| char origHost[MAXDNAME]; | |
| char origMethod[255]; | |
| int origPort = 80; | |
| if (t_state.hdr_info.server_request.valid()) { | |
| origPort = t_state.hdr_info.server_request.port_get(); | |
| if (auto tmpOrigHost{t_state.hdr_info.server_request.value_get(static_cast<std::string_view>(MIME_FIELD_HOST))}; | |
| !tmpOrigHost.empty()) { | |
| auto hostLen = std::min(tmpOrigHost.length(), sizeof(origHost) - 1); | |
| memcpy(origHost, tmpOrigHost.data(), hostLen); | |
| origHost[hostLen] = '\0'; | |
| } else { | |
| valid_origHost = false; | |
| } | |
| auto tmpOrigMethod{t_state.hdr_info.server_request.method_get()}; | |
| origMethod_len = tmpOrigMethod.length(); | |
| if (!tmpOrigMethod.empty()) { | |
| memcpy(origMethod, tmpOrigMethod.data(), std::min(origMethod_len, static_cast<int>(sizeof(origMethod)))); | |
| } else { | |
| valid_origHost = false; | |
| } | |
| } else { | |
| SMDbg(dbg_ctl_http_redir_error, "t_state.hdr_info.server_request not valid"); | |
| valid_origHost = false; | |
| } | |
| t_state.redirect_info.redirect_in_process = true; | |
| // set the passed in location url and parse it | |
| URL redirectUrl; | |
| redirectUrl.create(nullptr); | |
| redirectUrl.parse(arg_redirect_url, arg_redirect_len); | |
| { | |
| if (redirectUrl.scheme_get().empty() && !redirectUrl.host_get().empty() && arg_redirect_url[0] != '/') { | |
| // RFC7230 § 5.5 | |
| // The redirect URL lacked a scheme and so it is a relative URL. | |
| // The redirect URL did not begin with a slash, so we parsed some or all | |
| // of the relative URI path as the host. | |
| // Prepend a slash and parse again. | |
| std::string redirect_url_leading_slash(arg_redirect_len + 1, '\0'); | |
| redirect_url_leading_slash[0] = '/'; | |
| if (arg_redirect_len > 0) { | |
| memcpy(redirect_url_leading_slash.data() + 1, arg_redirect_url, arg_redirect_len); | |
| } | |
| url_nuke_proxy_stuff(redirectUrl.m_url_impl); | |
| redirectUrl.parse(redirect_url_leading_slash.c_str(), arg_redirect_len + 1); | |
| } | |
| } | |
| // copy the client url to the original url | |
| URL &origUrl = t_state.redirect_info.original_url; | |
| if (!origUrl.valid()) { | |
| origUrl.create(nullptr); | |
| origUrl.copy(&clientUrl); | |
| } | |
| // copy the redirect url to the client url | |
| clientUrl.copy(&redirectUrl); | |
| redirectUrl.destroy(); | |
| //(bug 2540703) Clear the previous response if we will attempt the redirect | |
| if (t_state.hdr_info.client_response.valid()) { | |
| // XXX - doing a destroy() for now, we can do a fileds_clear() if we have performance issue | |
| t_state.hdr_info.client_response.destroy(); | |
| } | |
| int scheme = t_state.next_hop_scheme; | |
| int scheme_len = hdrtoken_index_to_length(scheme); | |
| const char *next_hop_scheme = hdrtoken_index_to_wks(scheme); | |
| char scheme_str[scheme_len + 1]; | |
| if (next_hop_scheme) { | |
| memcpy(scheme_str, next_hop_scheme, scheme_len); | |
| } else { | |
| valid_origHost = false; | |
| } | |
| t_state.hdr_info.server_request.destroy(); | |
| // we want to close the server session | |
| // will do that in handle_api_return under the | |
| // HttpTransact::StateMachineAction_t::REDIRECT_READ state | |
| t_state.parent_result.reset(); | |
| t_state.request_sent_time = 0; | |
| t_state.response_received_time = 0; | |
| t_state.next_action = HttpTransact::StateMachineAction_t::REDIRECT_READ; | |
| // we have a new OS and need to have DNS lookup the new OS | |
| t_state.dns_info.resolved_p = false; | |
| t_state.force_dns = false; | |
| t_state.server_info.clear(); | |
| t_state.parent_info.clear(); | |
| // Must reset whether the InkAPI has set the destination address | |
| // t_state.dns_info.api_addr_set_p = false; | |
| if (t_state.txn_conf->cache_http) { | |
| t_state.cache_info.object_read = nullptr; | |
| } | |
| bool noPortInHost = HttpConfig::m_master.redirection_host_no_port; | |
| bool isRedirectUrlOriginForm = !clientUrl.m_url_impl->m_len_scheme && !clientUrl.m_url_impl->m_len_user && | |
| !clientUrl.m_url_impl->m_len_password && !clientUrl.m_url_impl->m_len_host && | |
| !clientUrl.m_url_impl->m_len_port; | |
| // check to see if the client request passed a host header, if so copy the host and port from the redirect url and | |
| // make a new host header | |
| if (t_state.hdr_info.client_request.presence(MIME_PRESENCE_HOST)) { | |
| auto host{clientUrl.host_get()}; | |
| auto host_len{static_cast<int>(host.length())}; | |
| if (!host.empty()) { | |
| int port = clientUrl.port_get(); | |
| if (auto redirectScheme{clientUrl.scheme_get()}; redirectScheme.empty()) { | |
| clientUrl.scheme_set({scheme_str, static_cast<std::string_view::size_type>(scheme_len)}); | |
| SMDbg(dbg_ctl_http_redirect, "URL without scheme"); | |
| } | |
| if (noPortInHost) { | |
| int redirectSchemeIdx = clientUrl.scheme_get_wksidx(); | |
| bool defaultPort = | |
| (((redirectSchemeIdx == URL_WKSIDX_HTTP) && (port == 80)) || ((redirectSchemeIdx == URL_WKSIDX_HTTPS) && (port == 443))); | |
| if (!defaultPort) { | |
| noPortInHost = false; | |
| } | |
| } | |
| if (!noPortInHost) { | |
| char buf[host_len + 7]; // 5 + 1 + 1 ("12345" + ':' + '\0') | |
| host_len = snprintf(buf, host_len + 7, "%.*s:%d", host_len, host.data(), port); | |
| t_state.hdr_info.client_request.value_set(static_cast<std::string_view>(MIME_FIELD_HOST), | |
| std::string_view{buf, static_cast<std::string_view::size_type>(host_len)}); | |
| } else { | |
| t_state.hdr_info.client_request.value_set(static_cast<std::string_view>(MIME_FIELD_HOST), host); | |
| } | |
| t_state.hdr_info.client_request.m_target_cached = false; | |
| t_state.hdr_info.server_request.m_target_cached = false; | |
| } else { | |
| // the client request didn't have a host, so use the current origin host | |
| if (valid_origHost) { | |
| char *saveptr = nullptr; | |
| // the client request didn't have a host, so use the current origin host | |
| SMDbg(dbg_ctl_http_redirect, "keeping client request host %s://%s", next_hop_scheme, origHost); | |
| char *origHostNoPort = strtok_r(origHost, ":", &saveptr); | |
| if (origHostNoPort == nullptr) { | |
| goto LhostError; | |
| } | |
| host_len = strlen(origHostNoPort); | |
| if (noPortInHost) { | |
| int redirectSchemeIdx = t_state.next_hop_scheme; | |
| bool defaultPort = (((redirectSchemeIdx == URL_WKSIDX_HTTP) && (origPort == 80)) || | |
| ((redirectSchemeIdx == URL_WKSIDX_HTTPS) && (origPort == 443))); | |
| if (!defaultPort) { | |
| noPortInHost = false; | |
| } | |
| } | |
| if (!noPortInHost) { | |
| char buf[host_len + 7]; // 5 + 1 + 1 ("12345" + ':' + '\0') | |
| host_len = snprintf(buf, host_len + 7, "%s:%d", origHostNoPort, origPort); | |
| t_state.hdr_info.client_request.value_set(static_cast<std::string_view>(MIME_FIELD_HOST), | |
| std::string_view{buf, static_cast<std::string_view::size_type>(host_len)}); | |
| } else { | |
| t_state.hdr_info.client_request.value_set( | |
| static_cast<std::string_view>(MIME_FIELD_HOST), | |
| std::string_view{origHostNoPort, static_cast<std::string_view::size_type>(host_len)}); | |
| } | |
| // Cleanup of state etc. | |
| url_nuke_proxy_stuff(clientUrl.m_url_impl); | |
| url_nuke_proxy_stuff(t_state.hdr_info.client_request.m_url_cached.m_url_impl); | |
| t_state.hdr_info.client_request.method_set( | |
| std::string_view{origMethod, std::min(static_cast<std::string_view::size_type>(origMethod_len), sizeof(origMethod))}); | |
| t_state.hdr_info.client_request.m_target_cached = false; | |
| t_state.hdr_info.server_request.m_target_cached = false; | |
| clientUrl.scheme_set({scheme_str, static_cast<std::string_view::size_type>(scheme_len)}); | |
| if (isRedirectUrlOriginForm) { | |
| // build the rest of the effective URL: the authority part | |
| clientUrl.user_set( | |
| {origUrl.m_url_impl->m_ptr_user, static_cast<std::string_view::size_type>(origUrl.m_url_impl->m_len_user)}); | |
| clientUrl.password_set( | |
| {origUrl.m_url_impl->m_ptr_password, static_cast<std::string_view::size_type>(origUrl.m_url_impl->m_len_password)}); | |
| clientUrl.host_set( | |
| {origUrl.m_url_impl->m_ptr_host, static_cast<std::string_view::size_type>(origUrl.m_url_impl->m_len_host)}); | |
| clientUrl.port_set(origUrl.port_get()); | |
| } | |
| } else { | |
| LhostError: | |
| // the server request didn't have a host, so remove it from the headers | |
| t_state.hdr_info.client_request.field_delete(static_cast<std::string_view>(MIME_FIELD_HOST)); | |
| } | |
| } | |
| } | |
| dump_header(dbg_ctl_http_hdrs, &t_state.hdr_info.client_request, sm_id, "Framed Client Request..checking"); | |
| // Reset HttpCacheSM for new cache operations | |
| cache_sm.reset(); | |
| } | |
| void | |
| HttpSM::set_http_schedule(Continuation *contp) | |
| { | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::get_http_schedule); | |
| schedule_cont = contp; | |
| } | |
| int | |
| HttpSM::get_http_schedule(int event, void * /* data ATS_UNUSED */) | |
| { | |
| bool plugin_lock; | |
| Ptr<ProxyMutex> plugin_mutex; | |
| if (schedule_cont->mutex) { | |
| plugin_mutex = schedule_cont->mutex; | |
| plugin_lock = MUTEX_TAKE_TRY_LOCK(schedule_cont->mutex, mutex->thread_holding); | |
| if (!plugin_lock) { | |
| HTTP_SM_SET_DEFAULT_HANDLER(&HttpSM::get_http_schedule); | |
| ink_assert(pending_action.empty()); | |
| pending_action = mutex->thread_holding->schedule_in(this, HRTIME_MSECONDS(10)); | |
| return 0; | |
| } else { | |
| pending_action = nullptr; // if there was a pending action, it'll get freed after this returns so clear it. | |
| } | |
| } else { | |
| plugin_lock = false; | |
| } | |
| // handle Mutex; | |
| schedule_cont->handleEvent(event, this); | |
| if (plugin_lock) { | |
| Mutex_unlock(plugin_mutex, mutex->thread_holding); | |
| } | |
| return 0; | |
| } | |
| /* | |
| * Used from an InkAPI | |
| */ | |
| bool | |
| HttpSM::set_server_session_private(bool private_session) | |
| { | |
| if (server_txn != nullptr) { | |
| static_cast<PoolableSession *>(server_txn->get_proxy_ssn())->set_private(private_session); | |
| return true; | |
| } | |
| return false; | |
| } | |
| bool | |
| HttpSM::is_private() const | |
| { | |
| bool res = false; | |
| if (will_be_private_ss) { | |
| res = will_be_private_ss; | |
| } | |
| return res; | |
| } | |
| // check to see if redirection is enabled and less than max redirections tries or if a plugin enabled redirection | |
| inline bool | |
| HttpSM::is_redirect_required() | |
| { | |
| bool redirect_required = (enable_redirection && (redirection_tries < t_state.txn_conf->number_of_redirections) && | |
| !HttpTransact::is_fresh_cache_hit(t_state.cache_lookup_result)); | |
| SMDbg(dbg_ctl_http_redirect, "redirect_required: %u", redirect_required); | |
| if (redirect_required == true) { | |
| HTTPStatus status = t_state.hdr_info.client_response.status_get(); | |
| // check to see if the response from the origin was a 301, 302, or 303 | |
| switch (status) { | |
| case HTTPStatus::MULTIPLE_CHOICES: // 300 | |
| case HTTPStatus::MOVED_PERMANENTLY: // 301 | |
| case HTTPStatus::MOVED_TEMPORARILY: // 302 | |
| case HTTPStatus::SEE_OTHER: // 303 | |
| case HTTPStatus::USE_PROXY: // 305 | |
| case HTTPStatus::TEMPORARY_REDIRECT: // 307 | |
| case HTTPStatus::PERMANENT_REDIRECT: // 308 | |
| redirect_required = true; | |
| break; | |
| default: | |
| redirect_required = false; | |
| break; | |
| } | |
| // if redirect_url is set by an user's plugin, ats will redirect to this url anyway. | |
| if (redirect_url != nullptr) { | |
| redirect_required = true; | |
| } | |
| } | |
| return redirect_required; | |
| } | |
| SNIRoutingType | |
| HttpSM::get_tunnel_type() const | |
| { | |
| return _tunnel_type; | |
| } | |
| // Fill in the client protocols used. Return the number of entries populated. | |
| int | |
| HttpSM::populate_client_protocol(std::string_view *result, int n) const | |
| { | |
| int retval = 0; | |
| if (n > 0) { | |
| std::string_view proto = HttpSM::find_proto_string(t_state.hdr_info.client_request.version_get()); | |
| if (!proto.empty()) { | |
| result[retval++] = proto; | |
| if (n > retval && _ua.get_txn()) { | |
| retval += _ua.get_txn()->populate_protocol(result + retval, n - retval); | |
| } | |
| } | |
| } | |
| return retval; | |
| } | |
| // Look for a specific client protocol | |
| const char * | |
| HttpSM::client_protocol_contains(std::string_view tag_prefix) const | |
| { | |
| const char *retval = nullptr; | |
| std::string_view proto = HttpSM::find_proto_string(t_state.hdr_info.client_request.version_get()); | |
| if (!proto.empty()) { | |
| std::string_view prefix(tag_prefix); | |
| if (prefix.size() <= proto.size() && 0 == strncmp(proto.data(), prefix.data(), prefix.size())) { | |
| retval = proto.data(); | |
| } else if (_ua.get_txn()) { | |
| retval = _ua.get_txn()->protocol_contains(prefix); | |
| } | |
| } | |
| return retval; | |
| } | |
| // Fill in the server protocols used. Return the number of entries populated. | |
| int | |
| HttpSM::populate_server_protocol(std::string_view *result, int n) const | |
| { | |
| int retval = 0; | |
| if (!t_state.hdr_info.server_request.valid()) { | |
| return retval; | |
| } | |
| if (n > 0) { | |
| std::string_view proto = HttpSM::find_proto_string(t_state.hdr_info.server_request.version_get()); | |
| if (!proto.empty()) { | |
| result[retval++] = proto; | |
| if (n > retval && server_txn) { | |
| retval += server_txn->populate_protocol(result + retval, n - retval); | |
| } | |
| } | |
| } | |
| return retval; | |
| } | |
| // Look for a specific server protocol | |
| const char * | |
| HttpSM::server_protocol_contains(std::string_view tag_prefix) const | |
| { | |
| const char *retval = nullptr; | |
| std::string_view proto = HttpSM::find_proto_string(t_state.hdr_info.server_request.version_get()); | |
| if (!proto.empty()) { | |
| std::string_view prefix(tag_prefix); | |
| if (prefix.size() <= proto.size() && 0 == strncmp(proto.data(), prefix.data(), prefix.size())) { | |
| retval = proto.data(); | |
| } else { | |
| if (server_txn) { | |
| retval = server_txn->protocol_contains(prefix); | |
| } | |
| } | |
| } | |
| return retval; | |
| } | |
| std::string_view | |
| HttpSM::find_proto_string(HTTPVersion version) const | |
| { | |
| if (version == HTTP_2_0) { | |
| return IP_PROTO_TAG_HTTP_2_0; | |
| } else if (version == HTTP_1_1) { | |
| return IP_PROTO_TAG_HTTP_1_1; | |
| } else if (version == HTTP_1_0) { | |
| return IP_PROTO_TAG_HTTP_1_0; | |
| } else if (version == HTTP_0_9) { | |
| return IP_PROTO_TAG_HTTP_0_9; | |
| } | |
| return {}; | |
| } | |
| void | |
| HttpSM::rewind_state_machine() | |
| { | |
| callout_state = HttpApiState_t::REWIND_STATE_MACHINE; | |
| } | |
| // YTS Team, yamsat Plugin | |
| // Function to copy the partial Post data while tunnelling | |
| int64_t | |
| PostDataBuffers::copy_partial_post_data(int64_t consumed_bytes) | |
| { | |
| if (post_data_buffer_done) { | |
| return 0; | |
| } | |
| int64_t const bytes_to_copy = std::min(consumed_bytes, this->ua_buffer_reader->read_avail()); | |
| Dbg(dbg_ctl_http_redirect, | |
| "given %" PRId64 " bytes consumed, copying %" PRId64 " bytes to buffers with %" PRId64 " available bytes", consumed_bytes, | |
| bytes_to_copy, this->ua_buffer_reader->read_avail()); | |
| this->postdata_copy_buffer->write(this->ua_buffer_reader, bytes_to_copy); | |
| this->ua_buffer_reader->consume(bytes_to_copy); | |
| return bytes_to_copy; | |
| } | |
| IOBufferReader * | |
| PostDataBuffers::get_post_data_buffer_clone_reader() | |
| { | |
| return this->postdata_copy_buffer->clone_reader(this->postdata_copy_buffer_start); | |
| } | |
| // YTS Team, yamsat Plugin | |
| // Allocating the post data buffers | |
| void | |
| PostDataBuffers::init(IOBufferReader *ua_reader) | |
| { | |
| Dbg(dbg_ctl_http_redirect, "[PostDataBuffers::init]"); | |
| this->ua_buffer_reader = ua_reader; | |
| if (this->postdata_copy_buffer == nullptr) { | |
| this->post_data_buffer_done = false; | |
| ink_assert(this->postdata_copy_buffer_start == nullptr); | |
| this->postdata_copy_buffer = new_empty_MIOBuffer(BUFFER_SIZE_INDEX_4K); | |
| this->postdata_copy_buffer_start = this->postdata_copy_buffer->alloc_reader(); | |
| } | |
| ink_assert(this->ua_buffer_reader != nullptr); | |
| } | |
| // YTS Team, yamsat Plugin | |
| // Deallocating the post data buffers | |
| void | |
| PostDataBuffers::clear() | |
| { | |
| Dbg(dbg_ctl_http_redirect, "[PostDataBuffers::clear]"); | |
| if (this->postdata_copy_buffer != nullptr) { | |
| free_MIOBuffer(this->postdata_copy_buffer); | |
| this->postdata_copy_buffer = nullptr; | |
| this->postdata_copy_buffer_start = nullptr; // deallocated by the buffer | |
| } | |
| this->post_data_buffer_done = false; | |
| } | |
| PostDataBuffers::~PostDataBuffers() | |
| { | |
| this->clear(); | |
| } | |
| HTTPVersion | |
| HttpSM::get_server_version(HTTPHdr &hdr) const | |
| { | |
| return this->server_txn->get_proxy_ssn()->get_version(hdr); | |
| } | |
| /// Update the milestone state given the milestones and timer. | |
| void | |
| HttpSM::milestone_update_api_time() | |
| { | |
| // Bit of funkiness - we set @a api_timer to be the negative value when we're tracking | |
| // non-active API time. In that case we need to make a note of it and flip the value back | |
| // to positive. | |
| if (api_timer) { | |
| ink_hrtime delta; | |
| bool active = api_timer >= 0; | |
| if (!active) { | |
| api_timer = -api_timer; | |
| } | |
| // Zero or negative time is a problem because we want to signal *something* happened | |
| // vs. no API activity at all. This can happen due to graininess or real time | |
| // clock adjustment. | |
| delta = std::max<ink_hrtime>(1, ink_get_hrtime() - api_timer); | |
| api_timer = 0; | |
| if (0 == milestones[TS_MILESTONE_PLUGIN_TOTAL]) { | |
| milestones[TS_MILESTONE_PLUGIN_TOTAL] = milestones[TS_MILESTONE_SM_START]; | |
| } | |
| milestones[TS_MILESTONE_PLUGIN_TOTAL] += delta; | |
| if (active) { | |
| if (0 == milestones[TS_MILESTONE_PLUGIN_ACTIVE]) { | |
| milestones[TS_MILESTONE_PLUGIN_ACTIVE] = milestones[TS_MILESTONE_SM_START]; | |
| } | |
| milestones[TS_MILESTONE_PLUGIN_ACTIVE] += delta; | |
| } | |
| this_ethread()->metrics.record_api_time(delta); | |
| } | |
| } |