plugins/esi/esi.cc (1,519 lines of code) (raw):

/** @file A brief file description @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 "tscore/ink_defs.h" #include <cstdio> #include <cstdlib> #include <climits> #include <cstring> #include <string> #include <list> #include <new> #include <limits> #include <arpa/inet.h> #include <getopt.h> #include "ts/ts.h" #include "ts/remap.h" #include "Utils.h" #include "gzip.h" #include "EsiGzip.h" #include "EsiGunzip.h" #include "EsiProcessor.h" #include "HttpDataFetcher.h" #include "HandlerManager.h" #include "serverIntercept.h" #include "Stats.h" #include "HttpDataFetcherImpl.h" using std::list; using std::string; using namespace EsiLib; using namespace Stats; struct OptionInfo { bool packed_node_support{false}; bool private_response{false}; bool disable_gzip_output{false}; bool first_byte_flush{false}; unsigned max_doc_size{1024 * 1024}; }; static HandlerManager *gHandlerManager = nullptr; static Utils::HeaderValueList gAllowlistCookies; #define DEBUG_TAG "plugin_esi" #define FETCHER_DEBUG_TAG "plugin_esi_fetcher" #define MIME_FIELD_XESI "X-Esi" #define MIME_FIELD_XESI_LEN 5 #define HTTP_VALUE_PRIVATE_EXPIRES "-1" #define HTTP_VALUE_PRIVATE_CC "max-age=0, private" enum DataType { DATA_TYPE_RAW_ESI = 0, DATA_TYPE_GZIPPED_ESI = 1, DATA_TYPE_PACKED_ESI = 2, }; static const char *DATA_TYPE_NAMES_[] = {"RAW_ESI", "GZIPPED_ESI", "PACKED_ESI"}; static const char *HEADER_MASK_PREFIX = "Mask-"; static const int HEADER_MASK_PREFIX_SIZE = 5; static DbgCtl dbg_ctl_local{DEBUG_TAG}; struct ContData { enum STATE { READING_ESI_DOC, FETCHING_DATA, PROCESSING_COMPLETE, }; STATE curr_state; TSVIO input_vio; TSIOBufferReader input_reader = nullptr; TSVIO output_vio; TSIOBuffer output_buffer; TSIOBufferReader output_reader; Variables *esi_vars; HttpDataFetcherImpl *data_fetcher; EsiProcessor *esi_proc; EsiGzip *esi_gzip; EsiGunzip *esi_gunzip; TSCont contp; TSHttpTxn txnp; const OptionInfo *const option_info; char *request_url; sockaddr const *client_addr; DataType input_type; string packed_node_list; string gzipped_data; bool gzip_output; bool initialized; bool xform_closed; bool intercept_header; bool cache_txn; bool head_only; bool os_response_cacheable; list<string> post_headers; ContData(TSCont contptr, TSHttpTxn tx, const OptionInfo *opt_info) : curr_state(READING_ESI_DOC), input_vio(nullptr), output_vio(nullptr), output_buffer(nullptr), output_reader(nullptr), esi_vars(nullptr), data_fetcher(nullptr), esi_proc(nullptr), esi_gzip(nullptr), esi_gunzip(nullptr), contp(contptr), txnp(tx), option_info(opt_info), request_url(nullptr), input_type(DATA_TYPE_RAW_ESI), packed_node_list(""), gzipped_data(""), gzip_output(false), initialized(false), xform_closed(false), intercept_header(false), cache_txn(false), head_only(false), os_response_cacheable(true) { client_addr = TSHttpTxnClientAddrGet(txnp); } void fillPostHeader(TSMBuffer bufp, TSMLoc hdr_loc); void getClientState(); void getServerState(); void checkXformStatus(); bool init(); ~ContData(); }; #define CONT_DATA_DBG(CONT_DATA_PTR, FMT, ...) Dbg(dbg_ctl_local, FMT " contp=%p", ##__VA_ARGS__, (CONT_DATA_PTR)) class TSStatSystem : public StatSystem { public: void create(int handle) override { g_stat_indices[handle] = TSStatCreate(Stats::STAT_NAMES[handle], TS_RECORDDATATYPE_INT, TS_STAT_PERSISTENT, TS_STAT_SYNC_COUNT); } void increment(int handle, int step = 1) override { TSStatIntIncrement(g_stat_indices[handle], step); } }; static bool checkHeaderValue(TSMBuffer bufp, TSMLoc hdr_loc, const char *name, int name_len, const char *exp_value = nullptr, int exp_value_len = 0, bool prefix = false); // forward decl static bool checkForCacheHeader(const char *name, int name_len, const char *value, int value_len, bool &cacheable); void ContData::checkXformStatus() { if (!xform_closed) { int retval = TSVConnClosedGet(contp); if ((retval == TS_ERROR) || retval) { if (retval == TS_ERROR) { CONT_DATA_DBG(this, "[%s] Error while getting close status of transformation at state %d", __FUNCTION__, curr_state); } else { CONT_DATA_DBG(this, "[%s] Vconn closed", __FUNCTION__); } xform_closed = true; } } } bool ContData::init() { if (initialized) { TSError("[esi][%s] ContData already initialized!", __FUNCTION__); return false; } checkXformStatus(); bool retval = false; if (!xform_closed) { // Get upstream VIO input_vio = TSVConnWriteVIOGet(contp); if (!input_vio) { TSError("[esi][%s] Error while getting input vio", __FUNCTION__); goto lReturn; } input_reader = TSVIOReaderGet(input_vio); // get downstream VIO TSVConn output_conn; output_conn = TSTransformOutputVConnGet(contp); if (!output_conn) { TSError("[esi][%s] Error while getting transform VC", __FUNCTION__); goto lReturn; } output_buffer = TSIOBufferCreate(); output_reader = TSIOBufferReaderAlloc(output_buffer); // we don't know how much data we are going to write, so INT_MAX output_vio = TSVConnWrite(output_conn, contp, output_reader, INT64_MAX); string fetcher_tag, vars_tag, expr_tag, proc_tag, gzip_tag, gunzip_tag; if (!data_fetcher) { data_fetcher = new HttpDataFetcherImpl(contp, client_addr, FETCHER_DEBUG_TAG); } if (!esi_vars) { esi_vars = new Variables(contp, gAllowlistCookies); } esi_proc = new EsiProcessor(contp, *data_fetcher, *esi_vars, *gHandlerManager, option_info->max_doc_size); esi_gzip = new EsiGzip(); esi_gunzip = new EsiGunzip(); CONT_DATA_DBG(this, "[%s] Set input data type to [%s]", __FUNCTION__, DATA_TYPE_NAMES_[input_type]); retval = true; } else { CONT_DATA_DBG(this, "[%s] Transformation closed during initialization; Returning false", __FUNCTION__); } lReturn: initialized = true; return retval; } void ContData::getClientState() { TSMBuffer req_bufp; TSMLoc req_hdr_loc; if (TSHttpTxnClientReqGet(txnp, &req_bufp, &req_hdr_loc) != TS_SUCCESS) { TSError("[esi][%s] Error while retrieving client request", __FUNCTION__); return; } if (!esi_vars) { string vars_tag; esi_vars = new Variables(contp, gAllowlistCookies); } if (!data_fetcher) { string fetcher_tag; data_fetcher = new HttpDataFetcherImpl(contp, client_addr, FETCHER_DEBUG_TAG); } if (req_bufp && req_hdr_loc) { TSMBuffer bufp; TSMLoc url_loc; if (TSHttpTxnPristineUrlGet(txnp, &bufp, &url_loc) != TS_SUCCESS) { TSError("[esi][%s] Error while retrieving hdr url", __FUNCTION__); return; } if (url_loc) { if (request_url) { TSfree(request_url); } int length; request_url = TSUrlStringGet(bufp, url_loc, &length); Dbg(dbg_ctl_local, "[%s] Got request URL [%s]", __FUNCTION__, request_url ? request_url : "(null)"); int query_len; const char *query = TSUrlHttpQueryGet(bufp, url_loc, &query_len); if (query) { esi_vars->populate(query, query_len); } TSHandleMLocRelease(bufp, req_hdr_loc, url_loc); } TSMLoc field_loc = TSMimeHdrFieldGet(req_bufp, req_hdr_loc, 0); while (field_loc) { TSMLoc next_field_loc; const char *name; int name_len; name = TSMimeHdrFieldNameGet(req_bufp, req_hdr_loc, field_loc, &name_len); if (name) { int n_values; n_values = TSMimeHdrFieldValuesCount(req_bufp, req_hdr_loc, field_loc); if (n_values && (n_values != TS_ERROR)) { const char *value = nullptr; int value_len = 0; if (n_values == 1) { value = TSMimeHdrFieldValueStringGet(req_bufp, req_hdr_loc, field_loc, 0, &value_len); if (nullptr != value && value_len) { if (Utils::areEqual(name, name_len, TS_MIME_FIELD_ACCEPT_ENCODING, TS_MIME_LEN_ACCEPT_ENCODING) && Utils::areEqual(value, value_len, TS_HTTP_VALUE_GZIP, TS_HTTP_LEN_GZIP)) { gzip_output = true; } } } else { for (int i = 0; i < n_values; ++i) { value = TSMimeHdrFieldValueStringGet(req_bufp, req_hdr_loc, field_loc, i, &value_len); if (nullptr != value && value_len) { if (Utils::areEqual(name, name_len, TS_MIME_FIELD_ACCEPT_ENCODING, TS_MIME_LEN_ACCEPT_ENCODING) && Utils::areEqual(value, value_len, TS_HTTP_VALUE_GZIP, TS_HTTP_LEN_GZIP)) { gzip_output = true; } } } value = TSMimeHdrFieldValueStringGet(req_bufp, req_hdr_loc, field_loc, -1, &value_len); } if (value != nullptr) { HttpHeader header(name, name_len, value, value_len); data_fetcher->useHeader(header); esi_vars->populate(header); } } } next_field_loc = TSMimeHdrFieldNext(req_bufp, req_hdr_loc, field_loc); TSHandleMLocRelease(req_bufp, req_hdr_loc, field_loc); field_loc = next_field_loc; } } if (gzip_output) { if (option_info->disable_gzip_output) { Dbg(dbg_ctl_local, "[%s] disable gzip output", __FUNCTION__); gzip_output = false; } else { Dbg(dbg_ctl_local, "[%s] Client accepts gzip encoding; will compress output", __FUNCTION__); } } TSHandleMLocRelease(req_bufp, TS_NULL_MLOC, req_hdr_loc); } void ContData::fillPostHeader(TSMBuffer bufp, TSMLoc hdr_loc) { int n_mime_headers = TSMimeHdrFieldsCount(bufp, hdr_loc); TSMLoc field_loc; const char *name, *value; int name_len, value_len; string header; for (int i = 0; i < n_mime_headers; ++i) { field_loc = TSMimeHdrFieldGet(bufp, hdr_loc, i); if (!field_loc) { Dbg(dbg_ctl_local, "[%s] Error while obtaining header field #%d", __FUNCTION__, i); continue; } name = TSMimeHdrFieldNameGet(bufp, hdr_loc, field_loc, &name_len); if (name) { if (Utils::areEqual(name, name_len, TS_MIME_FIELD_TRANSFER_ENCODING, TS_MIME_LEN_TRANSFER_ENCODING)) { Dbg(dbg_ctl_local, "[%s] Not retaining transfer encoding header", __FUNCTION__); } else if (Utils::areEqual(name, name_len, MIME_FIELD_XESI, MIME_FIELD_XESI_LEN)) { Dbg(dbg_ctl_local, "[%s] Not retaining 'X-Esi' header", __FUNCTION__); } else if (Utils::areEqual(name, name_len, TS_MIME_FIELD_CONTENT_LENGTH, TS_MIME_LEN_CONTENT_LENGTH)) { Dbg(dbg_ctl_local, "[%s] Not retaining 'Content-length' header", __FUNCTION__); } else { header.assign(name, name_len); header.append(": "); int n_field_values = TSMimeHdrFieldValuesCount(bufp, hdr_loc, field_loc); for (int j = 0; j < n_field_values; ++j) { value = TSMimeHdrFieldValueStringGet(bufp, hdr_loc, field_loc, j, &value_len); if (nullptr == value || !value_len) { Dbg(dbg_ctl_local, "[%s] Error while getting value #%d of header [%.*s]", __FUNCTION__, j, name_len, name); } else { if (Utils::areEqual(name, name_len, TS_MIME_FIELD_VARY, TS_MIME_LEN_VARY) && Utils::areEqual(value, value_len, TS_MIME_FIELD_ACCEPT_ENCODING, TS_MIME_LEN_ACCEPT_ENCODING)) { Dbg(dbg_ctl_local, "[%s] Not retaining 'vary: accept-encoding' header", __FUNCTION__); } else if (Utils::areEqual(name, name_len, TS_MIME_FIELD_CONTENT_ENCODING, TS_MIME_LEN_CONTENT_ENCODING) && Utils::areEqual(value, value_len, TS_HTTP_VALUE_GZIP, TS_HTTP_LEN_GZIP)) { Dbg(dbg_ctl_local, "[%s] Not retaining 'content-encoding: gzip' header", __FUNCTION__); } else { if (header[header.size() - 2] != ':') { header.append(", "); } header.append(value, value_len); checkForCacheHeader(name, name_len, value, value_len, os_response_cacheable); if (!os_response_cacheable) { Dbg(dbg_ctl_local, "[%s] Header [%.*s] with value [%.*s] is a no-cache header", __FUNCTION__, name_len, name, value_len, value); break; } } } // end if got value string } // end value iteration if (static_cast<int>(header.size()) > (name_len + 2 /* for ': ' */)) { header.append("\r\n"); post_headers.push_back(header); } } // end if processable header } // end if got header name TSHandleMLocRelease(bufp, hdr_loc, field_loc); if (!os_response_cacheable) { post_headers.clear(); break; } } // end header iteration } void ContData::getServerState() { TSMBuffer bufp; TSMLoc hdr_loc; if (cache_txn) { if (intercept_header) { input_type = DATA_TYPE_PACKED_ESI; return; } else if (TSHttpTxnCachedRespGet(txnp, &bufp, &hdr_loc) != TS_SUCCESS) { TSError("[esi][%s] Could not get server response; set input type to RAW_ESI", __FUNCTION__); input_type = DATA_TYPE_RAW_ESI; return; } } else if (TSHttpTxnServerRespGet(txnp, &bufp, &hdr_loc) != TS_SUCCESS) { TSError("[esi][%s] Could not get server response; set input type to RAW_ESI", __FUNCTION__); input_type = DATA_TYPE_RAW_ESI; return; } if (checkHeaderValue(bufp, hdr_loc, TS_MIME_FIELD_CONTENT_ENCODING, TS_MIME_LEN_CONTENT_ENCODING, TS_HTTP_VALUE_GZIP, TS_HTTP_LEN_GZIP)) { input_type = DATA_TYPE_GZIPPED_ESI; } else { input_type = DATA_TYPE_RAW_ESI; } if (option_info->packed_node_support && !cache_txn && !head_only) { fillPostHeader(bufp, hdr_loc); } TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc); } ContData::~ContData() { CONT_DATA_DBG(this, "[%s] Destroying continuation data", __FUNCTION__); if (output_reader) { TSIOBufferReaderFree(output_reader); } if (output_buffer) { TSIOBufferDestroy(output_buffer); } if (request_url) { TSfree(request_url); } if (esi_vars) { delete esi_vars; } if (data_fetcher) { delete data_fetcher; } if (esi_proc) { delete esi_proc; } if (esi_gzip) { delete esi_gzip; } if (esi_gunzip) { delete esi_gunzip; } } static int removeCacheHandler(TSCont contp, TSEvent /* event ATS_UNUSED */, void * /* edata ATS_UNUSED */) { TSContDestroy(contp); // just ignore cache remove message return 0; } static bool removeCacheKey(TSHttpTxn txnp) { TSMBuffer req_bufp; TSMLoc req_hdr_loc; TSMLoc url_loc = nullptr; TSCont contp = nullptr; TSCacheKey cacheKey = nullptr; bool result = false; if (TSHttpTxnClientReqGet(txnp, &req_bufp, &req_hdr_loc) != TS_SUCCESS) { TSError("[esi][%s] Error while retrieving client request", __FUNCTION__); return false; } do { if (TSHttpTxnPristineUrlGet(txnp, &req_bufp, &url_loc) != TS_SUCCESS) { TSError("[esi][%s] Error while retrieving hdr url", __FUNCTION__); break; } contp = TSContCreate(removeCacheHandler, nullptr); if (contp == nullptr) { TSError("[esi][%s] Could not create continuation", __FUNCTION__); break; } cacheKey = TSCacheKeyCreate(); if (cacheKey == nullptr) { TSError("[esi][%s] TSCacheKeyCreate fail", __FUNCTION__); break; } if (TSCacheKeyDigestFromUrlSet(cacheKey, url_loc) != TS_SUCCESS) { TSError("[esi][%s] TSCacheKeyDigestFromUrlSet fail", __FUNCTION__); break; } TSCacheRemove(contp, cacheKey); result = true; TSError("[esi][%s] TSCacheRemoved", __FUNCTION__); } while (false); if (cacheKey != nullptr) { TSCacheKeyDestroy(cacheKey); } if (!result) { if (contp != nullptr) { TSContDestroy(contp); } } TSHandleMLocRelease(req_bufp, req_hdr_loc, url_loc); if (req_hdr_loc != nullptr) { TSHandleMLocRelease(req_bufp, TS_NULL_MLOC, req_hdr_loc); } return result; } static void cacheNodeList(ContData *cont_data) { bool client_abort; if (TSHttpTxnAborted(cont_data->txnp, &client_abort) == TS_SUCCESS) { CONT_DATA_DBG(cont_data, "[%s] Not caching node list as txn has been aborted", __FUNCTION__); return; } string post_request(""); post_request.append(TS_HTTP_METHOD_POST); post_request.append(" "); post_request.append(cont_data->request_url); post_request.append(" HTTP/1.0\r\n"); post_request.append(SERVER_INTERCEPT_HEADER); post_request.append(": cache=1\r\n"); for (list<string>::iterator list_iter = cont_data->post_headers.begin(); list_iter != cont_data->post_headers.end(); ++list_iter) { post_request.append(ECHO_HEADER_PREFIX); if (((int)list_iter->length() > HEADER_MASK_PREFIX_SIZE) && (strncmp(list_iter->c_str(), HEADER_MASK_PREFIX, HEADER_MASK_PREFIX_SIZE) == 0)) { post_request.append(list_iter->c_str() + HEADER_MASK_PREFIX_SIZE, list_iter->length() - HEADER_MASK_PREFIX_SIZE); } else { post_request.append(*list_iter); } } post_request.append(TS_MIME_FIELD_ACCEPT_ENCODING, TS_MIME_LEN_ACCEPT_ENCODING); post_request.append(": "); post_request.append(TS_HTTP_VALUE_GZIP, TS_HTTP_LEN_GZIP); post_request.append("\r\n"); string body(""); cont_data->esi_proc->packNodeList(body, false); char buf[64]; snprintf(buf, 64, "%s: %d\r\n\r\n", TS_MIME_FIELD_CONTENT_LENGTH, static_cast<int>(body.size())); post_request.append(buf); post_request.append(body); TSFetchEvent event_ids = {0, 0, 0}; TSFetchUrl(post_request.data(), post_request.size(), cont_data->client_addr, cont_data->contp, NO_CALLBACK, event_ids); } static int transformData(TSCont contp) { ContData *cont_data; int64_t toread, consumed = 0, avail; bool input_vio_buf_null = false; bool process_input_complete = false; // Get the output (downstream) vconnection where we'll write data to. cont_data = static_cast<ContData *>(TSContDataGet(contp)); // If the input VIO's buffer is NULL, we need to terminate the transformation if (!TSVIOBufferGet(cont_data->input_vio)) { input_vio_buf_null = true; if (cont_data->curr_state == ContData::PROCESSING_COMPLETE) { CONT_DATA_DBG(cont_data, "[%s] input_vio NULL, marking transformation to be terminated", __FUNCTION__); return 1; } else if (cont_data->curr_state == ContData::READING_ESI_DOC) { CONT_DATA_DBG(cont_data, "[%s] input_vio NULL while in read state. Assuming end of input", __FUNCTION__); process_input_complete = true; } else { if (!cont_data->data_fetcher->isFetchComplete()) { CONT_DATA_DBG(cont_data, "[%s] input_vio NULL, but data needs to be fetched. Returning control", __FUNCTION__); if (!cont_data->option_info->first_byte_flush) { return 1; } } else { CONT_DATA_DBG(cont_data, "[%s] input_vio NULL, but processing needs to (and can) be completed", __FUNCTION__); } } } if (!process_input_complete && (cont_data->curr_state == ContData::READING_ESI_DOC)) { // Determine how much data we have left to read. toread = TSVIONTodoGet(cont_data->input_vio); CONT_DATA_DBG(cont_data, "[%s] upstream VC has %" PRId64 " bytes available to read", __FUNCTION__, toread); if (toread > 0) { avail = TSIOBufferReaderAvail(cont_data->input_reader); if (avail == TS_ERROR) { TSError("[esi][%s] Error while getting number of bytes available", __FUNCTION__); return 0; } // There are some data available for reading. Let's parse it if (avail > 0) { int64_t data_len; const char *data; TSIOBufferBlock block = TSIOBufferReaderStart(cont_data->input_reader); // Now start extraction while (block != nullptr) { data = TSIOBufferBlockReadStart(block, cont_data->input_reader, &data_len); if (cont_data->input_type == DATA_TYPE_RAW_ESI) { cont_data->esi_proc->addParseData(data, data_len); } else if (cont_data->input_type == DATA_TYPE_GZIPPED_ESI) { string udata = ""; cont_data->esi_gunzip->stream_decode(data, data_len, udata); cont_data->esi_proc->addParseData(udata.data(), udata.size()); } else { cont_data->packed_node_list.append(data, data_len); } CONT_DATA_DBG(cont_data, "[%s] Added chunk of %" PRId64 " bytes starting with [%.10s] to parse list", __FUNCTION__, data_len, (data_len ? data : "(null)")); consumed += data_len; block = TSIOBufferBlockNext(block); } } CONT_DATA_DBG(cont_data, "[%s] Consumed %" PRId64 " bytes from upstream VC", __FUNCTION__, consumed); TSIOBufferReaderConsume(cont_data->input_reader, consumed); // Modify the input VIO to reflect how much data we've completed. TSVIONDoneSet(cont_data->input_vio, TSVIONDoneGet(cont_data->input_vio) + consumed); toread = TSVIONTodoGet(cont_data->input_vio); // set this for the test after this if block } if (toread > 0) { // testing this again because it might have changed in previous if block // let upstream know we are ready to read new data TSContCall(TSVIOContGet(cont_data->input_vio), TS_EVENT_VCONN_WRITE_READY, cont_data->input_vio); } else { // we have consumed everything that there was to read process_input_complete = true; } } if (process_input_complete) { CONT_DATA_DBG(cont_data, "[%s] Completed reading input", __FUNCTION__); if (cont_data->input_type == DATA_TYPE_PACKED_ESI) { Dbg(dbg_ctl_local, "[%s] Going to use packed node list of size %d", __FUNCTION__, static_cast<int>(cont_data->packed_node_list.size())); if (cont_data->esi_proc->usePackedNodeList(cont_data->packed_node_list) == EsiProcessor::UNPACK_FAILURE) { removeCacheKey(cont_data->txnp); cont_data->input_type = DATA_TYPE_RAW_ESI; cont_data->esi_proc->start(); cont_data->esi_proc->addParseData(cont_data->packed_node_list.data(), cont_data->packed_node_list.size()); } } if (cont_data->input_type != DATA_TYPE_PACKED_ESI) { bool gunzip_complete = true; if (cont_data->input_type == DATA_TYPE_GZIPPED_ESI) { gunzip_complete = cont_data->esi_gunzip->stream_finish(); } if (cont_data->esi_proc->completeParse() && gunzip_complete) { if (cont_data->option_info->packed_node_support && cont_data->os_response_cacheable && !cont_data->cache_txn && !cont_data->head_only) { cacheNodeList(cont_data); } } } cont_data->curr_state = ContData::FETCHING_DATA; if (!input_vio_buf_null) { TSContCall(TSVIOContGet(cont_data->input_vio), TS_EVENT_VCONN_WRITE_COMPLETE, cont_data->input_vio); } } if ((cont_data->curr_state == ContData::FETCHING_DATA) && (!cont_data->option_info->first_byte_flush)) { // retest as state may have changed in previous block if (cont_data->data_fetcher->isFetchComplete()) { CONT_DATA_DBG(cont_data, "[%s] data ready; going to process doc", __FUNCTION__); const char *out_data; int out_data_len; EsiProcessor::ReturnCode retval = cont_data->esi_proc->process(out_data, out_data_len); CONT_DATA_DBG(cont_data, "[%s] data length: %d, retval: %d", __FUNCTION__, out_data_len, retval); if (retval == EsiProcessor::NEED_MORE_DATA) { CONT_DATA_DBG(cont_data, "[%s] ESI processor needs more data; " "will wait for all data to be fetched", __FUNCTION__); return 1; } cont_data->curr_state = ContData::PROCESSING_COMPLETE; if (retval == EsiProcessor::SUCCESS) { CONT_DATA_DBG(cont_data, "[%s] ESI processor output document of size %d starting with [%.10s]", __FUNCTION__, out_data_len, (out_data_len ? out_data : "(null)")); } else { TSError("[esi][%s] ESI processor failed to process document; will return empty document", __FUNCTION__); out_data = ""; out_data_len = 0; } // make sure transformation has not been prematurely terminated if (!cont_data->xform_closed) { string cdata; if (cont_data->gzip_output) { if (!gzip(out_data, out_data_len, cdata)) { TSError("[esi][%s] Error while gzipping content", __FUNCTION__); out_data_len = 0; out_data = ""; } else { CONT_DATA_DBG(cont_data, "[%s] Compressed document from size %d to %d bytes via gzip", __FUNCTION__, out_data_len, static_cast<int>(cdata.size())); out_data_len = cdata.size(); out_data = cdata.data(); } } // Get downstream VIO TSVConn output_conn; output_conn = TSTransformOutputVConnGet(contp); if (!output_conn) { TSError("[esi][%s] Error while getting transform VC", __FUNCTION__); return 0; } TSVIO output_vio; output_vio = TSVConnWrite(output_conn, contp, cont_data->output_reader, out_data_len); if (TSIOBufferWrite(TSVIOBufferGet(output_vio), out_data, out_data_len) == TS_ERROR) { TSError("[esi][%s] Error while writing bytes to downstream VC", __FUNCTION__); return 0; } TSVIONBytesSet(output_vio, out_data_len); // Reenable the output connection so it can read the data we've produced. TSVIOReenable(output_vio); } } else { CONT_DATA_DBG(cont_data, "[%s] Data not available yet; cannot process document", __FUNCTION__); } } if (((cont_data->curr_state == ContData::FETCHING_DATA) || (cont_data->curr_state == ContData::READING_ESI_DOC)) && (cont_data->option_info->first_byte_flush)) { // retest as state may have changed in previous block CONT_DATA_DBG(cont_data, "[%s] trying to process doc", __FUNCTION__); string out_data; string cdata; int overall_len; EsiProcessor::ReturnCode retval = cont_data->esi_proc->flush(out_data, overall_len); if ((cont_data->curr_state == ContData::FETCHING_DATA) && cont_data->data_fetcher->isFetchComplete()) { CONT_DATA_DBG(cont_data, "[%s] data ready; last process() will have finished the entire processing", __FUNCTION__); cont_data->curr_state = ContData::PROCESSING_COMPLETE; } if (retval == EsiProcessor::SUCCESS) { CONT_DATA_DBG(cont_data, "[%s] ESI processor output document of size %d starting with [%.10s]", __FUNCTION__, static_cast<int>(out_data.size()), (out_data.size() ? out_data.data() : "(null)")); } else { TSError("[esi][%s] ESI processor failed to process document; will return empty document", __FUNCTION__); out_data.assign(""); if (!cont_data->xform_closed) { TSVIONBytesSet(cont_data->output_vio, 0); TSVIOReenable(cont_data->output_vio); } } // make sure transformation has not been prematurely terminated if (!cont_data->xform_closed && out_data.size() > 0) { if (cont_data->gzip_output) { if (!cont_data->esi_gzip->stream_encode(out_data, cdata)) { TSError("[esi][%s] Error while gzipping content", __FUNCTION__); } else { CONT_DATA_DBG(cont_data, "[%s] Compressed document from size %d to %d bytes via EsiGzip", __FUNCTION__, static_cast<int>(out_data.size()), static_cast<int>(cdata.size())); } if (TSIOBufferWrite(TSVIOBufferGet(cont_data->output_vio), cdata.data(), cdata.size()) == TS_ERROR) { TSError("[esi][%s] Error while writing bytes to downstream VC", __FUNCTION__); return 0; } } else { if (TSIOBufferWrite(TSVIOBufferGet(cont_data->output_vio), out_data.data(), out_data.size()) == TS_ERROR) { TSError("[esi][%s] Error while writing bytes to downstream VC", __FUNCTION__); return 0; } } } if (!cont_data->xform_closed) { // should not set any fixed length if (cont_data->curr_state == ContData::PROCESSING_COMPLETE) { if (cont_data->gzip_output) { string cdata; int downstream_length; if (!cont_data->esi_gzip->stream_finish(cdata, downstream_length)) { TSError("[esi][%s] Error while finishing gzip", __FUNCTION__); return 0; } else { if (TSVIOBufferGet(cont_data->output_vio) == nullptr) { TSError("[esi][%s] Error while writing bytes to downstream VC", __FUNCTION__); return 0; } if (TSIOBufferWrite(TSVIOBufferGet(cont_data->output_vio), cdata.data(), cdata.size()) == TS_ERROR) { TSError("[esi][%s] Error while writing bytes to downstream VC", __FUNCTION__); return 0; } CONT_DATA_DBG(cont_data, "[%s] ESI processed overall/gzip: %d", __FUNCTION__, downstream_length); TSVIONBytesSet(cont_data->output_vio, downstream_length); } } else { CONT_DATA_DBG(cont_data, "[%s] ESI processed overall: %d", __FUNCTION__, overall_len); TSVIONBytesSet(cont_data->output_vio, overall_len); } } // Reenable the output connection so it can read the data we've produced. TSVIOReenable(cont_data->output_vio); } } return 1; } static int transformHandler(TSCont contp, TSEvent event, void *edata) { TSVIO input_vio; ContData *cont_data; cont_data = static_cast<ContData *>(TSContDataGet(contp)); // we need these later, but declaring now avoid compiler warning w.r.t. goto bool process_event = true; bool shutdown, is_fetch_event; if (!cont_data->initialized) { if (!cont_data->init()) { TSError("[esi][%s] Could not initialize continuation data; shutting down transformation", __FUNCTION__); goto lShutdown; } CONT_DATA_DBG(cont_data, "[%s] initialized continuation data", __FUNCTION__); } cont_data->checkXformStatus(); is_fetch_event = cont_data->data_fetcher->isFetchEvent(event); if (cont_data->xform_closed) { CONT_DATA_DBG(cont_data, "[%s] Transformation closed, post-processing", __FUNCTION__); if (cont_data->curr_state == ContData::PROCESSING_COMPLETE) { CONT_DATA_DBG(cont_data, "[%s] Processing is complete, not processing current event %d", __FUNCTION__, event); process_event = false; } else if (cont_data->curr_state == ContData::READING_ESI_DOC) { CONT_DATA_DBG(cont_data, "[%s] Parsing is incomplete, will force end of input", __FUNCTION__); cont_data->curr_state = ContData::FETCHING_DATA; } if (cont_data->curr_state == ContData::FETCHING_DATA) { // retest as it may be modified in prev. if block if (cont_data->data_fetcher->isFetchComplete()) { CONT_DATA_DBG(cont_data, "[%s] Requested data has been fetched; will skip event and marking processing as complete ", __FUNCTION__); cont_data->curr_state = ContData::PROCESSING_COMPLETE; process_event = false; } else { if (is_fetch_event) { CONT_DATA_DBG(cont_data, "[%s] Going to process received data", __FUNCTION__); } else { // transformation is over, but data hasn't been fetched; // let's wait for data to be fetched - we will be called // by Fetch API and go through this loop again CONT_DATA_DBG(cont_data, "[%s] Ignoring event %d; Will wait for pending data", __FUNCTION__, event); process_event = false; } } } } if (process_event) { switch (event) { case TS_EVENT_ERROR: // doubt: what is this code doing? input_vio = TSVConnWriteVIOGet(contp); if (!input_vio) { TSError("[esi][%s] Error while getting upstream vio", __FUNCTION__); } else { TSContCall(TSVIOContGet(input_vio), TS_EVENT_ERROR, input_vio); } // FetchSM also might send this; let's just output whatever we have cont_data->curr_state = ContData::FETCHING_DATA; transformData(contp); break; case TS_EVENT_VCONN_WRITE_READY: CONT_DATA_DBG(cont_data, "[%s] WRITE_READY", __FUNCTION__); if (!cont_data->option_info->first_byte_flush) { TSVConnShutdown(TSTransformOutputVConnGet(contp), 0, 1); } break; case TS_EVENT_VCONN_WRITE_COMPLETE: CONT_DATA_DBG(cont_data, "[%s] shutting down transformation", __FUNCTION__); TSVConnShutdown(TSTransformOutputVConnGet(contp), 0, 1); break; case TS_EVENT_IMMEDIATE: CONT_DATA_DBG(cont_data, "[%s] handling TS_EVENT_IMMEDIATE", __FUNCTION__); transformData(contp); break; default: if (is_fetch_event) { CONT_DATA_DBG(cont_data, "[%s] Handling fetch event %d", __FUNCTION__, event); if (cont_data->data_fetcher->handleFetchEvent(event, edata)) { if ((cont_data->curr_state == ContData::FETCHING_DATA) || (cont_data->curr_state == ContData::READING_ESI_DOC)) { // there's a small chance that fetcher is ready even before // parsing is complete; hence we need to check the state too if (cont_data->option_info->first_byte_flush || cont_data->data_fetcher->isFetchComplete()) { CONT_DATA_DBG(cont_data, "[%s] fetcher is ready with data, going into process stage", __FUNCTION__); transformData(contp); } } } else { TSError("[esi][%s] Could not handle fetch event!", __FUNCTION__); } } else { TSAssert(!"Unexpected event"); } break; } } CONT_DATA_DBG(cont_data, "[%s] transformHandler, event: %d, curr_state: %d", __FUNCTION__, static_cast<int>(event), static_cast<int>(cont_data->curr_state)); shutdown = (cont_data->xform_closed && (cont_data->curr_state == ContData::PROCESSING_COMPLETE)); if (shutdown) { if (process_event && is_fetch_event) { // we need to return control to the fetch API to give up it's // lock on our continuation which will fail if we destroy // ourselves right now CONT_DATA_DBG(cont_data, "[%s] Deferring shutdown as data event was just processed", __FUNCTION__); TSContScheduleOnPool(contp, 10, TS_THREAD_POOL_TASK); } else { goto lShutdown; } } return 1; lShutdown: CONT_DATA_DBG(cont_data, "[%s] transformation closed; cleaning up data", __FUNCTION__); delete cont_data; TSContDestroy(contp); return 1; } struct RespHdrModData { bool cache_txn; bool gzip_encoding; bool head_only; const OptionInfo *option_info; }; static void addMimeHeaderField(TSMBuffer bufp, TSMLoc hdr_loc, const char *name, int name_len, const char *value, int value_len) { TSMLoc field_loc = (TSMLoc) nullptr; TSMimeHdrFieldCreate(bufp, hdr_loc, &field_loc); if (!field_loc) { TSError("[esi][%s] Error while creating mime field", __FUNCTION__); } else { if (TSMimeHdrFieldNameSet(bufp, hdr_loc, field_loc, name, name_len) != TS_SUCCESS) { TSError("[esi][%s] Error while setting name [%.*s] for MIME header field", __FUNCTION__, name_len, name); } else { if (TSMimeHdrFieldValueStringInsert(bufp, hdr_loc, field_loc, 0, value, value_len) != TS_SUCCESS) { TSError("[esi][%s] Error while inserting value [%.*s] string to MIME field [%.*s]", __FUNCTION__, value_len, value, name_len, name); } else { if (TSMimeHdrFieldAppend(bufp, hdr_loc, field_loc) != TS_SUCCESS) { TSError("[esi][%s] Error while appending MIME field with name [%.*s] and value [%.*s]", __FUNCTION__, name_len, name, value_len, value); } } } TSHandleMLocRelease(bufp, hdr_loc, field_loc); } } static int modifyResponseHeader(TSCont contp, TSEvent event, void *edata) { int retval = 0; RespHdrModData *mod_data = static_cast<RespHdrModData *>(TSContDataGet(contp)); TSHttpTxn txnp = static_cast<TSHttpTxn>(edata); if (event != TS_EVENT_HTTP_SEND_RESPONSE_HDR) { TSError("[esi][%s] Unexpected event (%d)", __FUNCTION__, event); goto lReturn; } TSMBuffer bufp; TSMLoc hdr_loc; if (TSHttpTxnClientRespGet(txnp, &bufp, &hdr_loc) == TS_SUCCESS) { int n_mime_headers = TSMimeHdrFieldsCount(bufp, hdr_loc); TSMLoc field_loc; const char *name, *value; int name_len, value_len; for (int i = 0; i < n_mime_headers; ++i) { field_loc = TSMimeHdrFieldGet(bufp, hdr_loc, i); if (!field_loc) { Dbg(dbg_ctl_local, "[%s] Error while obtaining header field #%d", __FUNCTION__, i); continue; } name = TSMimeHdrFieldNameGet(bufp, hdr_loc, field_loc, &name_len); if (name) { bool destroy_header = false; if (Utils::areEqual(name, name_len, SERVER_INTERCEPT_HEADER, SERVER_INTERCEPT_HEADER_LEN)) { destroy_header = true; } else if (Utils::areEqual(name, name_len, TS_MIME_FIELD_AGE, TS_MIME_LEN_AGE)) { destroy_header = true; } else if (Utils::areEqual(name, name_len, MIME_FIELD_XESI, MIME_FIELD_XESI_LEN)) { destroy_header = true; } else if ((name_len > HEADER_MASK_PREFIX_SIZE) && (strncmp(name, HEADER_MASK_PREFIX, HEADER_MASK_PREFIX_SIZE) == 0)) { destroy_header = true; } else if (mod_data->option_info->private_response && (Utils::areEqual(name, name_len, TS_MIME_FIELD_CACHE_CONTROL, TS_MIME_LEN_CACHE_CONTROL) || Utils::areEqual(name, name_len, TS_MIME_FIELD_EXPIRES, TS_MIME_LEN_EXPIRES))) { destroy_header = true; } else if (Utils::areEqual(name, name_len, TS_MIME_FIELD_CONTENT_LENGTH, TS_MIME_LEN_CONTENT_LENGTH)) { if (mod_data->head_only) { destroy_header = true; Dbg(dbg_ctl_local, "[%s] remove Content-Length", __FUNCTION__); } } else { int n_field_values = TSMimeHdrFieldValuesCount(bufp, hdr_loc, field_loc); for (int j = 0; j < n_field_values; ++j) { value = TSMimeHdrFieldValueStringGet(bufp, hdr_loc, field_loc, j, &value_len); if (nullptr == value || !value_len) { Dbg(dbg_ctl_local, "[%s] Error while getting value #%d of header [%.*s]", __FUNCTION__, j, name_len, name); } else { if (!mod_data->option_info->packed_node_support || mod_data->cache_txn) { bool response_cacheable, is_cache_header; is_cache_header = checkForCacheHeader(name, name_len, value, value_len, response_cacheable); if (is_cache_header && response_cacheable) { destroy_header = true; } } } // if got valid value for header } // end for } if (destroy_header) { Dbg(dbg_ctl_local, "[%s] Removing header with name [%.*s]", __FUNCTION__, name_len, name); TSMimeHdrFieldDestroy(bufp, hdr_loc, field_loc); --n_mime_headers; --i; } } TSHandleMLocRelease(bufp, hdr_loc, field_loc); } if (mod_data->gzip_encoding && !checkHeaderValue(bufp, hdr_loc, TS_MIME_FIELD_CONTENT_ENCODING, TS_MIME_LEN_CONTENT_ENCODING, TS_HTTP_VALUE_GZIP, TS_HTTP_LEN_GZIP)) { addMimeHeaderField(bufp, hdr_loc, TS_MIME_FIELD_CONTENT_ENCODING, TS_MIME_LEN_CONTENT_ENCODING, TS_HTTP_VALUE_GZIP, TS_HTTP_LEN_GZIP); } if (mod_data->option_info->packed_node_support && mod_data->cache_txn) { addMimeHeaderField(bufp, hdr_loc, TS_MIME_FIELD_VARY, TS_MIME_LEN_VARY, TS_MIME_FIELD_ACCEPT_ENCODING, TS_MIME_LEN_ACCEPT_ENCODING); } if (mod_data->option_info->private_response) { addMimeHeaderField(bufp, hdr_loc, TS_MIME_FIELD_EXPIRES, TS_MIME_LEN_EXPIRES, HTTP_VALUE_PRIVATE_EXPIRES, sizeof(HTTP_VALUE_PRIVATE_EXPIRES) - 1); addMimeHeaderField(bufp, hdr_loc, TS_MIME_FIELD_CACHE_CONTROL, TS_MIME_LEN_CACHE_CONTROL, HTTP_VALUE_PRIVATE_CC, sizeof(HTTP_VALUE_PRIVATE_CC) - 1); } TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc); Dbg(dbg_ctl_local, "[%s] Inspected client-bound headers", __FUNCTION__); retval = 1; } else { TSError("[esi][%s] Error while getting response from txn", __FUNCTION__); } lReturn: delete mod_data; TSContDestroy(contp); TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); return retval; } static bool checkHeaderValue(TSMBuffer bufp, TSMLoc hdr_loc, const char *name, int name_len, const char *exp_value, int exp_value_len, bool prefix) { TSMLoc field_loc = TSMimeHdrFieldFind(bufp, hdr_loc, name, name_len); if (!field_loc) { return false; } bool retval = false; if (exp_value && exp_value_len) { const char *value; int value_len; int n_values = TSMimeHdrFieldValuesCount(bufp, hdr_loc, field_loc); for (int i = 0; i < n_values; ++i) { value = TSMimeHdrFieldValueStringGet(bufp, hdr_loc, field_loc, i, &value_len); if (nullptr != value && value_len) { if (prefix) { if ((value_len >= exp_value_len) && (strncasecmp(value, exp_value, exp_value_len) == 0)) { retval = true; } } else if (Utils::areEqual(value, value_len, exp_value, exp_value_len)) { retval = true; } } else { Dbg(dbg_ctl_local, "[%s] Error while getting value # %d of header [%.*s]", __FUNCTION__, i, name_len, name); } if (retval) { break; } } } else { // only presence required retval = true; } TSHandleMLocRelease(bufp, hdr_loc, field_loc); return retval; } static void maskOsCacheHeaders(TSHttpTxn txnp) { TSMBuffer bufp; TSMLoc hdr_loc; if (TSHttpTxnServerRespGet(txnp, &bufp, &hdr_loc) != TS_SUCCESS) { TSError("[esi][%s] Couldn't get server response from txn", __FUNCTION__); return; } int n_mime_headers = TSMimeHdrFieldsCount(bufp, hdr_loc); TSMLoc field_loc; const char *name, *value; int name_len, value_len, n_field_values; bool os_response_cacheable, mask_header; string masked_name; os_response_cacheable = true; for (int i = 0; i < n_mime_headers; ++i) { field_loc = TSMimeHdrFieldGet(bufp, hdr_loc, i); if (!field_loc) { Dbg(dbg_ctl_local, "[%s] Error while obtaining header field #%d", __FUNCTION__, i); continue; } name = TSMimeHdrFieldNameGet(bufp, hdr_loc, field_loc, &name_len); if (name) { mask_header = false; n_field_values = TSMimeHdrFieldValuesCount(bufp, hdr_loc, field_loc); for (int j = 0; j < n_field_values; ++j) { value = TSMimeHdrFieldValueStringGet(bufp, hdr_loc, field_loc, j, &value_len); if (nullptr == value || !value_len) { Dbg(dbg_ctl_local, "[%s] Error while getting value #%d of header [%.*s]", __FUNCTION__, j, name_len, name); } else { bool is_cache_header = checkForCacheHeader(name, name_len, value, value_len, os_response_cacheable); if (!os_response_cacheable) { break; } if (is_cache_header) { Dbg(dbg_ctl_local, "[%s] Masking OS cache header [%.*s] with value [%.*s]. ", __FUNCTION__, name_len, name, value_len, value); mask_header = true; } } // end if got value string } // end value iteration if (mask_header) { masked_name.assign(HEADER_MASK_PREFIX); masked_name.append(name, name_len); if (TSMimeHdrFieldNameSet(bufp, hdr_loc, field_loc, masked_name.data(), masked_name.size()) != TS_SUCCESS) { TSError("[esi][%s] Couldn't rename header [%.*s]", __FUNCTION__, name_len, name); } } } // end if got header name TSHandleMLocRelease(bufp, hdr_loc, field_loc); if (!os_response_cacheable) { break; } } // end header iteration TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc); } static bool isTxnTransformable(TSHttpTxn txnp, bool is_cache_txn, bool *intercept_header, bool *head_only) { // We are only interested in transforming "200 OK" responses with a // Content-Type: text/ header and with X-Esi header TSMBuffer bufp; TSMLoc hdr_loc; TSReturnCode header_obtained; bool retval = false; if (TSHttpTxnClientReqGet(txnp, &bufp, &hdr_loc) != TS_SUCCESS) { TSError("[esi][%s] Couldn't get txn header", __FUNCTION__); return false; } int method_len; const char *method; method = TSHttpHdrMethodGet(bufp, hdr_loc, &method_len); if (method == nullptr) { TSError("[esi][%s] Couldn't get method", __FUNCTION__); TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc); return false; } if (method_len >= TS_HTTP_LEN_HEAD && memcmp(method, TS_HTTP_METHOD_HEAD, TS_HTTP_LEN_HEAD) == 0) { *head_only = true; } else if (!(((method_len >= TS_HTTP_LEN_POST && memcmp(method, TS_HTTP_METHOD_POST, TS_HTTP_LEN_POST) == 0)) || ((method_len >= TS_HTTP_LEN_GET && memcmp(method, TS_HTTP_METHOD_GET, TS_HTTP_LEN_GET) == 0)))) { Dbg(dbg_ctl_local, "[%s] method %.*s will be ignored", __FUNCTION__, method_len, method); TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc); return false; } TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc); header_obtained = is_cache_txn ? TSHttpTxnCachedRespGet(txnp, &bufp, &hdr_loc) : TSHttpTxnServerRespGet(txnp, &bufp, &hdr_loc); if (header_obtained != TS_SUCCESS) { TSError("[esi][%s] Couldn't get txn header", __FUNCTION__); return false; } // if origin returns status 304, check cached response instead int response_status; if (is_cache_txn == false) { response_status = TSHttpHdrStatusGet(bufp, hdr_loc); if (response_status == TS_HTTP_STATUS_NOT_MODIFIED) { TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc); header_obtained = TSHttpTxnCachedRespGet(txnp, &bufp, &hdr_loc); if (header_obtained != TS_SUCCESS) { TSError("[esi][%s] Couldn't get txn cache response header", __FUNCTION__); return false; } } } do { *intercept_header = checkHeaderValue(bufp, hdr_loc, SERVER_INTERCEPT_HEADER, SERVER_INTERCEPT_HEADER_LEN); if (*intercept_header) { if (is_cache_txn) { Dbg(dbg_ctl_local, "[%s] Packed ESI document found in cache; will process", __FUNCTION__); retval = true; } else { Dbg(dbg_ctl_local, "[%s] Found Intercept header in server response; document not processable", __FUNCTION__); } break; // found internal header; no other detection required } if ((!checkHeaderValue(bufp, hdr_loc, TS_MIME_FIELD_CONTENT_TYPE, TS_MIME_LEN_CONTENT_TYPE, "text/", 5, true)) && (!checkHeaderValue(bufp, hdr_loc, TS_MIME_FIELD_CONTENT_TYPE, TS_MIME_LEN_CONTENT_TYPE, "application/javascript", 22, true)) && (!checkHeaderValue(bufp, hdr_loc, TS_MIME_FIELD_CONTENT_TYPE, TS_MIME_LEN_CONTENT_TYPE, "application/x-javascript", 24, true)) && (!checkHeaderValue(bufp, hdr_loc, TS_MIME_FIELD_CONTENT_TYPE, TS_MIME_LEN_CONTENT_TYPE, "application/json", 16, true)) && (!checkHeaderValue(bufp, hdr_loc, TS_MIME_FIELD_CONTENT_TYPE, TS_MIME_LEN_CONTENT_TYPE, "multipart/mixed", 15, true))) { Dbg(dbg_ctl_local, "[%s] Not text content", __FUNCTION__); break; } if (!checkHeaderValue(bufp, hdr_loc, MIME_FIELD_XESI, MIME_FIELD_XESI_LEN)) { Dbg(dbg_ctl_local, "[%s] ESI header [%s] not found", __FUNCTION__, MIME_FIELD_XESI); break; } retval = true; } while (false); TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc); return retval; } static bool isCacheObjTransformable(TSHttpTxn txnp, bool *intercept_header, bool *head_only) { int obj_status; if (TSHttpTxnCacheLookupStatusGet(txnp, &obj_status) == TS_ERROR) { TSError("[esi][%s] Couldn't get cache status of object", __FUNCTION__); return false; } if (obj_status == TS_CACHE_LOOKUP_HIT_FRESH) { Dbg(dbg_ctl_local, "[%s] doc found in cache, will add transformation", __FUNCTION__); return isTxnTransformable(txnp, true, intercept_header, head_only); } Dbg(dbg_ctl_local, "[%s] cache object's status is %d; not transformable", __FUNCTION__, obj_status); return false; } static bool isInterceptRequest(TSHttpTxn txnp) { if (!TSHttpTxnIsInternal(txnp)) { Dbg(dbg_ctl_local, "[%s] Skipping external request", __FUNCTION__); return false; } TSMBuffer bufp; TSMLoc hdr_loc; if (TSHttpTxnClientReqGet(txnp, &bufp, &hdr_loc) != TS_SUCCESS) { TSError("[esi][%s] Could not get client request", __FUNCTION__); return false; } bool valid_request = false; bool retval = false; int method_len; const char *method = TSHttpHdrMethodGet(bufp, hdr_loc, &method_len); if (!method) { TSError("[esi][%s] Could not obtain method!", __FUNCTION__); } else { if ((method_len != TS_HTTP_LEN_POST) || (strncasecmp(method, TS_HTTP_METHOD_POST, TS_HTTP_LEN_POST))) { Dbg(dbg_ctl_local, "[%s] Method [%.*s] invalid, [%s] expected", __FUNCTION__, method_len, method, TS_HTTP_METHOD_POST); } else { Dbg(dbg_ctl_local, "[%s] Valid server intercept method found", __FUNCTION__); valid_request = true; } } if (valid_request) { retval = checkHeaderValue(bufp, hdr_loc, SERVER_INTERCEPT_HEADER, SERVER_INTERCEPT_HEADER_LEN); } TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc); return retval; } static bool checkForCacheHeader(const char *name, int name_len, const char *value, int value_len, bool &cacheable) { cacheable = true; if (Utils::areEqual(name, name_len, TS_MIME_FIELD_EXPIRES, TS_MIME_LEN_EXPIRES)) { if ((value_len == 1) && (*value == '0')) { cacheable = false; } else if (Utils::areEqual(value, value_len, "-1", 2)) { cacheable = false; } return true; } if (Utils::areEqual(name, name_len, TS_MIME_FIELD_CACHE_CONTROL, TS_MIME_LEN_CACHE_CONTROL)) { if (Utils::areEqual(value, value_len, TS_HTTP_VALUE_PRIVATE, TS_HTTP_LEN_PRIVATE)) { cacheable = false; } return true; } return false; } static bool addSendResponseHeaderHook(TSHttpTxn txnp, const ContData *src_cont_data) { TSCont contp = TSContCreate(modifyResponseHeader, nullptr); if (!contp) { TSError("[esi][%s] Could not create continuation", __FUNCTION__); return false; } TSHttpTxnHookAdd(txnp, TS_HTTP_SEND_RESPONSE_HDR_HOOK, contp); RespHdrModData *cont_data = new RespHdrModData(); cont_data->option_info = src_cont_data->option_info; cont_data->cache_txn = src_cont_data->cache_txn; cont_data->head_only = src_cont_data->head_only; cont_data->gzip_encoding = src_cont_data->gzip_output; TSContDataSet(contp, cont_data); return true; } static bool addTransform(TSHttpTxn txnp, const bool processing_os_response, const bool intercept_header, const bool head_only, const OptionInfo *pOptionInfo) { TSCont contp = nullptr; ContData *cont_data = nullptr; contp = TSTransformCreate(transformHandler, txnp); if (!contp) { TSError("[esi][%s] Error while creating a new transformation", __FUNCTION__); goto lFail; } cont_data = new ContData(contp, txnp, pOptionInfo); TSContDataSet(contp, cont_data); cont_data->cache_txn = !processing_os_response; cont_data->intercept_header = intercept_header; cont_data->head_only = head_only; cont_data->getClientState(); cont_data->getServerState(); if (cont_data->cache_txn) { if (cont_data->option_info->packed_node_support) { if (cont_data->input_type != DATA_TYPE_PACKED_ESI) { removeCacheKey(txnp); } } else { if (cont_data->input_type == DATA_TYPE_PACKED_ESI) { removeCacheKey(txnp); } } } TSHttpTxnHookAdd(txnp, TS_HTTP_RESPONSE_TRANSFORM_HOOK, contp); if (!addSendResponseHeaderHook(txnp, cont_data)) { TSError("[esi][%s] Couldn't add send response header hook", __FUNCTION__); goto lFail; } TSHttpTxnTransformedRespCache(txnp, 0); if (cont_data->option_info->packed_node_support) { TSHttpTxnUntransformedRespCache(txnp, 0); } else { TSHttpTxnUntransformedRespCache(txnp, 1); } Dbg(dbg_ctl_local, "[%s] Added transformation (0x%p)", __FUNCTION__, contp); return true; lFail: if (contp) { TSContDestroy(contp); } if (cont_data) { delete cont_data; } return false; } static int globalHookHandler(TSCont contp, TSEvent event, void *edata) { TSHttpTxn txnp = static_cast<TSHttpTxn>(edata); bool intercept_header = false; bool head_only = false; bool intercept_req = isInterceptRequest(txnp); struct OptionInfo *pOptionInfo = static_cast<OptionInfo *>(TSContDataGet(contp)); switch (event) { case TS_EVENT_HTTP_READ_REQUEST_HDR: Dbg(dbg_ctl_local, "[%s] handling read request header event", __FUNCTION__); if (intercept_req) { if (!setupServerIntercept(txnp)) { TSError("[esi][%s] Could not setup server intercept", __FUNCTION__); } else { Dbg(dbg_ctl_local, "[%s] Setup server intercept", __FUNCTION__); } } else { Dbg(dbg_ctl_local, "[%s] Not setting up intercept", __FUNCTION__); } break; case TS_EVENT_HTTP_READ_RESPONSE_HDR: case TS_EVENT_HTTP_CACHE_LOOKUP_COMPLETE: if (!intercept_req) { if (event == TS_EVENT_HTTP_READ_RESPONSE_HDR) { bool mask_cache_headers = false; Dbg(dbg_ctl_local, "[%s] handling read response header event", __FUNCTION__); if (isTxnTransformable(txnp, false, &intercept_header, &head_only)) { addTransform(txnp, true, intercept_header, head_only, pOptionInfo); Stats::increment(Stats::N_OS_DOCS); mask_cache_headers = true; } if (pOptionInfo->packed_node_support && mask_cache_headers) { // we'll 'mask' OS cache headers so that traffic server will // not try to cache this. We cannot outright delete them // because we need them in our POST request; hence the 'masking' maskOsCacheHeaders(txnp); } } else { Dbg(dbg_ctl_local, "[%s] handling cache lookup complete event", __FUNCTION__); if (isCacheObjTransformable(txnp, &intercept_header, &head_only)) { // we make the assumption above that a transformable cache // object would already have a transformation. We should revisit // that assumption in case we change the statement below addTransform(txnp, false, intercept_header, head_only, pOptionInfo); Stats::increment(Stats::N_CACHE_DOCS); } } } break; default: Dbg(dbg_ctl_local, "[%s] Don't know how to handle event type %d", __FUNCTION__, event); break; } TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); return 0; } static void loadHandlerConf(const char *file_name, Utils::KeyValueMap &handler_conf) { std::list<string> conf_lines; TSFile conf_file = TSfopen(file_name, "r"); if (conf_file != nullptr) { char buf[1024]; while (TSfgets(conf_file, buf, sizeof(buf) - 1) != nullptr) { conf_lines.push_back(string(buf)); } TSfclose(conf_file); Utils::parseKeyValueConfig(conf_lines, handler_conf, gAllowlistCookies); Dbg(dbg_ctl_local, "[%s] Loaded handler conf file [%s]", __FUNCTION__, file_name); } else { TSError("[esi][%s] Failed to open handler config file [%s]", __FUNCTION__, file_name); } } static int esiPluginInit(int argc, const char *argv[], OptionInfo *pOptionInfo) { static TSStatSystem *statSystem = nullptr; if (statSystem == nullptr) { statSystem = new TSStatSystem(); Stats::init(statSystem); } if (gHandlerManager == nullptr) { gHandlerManager = new HandlerManager(); } new (pOptionInfo) OptionInfo; if (argc > 1) { int c; static const struct option longopts[] = { {const_cast<char *>("packed-node-support"), no_argument, nullptr, 'n'}, {const_cast<char *>("private-response"), no_argument, nullptr, 'p'}, {const_cast<char *>("disable-gzip-output"), no_argument, nullptr, 'z'}, {const_cast<char *>("first-byte-flush"), no_argument, nullptr, 'b'}, {const_cast<char *>("handler-filename"), required_argument, nullptr, 'f'}, {const_cast<char *>("max-doc-size"), required_argument, nullptr, 'd'}, {nullptr, 0, nullptr, 0 }, }; int longindex = 0; while ((c = getopt_long(argc, const_cast<char *const *>(argv), "npzbf:d:", longopts, &longindex)) != -1) { switch (c) { case 'n': pOptionInfo->packed_node_support = true; break; case 'p': pOptionInfo->private_response = true; break; case 'z': pOptionInfo->disable_gzip_output = true; break; case 'b': pOptionInfo->first_byte_flush = true; break; case 'f': { Utils::KeyValueMap handler_conf; loadHandlerConf(optarg, handler_conf); gHandlerManager->loadObjects(handler_conf); break; } case 'd': { unsigned max, coeff{1}; char multiplier, crap; auto num_assigned = std::sscanf(optarg, "%u%c%c", &max, &multiplier, &crap); if (2 == num_assigned) { if ('K' == multiplier) { coeff = 1024; num_assigned = 1; } else if ('M' == multiplier) { coeff = 1024 * 1024; num_assigned = 1; } } if (num_assigned != 1) { TSEmergency("[esi][%s] value for maximum document size (%s) has bad format", __FUNCTION__, optarg); } if ((coeff != 1) && (max > (std::numeric_limits<unsigned>::max() / coeff))) { TSEmergency("[esi][%s] specified maximum document size (%u%c) too large", __FUNCTION__, max, multiplier); } pOptionInfo->max_doc_size = max * coeff; break; } default: TSEmergency("[esi][%s] bad option", __FUNCTION__); return -1; } } } Dbg(dbg_ctl_local, "[%s] Plugin started, " "packed-node-support: %d, private-response: %d, disable-gzip-output: %d, first-byte-flush: %d, max-doc-size %u ", __FUNCTION__, pOptionInfo->packed_node_support, pOptionInfo->private_response, pOptionInfo->disable_gzip_output, pOptionInfo->first_byte_flush, pOptionInfo->max_doc_size); return 0; } void TSPluginInit(int argc, const char *argv[]) { TSPluginRegistrationInfo info; info.plugin_name = (char *)"esi"; info.vendor_name = (char *)"Apache Software Foundation"; info.support_email = (char *)"dev@trafficserver.apache.org"; if (TSPluginRegister(&info) != TS_SUCCESS) { TSError("[esi][%s] plugin registration failed", __FUNCTION__); return; } auto pOptionInfo = TSRalloc<OptionInfo>(); if (pOptionInfo == nullptr) { TSError("[esi][%s] malloc %d bytes fail", __FUNCTION__, static_cast<int>(sizeof(OptionInfo))); return; } if (esiPluginInit(argc, argv, pOptionInfo) != 0) { TSfree(pOptionInfo); return; } TSCont global_contp = TSContCreate(globalHookHandler, nullptr); if (!global_contp) { TSError("[esi][%s] Could not create global continuation", __FUNCTION__); TSfree(pOptionInfo); return; } TSContDataSet(global_contp, pOptionInfo); TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, global_contp); TSHttpHookAdd(TS_HTTP_READ_RESPONSE_HDR_HOOK, global_contp); TSHttpHookAdd(TS_HTTP_CACHE_LOOKUP_COMPLETE_HOOK, global_contp); } /////////////////////////////////////////////////////////////////////////////// // Initialize the plugin as a remap plugin. // TSReturnCode TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size) { if (!api_info) { snprintf(errbuf, errbuf_size, "[TSRemapInit] - Invalid TSRemapInterface argument"); TSError("[esi][TSRemapInit] - Invalid TSRemapInterface argument"); return TS_ERROR; } if (api_info->size < sizeof(TSRemapInterface)) { snprintf(errbuf, errbuf_size, "[TSRemapInit] - Incorrect size of TSRemapInterface structure"); TSError("[esi][TSRemapInit] - Incorrect size of TSRemapInterface structure"); return TS_ERROR; } Dbg(dbg_ctl_local, "esi remap plugin is successfully initialized"); return TS_SUCCESS; } TSReturnCode TSRemapNewInstance(int argc, char *argv[], void **ih, char *errbuf, int errbuf_size) { if (argc < 2) { snprintf(errbuf, errbuf_size, "Unable to create remap instance, " "argc: %d < 2", argc); TSError("[esi]Unable to create remap instance! argc: %d < 2", argc); return TS_ERROR; } int index = 0; const char *new_argv[argc]; new_argv[index++] = "esi.so"; for (int i = 2; i < argc; i++) { new_argv[index++] = argv[i]; } new_argv[index] = nullptr; OptionInfo *pOptionInfo = TSRalloc<OptionInfo>(); if (pOptionInfo == nullptr) { snprintf(errbuf, errbuf_size, "malloc %d bytes fail", static_cast<int>(sizeof(OptionInfo))); TSError("[esi][%s] malloc %d bytes fail", __FUNCTION__, static_cast<int>(sizeof(OptionInfo))); return TS_ERROR; } if (esiPluginInit(index, new_argv, pOptionInfo) != 0) { snprintf(errbuf, errbuf_size, "esiPluginInit fail!"); TSfree(pOptionInfo); return TS_ERROR; } TSCont contp = TSContCreate(globalHookHandler, nullptr); TSContDataSet(contp, pOptionInfo); *ih = static_cast<void *>(contp); return TS_SUCCESS; } void TSRemapDeleteInstance(void *ih) { TSCont contp = static_cast<TSCont>(ih); if (contp != nullptr) { TSContDestroy(contp); } } /////////////////////////////////////////////////////////////////////////////// // Main entry point when used as a remap plugin. // TSRemapStatus TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo * /* rri ATS_UNUSED */) { if (nullptr != ih) { TSCont contp = static_cast<TSCont>(ih); TSHttpTxnHookAdd(txnp, TS_HTTP_READ_RESPONSE_HDR_HOOK, contp); TSHttpTxnHookAdd(txnp, TS_HTTP_CACHE_LOOKUP_COMPLETE_HOOK, contp); if (isInterceptRequest(txnp)) { if (!setupServerIntercept(txnp)) { TSError("[esi][%s] Could not setup server intercept", __FUNCTION__); } else { Dbg(dbg_ctl_local, "[%s] Setup server intercept", __FUNCTION__); } } else { Dbg(dbg_ctl_local, "[%s] Not setting up intercept", __FUNCTION__); } } return TSREMAP_NO_REMAP; // This plugin never rewrites anything. }