| /** | |
| 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. | |
| */ | |
| /** | |
| * @file InterceptPlugin.cc | |
| */ | |
| #include "tscpp/api/InterceptPlugin.h" | |
| #include "ts/ts.h" | |
| #include "logging_internal.h" | |
| #include "tscpp/api/noncopyable.h" | |
| #include "utils_internal.h" | |
| #include <cstdlib> | |
| #include <cerrno> | |
| #ifndef INT64_MAX | |
| #define INT64_MAX (9223372036854775807LL) | |
| #endif | |
| using namespace atscppapi; | |
| using std::string; | |
| /** | |
| * @private | |
| */ | |
| struct InterceptPlugin::State { | |
| TSCont cont_; | |
| TSVConn net_vc_ = nullptr; | |
| struct IoHandle { | |
| TSVIO vio_ = nullptr; | |
| TSIOBuffer buffer_ = nullptr; | |
| TSIOBufferReader reader_ = nullptr; | |
| IoHandle() = default; | |
| ; | |
| ~IoHandle() | |
| { | |
| if (reader_) { | |
| TSIOBufferReaderFree(reader_); | |
| } | |
| if (buffer_) { | |
| TSIOBufferDestroy(buffer_); | |
| } | |
| }; | |
| }; | |
| IoHandle input_; | |
| IoHandle output_; | |
| /** the API doesn't recognize end of input; so we have to explicitly | |
| * figure out when to continue reading and when to stop */ | |
| TSHttpParser http_parser_; | |
| int expected_body_size_ = 0; | |
| int num_body_bytes_read_ = 0; | |
| bool hdr_parsed_ = false; | |
| TSMBuffer hdr_buf_ = nullptr; | |
| TSMLoc hdr_loc_ = nullptr; | |
| int num_bytes_written_ = 0; | |
| std::shared_ptr<Mutex> plugin_mutex_; | |
| InterceptPlugin *plugin_ = nullptr; | |
| Headers request_headers_; | |
| /** these two fields to be used by the continuation callback only */ | |
| TSEvent saved_event_ = TS_EVENT_NONE; | |
| void *saved_edata_ = nullptr; | |
| TSAction timeout_action_ = nullptr; | |
| bool plugin_io_done_ = false; | |
| State(TSCont cont, InterceptPlugin *plugin) : cont_(cont), plugin_(plugin) | |
| { | |
| plugin_mutex_ = plugin->getMutex(); | |
| http_parser_ = TSHttpParserCreate(); | |
| } | |
| ~State() | |
| { | |
| TSHttpParserDestroy(http_parser_); | |
| if (hdr_loc_) { | |
| TSHandleMLocRelease(hdr_buf_, TS_NULL_MLOC, hdr_loc_); | |
| } | |
| if (hdr_buf_) { | |
| TSMBufferDestroy(hdr_buf_); | |
| } | |
| } | |
| }; | |
| namespace | |
| { | |
| int handleEvents(TSCont cont, TSEvent event, void *edata); | |
| void destroyCont(InterceptPlugin::State *state); | |
| } // namespace | |
| InterceptPlugin::InterceptPlugin(Transaction &transaction, InterceptPlugin::Type type) : TransactionPlugin(transaction) | |
| { | |
| TSCont cont = TSContCreate(handleEvents, TSMutexCreate()); | |
| state_ = new State(cont, this); | |
| TSContDataSet(cont, state_); | |
| TSHttpTxn txn = static_cast<TSHttpTxn>(transaction.getAtsHandle()); | |
| if (type == SERVER_INTERCEPT) { | |
| TSHttpTxnServerIntercept(cont, txn); | |
| } else { | |
| TSHttpTxnIntercept(cont, txn); | |
| } | |
| } | |
| InterceptPlugin::~InterceptPlugin() | |
| { | |
| if (state_->cont_) { | |
| LOG_DEBUG("Relying on callback for cleanup"); | |
| state_->plugin_ = nullptr; // prevent callback from invoking plugin | |
| } else { // safe to cleanup | |
| LOG_DEBUG("Normal cleanup"); | |
| delete state_; | |
| } | |
| } | |
| bool | |
| InterceptPlugin::produce(const void *data, int data_size) | |
| { | |
| std::lock_guard<Mutex> lock(*getMutex()); | |
| if (!state_->net_vc_) { | |
| LOG_ERROR("Intercept not operational"); | |
| return false; | |
| } | |
| if (!state_->output_.buffer_) { | |
| state_->output_.buffer_ = TSIOBufferCreate(); | |
| state_->output_.reader_ = TSIOBufferReaderAlloc(state_->output_.buffer_); | |
| state_->output_.vio_ = TSVConnWrite(state_->net_vc_, state_->cont_, state_->output_.reader_, INT64_MAX); | |
| } | |
| int num_bytes_written = TSIOBufferWrite(state_->output_.buffer_, data, data_size); | |
| if (num_bytes_written != data_size) { | |
| LOG_ERROR("Error while writing to buffer! Attempted %d bytes but only wrote %d bytes", data_size, num_bytes_written); | |
| return false; | |
| } | |
| TSVIOReenable(state_->output_.vio_); | |
| state_->num_bytes_written_ += data_size; | |
| LOG_DEBUG("Wrote %d bytes in response", data_size); | |
| return true; | |
| } | |
| bool | |
| InterceptPlugin::setOutputComplete() | |
| { | |
| std::lock_guard<Mutex> scopedLock(*getMutex()); | |
| if (!state_->net_vc_) { | |
| LOG_ERROR("Intercept not operational"); | |
| return false; | |
| } | |
| if (!state_->output_.buffer_) { | |
| LOG_ERROR("No output produced so far"); | |
| return false; | |
| } | |
| TSVIONBytesSet(state_->output_.vio_, state_->num_bytes_written_); | |
| TSVIOReenable(state_->output_.vio_); | |
| state_->plugin_io_done_ = true; | |
| LOG_DEBUG("Response complete"); | |
| return true; | |
| } | |
| Headers & | |
| InterceptPlugin::getRequestHeaders() | |
| { | |
| return state_->request_headers_; | |
| } | |
| TSSslConnection | |
| InterceptPlugin::getSslConnection() | |
| { | |
| if (!state_->net_vc_) { | |
| LOG_ERROR("Intercept Plugin is not ready to provide SSL Connection"); | |
| return nullptr; | |
| } | |
| return TSVConnSslConnectionGet(state_->net_vc_); | |
| } | |
| bool | |
| InterceptPlugin::doRead() | |
| { | |
| int avail = TSIOBufferReaderAvail(state_->input_.reader_); | |
| if (avail == TS_ERROR) { | |
| LOG_ERROR("Error while getting number of bytes available"); | |
| return false; | |
| } | |
| int consumed = 0; // consumed is used to update the input buffers | |
| if (avail > 0) { | |
| int64_t num_body_bytes_in_block; | |
| int64_t data_len; // size of all data (header + body) in a block | |
| const char *data, *startptr; | |
| TSIOBufferBlock block = TSIOBufferReaderStart(state_->input_.reader_); | |
| while (block != nullptr) { | |
| startptr = data = TSIOBufferBlockReadStart(block, state_->input_.reader_, &data_len); | |
| num_body_bytes_in_block = 0; | |
| if (!state_->hdr_parsed_) { | |
| const char *endptr = data + data_len; | |
| if (TSHttpHdrParseReq(state_->http_parser_, state_->hdr_buf_, state_->hdr_loc_, &data, endptr) == TS_PARSE_DONE) { | |
| LOG_DEBUG("Parsed header"); | |
| string content_length_str = state_->request_headers_.value("Content-Length"); | |
| if (!content_length_str.empty()) { | |
| const char *start_ptr = content_length_str.data(); | |
| char *end_ptr; | |
| int content_length = strtol(start_ptr, &end_ptr, 10 /* base */); | |
| if ((errno != ERANGE) && (end_ptr != start_ptr) && (*end_ptr == '\0')) { | |
| LOG_DEBUG("Got content length: %d", content_length); | |
| state_->expected_body_size_ = content_length; | |
| } else { | |
| LOG_ERROR("Invalid content length header [%s]; Assuming no content", content_length_str.c_str()); | |
| } | |
| } | |
| if (state_->request_headers_.value("Transfer-Encoding") == "chunked") { | |
| // implementing a "dechunker" is non-trivial and in the real | |
| // world, most browsers don't send chunked requests | |
| LOG_ERROR("Support for chunked request not implemented! Assuming no body"); | |
| } | |
| LOG_DEBUG("Expecting %d bytes of request body", state_->expected_body_size_); | |
| state_->hdr_parsed_ = true; | |
| // remaining data in this block is body; 'data' will be pointing to first byte of the body | |
| num_body_bytes_in_block = endptr - data; | |
| } | |
| consume(string(startptr, data - startptr), InterceptPlugin::REQUEST_HEADER); | |
| } else { | |
| num_body_bytes_in_block = data_len; | |
| } | |
| if (num_body_bytes_in_block) { | |
| state_->num_body_bytes_read_ += num_body_bytes_in_block; | |
| consume(string(data, num_body_bytes_in_block), InterceptPlugin::REQUEST_BODY); | |
| } | |
| consumed += data_len; | |
| block = TSIOBufferBlockNext(block); | |
| } | |
| } | |
| LOG_DEBUG("Consumed %d bytes from input vio", consumed); | |
| TSIOBufferReaderConsume(state_->input_.reader_, consumed); | |
| // Modify the input VIO to reflect how much data we've completed. | |
| TSVIONDoneSet(state_->input_.vio_, TSVIONDoneGet(state_->input_.vio_) + consumed); | |
| if (isWebsocket()) { | |
| TSVIOReenable(state_->input_.vio_); | |
| return true; | |
| } | |
| if ((state_->hdr_parsed_) && (state_->num_body_bytes_read_ >= state_->expected_body_size_)) { | |
| LOG_DEBUG("Completely read body"); | |
| if (state_->num_body_bytes_read_ > state_->expected_body_size_) { | |
| LOG_ERROR("Read more data than specified in request"); | |
| // TODO: any further action required? | |
| } | |
| handleInputComplete(); | |
| } else { | |
| LOG_DEBUG("Reenabling input vio as %d bytes still need to be read", state_->expected_body_size_ - state_->num_body_bytes_read_); | |
| TSVIOReenable(state_->input_.vio_); | |
| } | |
| return true; | |
| } | |
| void | |
| InterceptPlugin::handleEvent(int abstract_event, void *edata) | |
| { | |
| TSEvent event = static_cast<TSEvent>(abstract_event); | |
| LOG_DEBUG("Received event %d", event); | |
| switch (event) { | |
| case TS_EVENT_NET_ACCEPT: | |
| LOG_DEBUG("Handling net accept"); | |
| state_->net_vc_ = static_cast<TSVConn>(edata); | |
| state_->input_.buffer_ = TSIOBufferCreate(); | |
| state_->input_.reader_ = TSIOBufferReaderAlloc(state_->input_.buffer_); | |
| state_->input_.vio_ = TSVConnRead(state_->net_vc_, state_->cont_, state_->input_.buffer_, | |
| INT64_MAX /* number of bytes to read - high value initially */); | |
| state_->hdr_buf_ = TSMBufferCreate(); | |
| state_->hdr_loc_ = TSHttpHdrCreate(state_->hdr_buf_); | |
| state_->request_headers_.reset(state_->hdr_buf_, state_->hdr_loc_); | |
| TSHttpHdrTypeSet(state_->hdr_buf_, state_->hdr_loc_, TS_HTTP_TYPE_REQUEST); | |
| break; | |
| case TS_EVENT_VCONN_WRITE_READY: // nothing to do | |
| LOG_DEBUG("Got write ready"); | |
| break; | |
| case TS_EVENT_VCONN_READ_READY: | |
| LOG_DEBUG("Handling read ready"); | |
| if (doRead()) { | |
| break; | |
| } | |
| // else fall through into the next shut down cases | |
| LOG_ERROR("Error while reading request!"); | |
| // fallthrough | |
| case TS_EVENT_VCONN_READ_COMPLETE: | |
| case TS_EVENT_VCONN_WRITE_COMPLETE: | |
| case TS_EVENT_VCONN_EOS: | |
| case TS_EVENT_ERROR: // erroring out, nothing more to do | |
| case TS_EVENT_NET_ACCEPT_FAILED: // somebody canceled the transaction | |
| if (event == TS_EVENT_ERROR) { | |
| LOG_ERROR("Unknown Error!"); | |
| } else if (event == TS_EVENT_NET_ACCEPT_FAILED) { | |
| LOG_ERROR("Got net_accept_failed!"); | |
| } | |
| LOG_DEBUG("Shutting down intercept"); | |
| destroyCont(state_); | |
| break; | |
| default: | |
| LOG_ERROR("Unknown event %d", event); | |
| } | |
| } | |
| namespace | |
| { | |
| /** RAII try-lock helper used by the @c InterceptPlugin continuation. | |
| * | |
| * The guard takes shared ownership of the mutex for its entire scope. The | |
| * locked region below may destroy the @c InterceptPlugin::State that owns | |
| * the only other @c std::shared_ptr to the mutex. | |
| */ | |
| class TryLockGuard | |
| { | |
| public: | |
| TryLockGuard(std::shared_ptr<Mutex> m) : _m(std::move(m)), _isLocked(_m && _m->try_lock()) {} | |
| TryLockGuard(const TryLockGuard &) = delete; | |
| TryLockGuard &operator=(const TryLockGuard &) = delete; | |
| TryLockGuard(TryLockGuard &&) = delete; | |
| TryLockGuard &operator=(TryLockGuard &&) = delete; | |
| bool | |
| isLocked() const | |
| { | |
| return _isLocked; | |
| } | |
| ~TryLockGuard() | |
| { | |
| if (_isLocked) { | |
| _m->unlock(); | |
| } | |
| } | |
| private: | |
| std::shared_ptr<Mutex> _m; ///< Shared ownership of the protected mutex. | |
| const bool _isLocked; ///< Whether @c _m->try_lock() succeeded. | |
| }; | |
| int | |
| handleEvents(TSCont cont, TSEvent pristine_event, void *pristine_edata) | |
| { | |
| // Separating pristine and mutable data helps debugging | |
| TSEvent event = pristine_event; | |
| void *edata = pristine_edata; | |
| InterceptPlugin::State *state = static_cast<InterceptPlugin::State *>(TSContDataGet(cont)); | |
| if (!state) { // plugin is done, return. | |
| return 0; | |
| } | |
| TryLockGuard scopedTryLock(state->plugin_mutex_); | |
| if (!scopedTryLock.isLocked()) { | |
| LOG_ERROR("Couldn't get plugin lock. Will retry"); | |
| if (event != TS_EVENT_TIMEOUT) { // save only "non-retry" info | |
| state->saved_event_ = event; | |
| state->saved_edata_ = edata; | |
| } | |
| state->timeout_action_ = TSContScheduleOnPool(cont, 1, TS_THREAD_POOL_NET); | |
| return 0; | |
| } | |
| if (event == TS_EVENT_TIMEOUT) { // we have a saved event to restore | |
| state->timeout_action_ = nullptr; | |
| if (state->plugin_io_done_) { // plugin is done, so can't send it saved event | |
| event = TS_EVENT_VCONN_EOS; // fake completion | |
| edata = nullptr; | |
| } else { | |
| event = state->saved_event_; | |
| edata = state->saved_edata_; | |
| } | |
| } | |
| if (state->plugin_) { | |
| utils::internal::dispatchInterceptEvent(state->plugin_, event, edata); | |
| } else { // plugin was destroyed before intercept was completed; cleaning up here | |
| LOG_DEBUG("Cleaning up as intercept plugin is already destroyed"); | |
| destroyCont(state); | |
| TSContDataSet(cont, nullptr); | |
| delete state; | |
| } | |
| return 0; | |
| } | |
| void | |
| destroyCont(InterceptPlugin::State *state) | |
| { | |
| if (state->net_vc_) { | |
| TSVConnShutdown(state->net_vc_, 1, 1); | |
| TSVConnClose(state->net_vc_); | |
| state->net_vc_ = nullptr; | |
| } | |
| if (state->cont_) { | |
| if (state->timeout_action_) { | |
| TSActionCancel(state->timeout_action_); | |
| state->timeout_action_ = nullptr; | |
| } | |
| TSContDestroy(state->cont_); | |
| state->cont_ = nullptr; | |
| } | |
| } | |
| } // namespace |