| /** |
| @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. |
| |
| Copyright 2019, Oath Inc. |
| */ |
| |
| #include <swoc/MemSpan.h> |
| #include <swoc/ArenaWriter.h> |
| |
| #include "txn_box/Context.h" |
| #include "txn_box/Config.h" |
| #include "txn_box/Expr.h" |
| #include "txn_box/ts_util.h" |
| |
| using swoc::ArenaWriter; |
| using swoc::BufferWriter; |
| using swoc::Errata; |
| using swoc::FixedBufferWriter; |
| using swoc::MemSpan; |
| using swoc::TextView; |
| using namespace swoc::literals; |
| |
| /* ------------------------------------------------------------------------------------ */ |
| bool |
| Expr::bwf_ex::operator()(std::string_view &literal, Extractor::Spec &spec) |
| { |
| bool zret = false; |
| if (_iter->_type == swoc::bwf::Spec::LITERAL_TYPE) { |
| literal = _iter->_ext; |
| if (++_iter == _specs.end()) { // all done! |
| return zret; |
| } |
| } |
| if (_iter->_type != swoc::bwf::Spec::LITERAL_TYPE) { |
| spec = *_iter; |
| ++_iter; |
| zret = true; |
| } |
| return zret; |
| } |
| /* ------------------------------------------------------------------------------------ */ |
| |
| Context::Context(std::shared_ptr<Config> const &cfg) : _cfg(cfg) |
| { |
| size_t reserved_size = G._remap_ctx_storage_required + (cfg ? cfg->reserved_ctx_storage_size() : 0); |
| // This is arranged so @a _arena destructor will clean up properly, nothing more need be done. |
| _arena.reset(swoc::MemArena::construct_self_contained(4000 + reserved_size)); |
| |
| _rxp_ctx = pcre2_general_context_create( |
| [](PCRE2_SIZE size, void *ctx) -> void * { return static_cast<self_type *>(ctx)->_arena->alloc(size).data(); }, |
| [](void *, void *) -> void {}, this); |
| if (cfg) { |
| /// Make sure there are sufficient capture groups. |
| this->rxp_match_require(cfg->_capture_groups); |
| } |
| |
| if (reserved_size) { |
| // Directive shared storage |
| _ctx_store = _arena->alloc(reserved_size); |
| memset(_ctx_store, 0); // Zero initialize it. |
| } |
| } |
| |
| Context::~Context() |
| { |
| // Invoke all the finalizers to do additional cleanup. |
| for (auto &&f : _finalizers) { |
| f._f(f._ptr); |
| std::destroy_at(&f._f); // clean up the cleaner too, just in case. |
| } |
| } |
| |
| swoc::Errata |
| Context::Callback::invoke(Context &ctx) |
| { |
| return _drtv->invoke(ctx); |
| } |
| |
| Errata |
| Context::on_hook_do(Hook hook_idx, Directive *drtv) |
| { |
| auto &info{_hooks[IndexFor(hook_idx)]}; |
| if (!info.hook_set_p) { // no hook to invoke this directive, set one up. |
| if (hook_idx >= _cur_hook) { |
| TSHttpTxnHookAdd(_txn, TS_Hook[IndexFor(hook_idx)], _cont); |
| info.hook_set_p = true; |
| } else if (hook_idx < _cur_hook) { |
| // error condition - should report. Also, should detect this on config load. |
| } |
| } |
| info.cb_list.append(_arena->make<Callback>(drtv)); |
| return {}; |
| } |
| |
| Errata |
| Context::invoke_callbacks() |
| { |
| // Bit of subtlety here - directives / callbacks can be added to the list due to the action of the |
| // invoked directive from this list. However, because this is an intrusive list and items are only |
| // added to the end, the @c next pointer for the current item will be updated before the loop |
| // iteration occurs and therefore new directives will be invoked. |
| auto &info{_hooks[IndexFor(_cur_hook)]}; |
| for (auto &cb : info.cb_list) { |
| _terminal_p = false; // reset before each scheduled callback. |
| cb.invoke(*this); |
| } |
| return {}; |
| } |
| |
| Errata |
| Context::invoke_for_hook(Hook hook) |
| { |
| _cur_hook = hook; |
| this->clear_cache(); |
| |
| // Run the top level directives in the config first. |
| if (_cfg) { |
| for (auto const &handle : _cfg->hook_directives(hook)) { |
| _terminal_p = false; // reset before each top level invocation. |
| handle->invoke(*this); // need to log errors here. |
| } |
| } |
| this->invoke_callbacks(); |
| |
| _cur_hook = Hook::INVALID; |
| |
| return {}; |
| } |
| |
| Errata |
| Context::invoke_for_remap(Config &rule_cfg, TSRemapRequestInfo *rri) |
| { |
| _cur_hook = Hook::REMAP; |
| _remap_info = rri; |
| this->clear_cache(); |
| this->rxp_match_require(rule_cfg._capture_groups); |
| |
| // What about directive storage? |
| |
| // Remap rule directives. |
| _terminal_p = false; // reset before each top level invocation. |
| for (auto const &handle : rule_cfg.hook_directives(_cur_hook)) { |
| handle->invoke(*this); // need to log errors here. |
| if (_terminal_p) { |
| break; |
| } |
| } |
| // Now the global config directives for REMAP |
| if (_cfg) { |
| for (auto const &handle : _cfg->hook_directives(_cur_hook)) { |
| _terminal_p = false; // reset before each top level invocation. |
| handle->invoke(*this); // need to log errors here. |
| } |
| } |
| this->invoke_callbacks(); // Any accumulated callbacks. |
| |
| // Revert from remap style invocation. |
| _cur_hook = Hook::INVALID; |
| _remap_info = nullptr; |
| |
| return {}; |
| } |
| |
| void |
| Context::operator()(swoc::BufferWriter &w, Extractor::Spec const &spec) |
| { |
| spec._exf->format(w, spec, *this); |
| } |
| |
| Feature |
| Expr::bwf_visitor::operator()(const Composite &comp) |
| { |
| return _ctx.render_transient([&](BufferWriter &w) { w.print_nfv(_ctx, bwf_ex{comp._specs}, Context::ArgPack(_ctx)); }); |
| } |
| |
| Feature |
| Expr::bwf_visitor::operator()(List const &list) |
| { |
| feature_type_for<TUPLE> expr_tuple = _ctx.alloc_span<Feature>(list._exprs.size()); |
| unsigned idx = 0; |
| for (auto const &expr : list._exprs) { |
| Feature feature{_ctx.extract(expr)}; |
| _ctx.commit(feature); |
| expr_tuple[idx++] = feature; |
| } |
| return expr_tuple; |
| } |
| |
| Feature |
| Context::extract(Expr const &expr) |
| { |
| auto value = std::visit(Expr::bwf_visitor(*this), expr._raw); |
| for (auto const &mod : expr._mods) { |
| value = (*mod)(*this, value); |
| } |
| return value; |
| } |
| |
| FeatureView |
| Context::extract_view(const Expr &expr, std::initializer_list<ViewOption> opts) |
| { |
| FeatureView zret{}; |
| |
| bool commit_p = false; |
| bool cstr_p = false; |
| for (auto opt : opts) { |
| switch (opt) { |
| case EX_COMMIT: |
| commit_p = true; |
| break; |
| case EX_C_STR: |
| cstr_p = true; |
| break; |
| } |
| } |
| |
| auto f = this->extract(expr); |
| if (IndexFor(STRING) == f.index()) { |
| auto view = std::get<IndexFor(STRING)>(f); |
| if (cstr_p && !view._cstr_p) { |
| if (!view._literal_p && !view._direct_p) { // in temporary memory |
| // If there's room, just add the null terminator. |
| if (auto span = _arena->remnant().rebind<char>(); span.data() == view.data_end()) { |
| _arena->alloc(1); |
| span[0] = '\0'; |
| view._cstr_p = true; |
| } else { |
| _arena->alloc(view.size()); // commit the view data and copy. |
| view._literal_p = true; |
| } |
| } |
| // if it's still in fixed memory, need to copy and terminate. |
| if (view._literal_p) { |
| auto span = _arena->require(view.size() + 1).remnant().rebind<char>(); |
| memcpy(span, view); |
| span[view.size()] = '\0'; |
| view = TextView(span); |
| view.remove_suffix(1); // drop null from view. |
| view._cstr_p = true; |
| view._literal_p = false; |
| } |
| } |
| zret = view; |
| } else { |
| ArenaWriter w{*_arena}; |
| if (cstr_p) { |
| w.print("{}\0", f); |
| zret = TextView{w.view()}.remove_suffix(1); |
| zret._cstr_p = true; |
| } else { |
| w.print("{}", f); |
| zret = w.view(); |
| } |
| } |
| if (commit_p && !zret._literal_p && !zret._direct_p) { |
| _arena->alloc(zret.size() + (zret._cstr_p ? 1 : 0)); |
| zret._literal_p = true; |
| } |
| return zret; |
| } |
| |
| FeatureView |
| Context::commit(FeatureView const &view) |
| { |
| FeatureView zret{view}; |
| if (view._literal_p) { // already committed. |
| } else if (view._direct_p) { |
| auto span{_arena->alloc(view.size())}; |
| memcpy(span, view); |
| zret = TextView(span.rebind<char>()); // update full to be the localized copy. |
| zret._direct_p = false; |
| zret._literal_p = true; |
| } else if (auto r{_arena->remnant()}; r.contains(view.data())) { |
| // it's in transient memory, finalize it. |
| // Need to commit everything before it as well. |
| size_t n = (view.data() - r.rebind<char>().data()) + view.size(); |
| _arena->alloc(n); |
| _transient = (n >= _transient ? 0 : _transient - n); |
| zret._literal_p = true; |
| } else if (_arena->contains(view.data())) { // it's already been committed, mark it so. |
| zret._literal_p = true; |
| } |
| return zret; |
| } |
| |
| Feature & |
| Context::commit(Feature &feature) |
| { |
| if (auto fv = std::get_if<STRING>(&feature); fv != nullptr) { |
| feature = this->commit(*fv); |
| } |
| return feature; |
| } |
| |
| ts::HttpRequest |
| Context::ua_req_hdr() |
| { |
| if (!_ua_req.is_valid()) { |
| _ua_req = _txn.ua_req_hdr(); |
| } |
| return _ua_req; |
| } |
| |
| ts::HttpRequest |
| Context::proxy_req_hdr() |
| { |
| if (!_proxy_req.is_valid()) { |
| _proxy_req = _txn.preq_hdr(); |
| } |
| return _proxy_req; |
| } |
| |
| ts::HttpResponse |
| Context::upstream_rsp_hdr() |
| { |
| if (!_upstream_rsp.is_valid()) { |
| _upstream_rsp = _txn.ursp_hdr(); |
| } |
| return _upstream_rsp; |
| } |
| |
| ts::HttpResponse |
| Context::proxy_rsp_hdr() |
| { |
| if (!_proxy_rsp.is_valid()) { |
| _proxy_rsp = _txn.prsp_hdr(); |
| } |
| return _proxy_rsp; |
| } |
| |
| Context::self_type & |
| Context::enable_hooks(TSHttpTxn txn) |
| { |
| // Create a continuation to hold the data. |
| _cont = TSContCreate(ts_callback, TSContMutexGet(reinterpret_cast<TSCont>(txn))); |
| TSContDataSet(_cont, this); |
| _txn = txn; |
| |
| // set hooks for top level directives. |
| if (_cfg) { |
| for (unsigned idx = 0; idx < std::tuple_size<Hook>::value; ++idx) { |
| auto const &drtv_list{_cfg->hook_directives(static_cast<Hook>(idx))}; |
| if (!drtv_list.empty()) { |
| TSHttpTxnHookAdd(txn, TS_Hook[idx], _cont); |
| _hooks[idx].hook_set_p = true; |
| } |
| } |
| } |
| |
| // Always set a cleanup hook. |
| TSHttpTxnHookAdd(txn, TS_HTTP_TXN_CLOSE_HOOK, _cont); |
| _txn.arg_assign(G.TxnArgIdx, this); |
| return *this; |
| } |
| |
| int |
| Context::ts_callback(TSCont cont, TSEvent evt, void *) |
| { |
| self_type *self = static_cast<self_type *>(TSContDataGet(cont)); |
| auto txn = self->_txn; // cache for TXN_CLOSE. |
| self->_global_status = TS_EVENT_HTTP_CONTINUE; |
| |
| // Run the directives. |
| Hook hook{Convert_TS_Event_To_TxB_Hook(evt)}; |
| if (Hook::INVALID != hook) { |
| self->invoke_for_hook(hook); |
| } |
| |
| /// TXN Close is special - do internal cleanup after explicit directives are done. |
| TSEvent saved_status = self->_global_status; // avoid possible use after free. |
| if (TS_EVENT_HTTP_TXN_CLOSE == evt) { |
| TSContDataSet(cont, nullptr); |
| TSContDestroy(cont); |
| delete self; |
| } |
| |
| TSHttpTxnReenable(txn, saved_status); |
| return TS_SUCCESS; |
| } |
| |
| Context & |
| Context::rxp_match_require(unsigned n) |
| { |
| if (_rxp_n < n) { |
| // Bump up at least 7, or 50%, or at least @a n. |
| n = std::max(_rxp_n + 7, n); |
| n = std::max((3 * _rxp_n) / 2, n); |
| _rxp_working = pcre2_match_data_create(n, _rxp_ctx); |
| _rxp_active = pcre2_match_data_create(n, _rxp_ctx); |
| _rxp_n = n; |
| } |
| return *this; |
| } |
| |
| void |
| Context::set_literal_capture(swoc::TextView text) |
| { |
| auto ovector = pcre2_get_ovector_pointer(_rxp_active); |
| ovector[0] = 0; |
| ovector[1] = text.size() - 1; |
| _rxp_src = text; |
| } |
| |
| pcre2_match_data * |
| Context::rxp_commit_match(swoc::TextView const &src) |
| { |
| _rxp_src = src; |
| std::swap(_rxp_active, _rxp_working); |
| return _rxp_active; |
| } |
| |
| Feature const & |
| Context::load_txn_var(swoc::TextView const &name) |
| { |
| auto spot = _txn_vars.find(name); |
| if (spot == _txn_vars.end()) { |
| // Later, need to search inbound_ssn and global variables and retrieve those if found. |
| return NIL_FEATURE; |
| } |
| return spot->_value; |
| } |
| |
| Context::self_type & |
| Context::store_txn_var(swoc::TextView const &name, Feature &value) |
| { |
| auto spot = _txn_vars.find(name); |
| this->commit(value); |
| if (spot == _txn_vars.end()) { |
| _txn_vars.insert(_arena->make<TxnVar>(name, value)); |
| } else { |
| spot->_value = value; |
| } |
| return *this; |
| } |
| |
| TextView |
| Context::localize_as_c_str(swoc::TextView text) |
| { |
| // If it's empty or isn't already a C string, make a copy that is. |
| if (text.empty() || '\0' != text.back()) { |
| auto span = _arena->alloc_span<char>(text.size() + 1); |
| memcpy(span, text); |
| span[text.size()] = '\0'; |
| text = span; |
| } |
| return text; |
| } |
| |
| MemSpan<void> |
| Context::overflow_storage_for(const ReservedSpan &span) |
| { |
| using T = decltype(_overflow_spans)::value_type; |
| // Is this offset already in the list? |
| auto spot = |
| std::find_if(_overflow_spans.begin(), _overflow_spans.end(), [&](auto const &item) { return item._offset == span.offset; }); |
| if (spot != _overflow_spans.end()) { |
| return spot->_storage; |
| } |
| // Make a record for the list to find later. |
| auto item = _arena->alloc_span<T>(1).data(); |
| new (item) T; |
| item->_offset = span.offset; |
| _overflow_spans.append(item); |
| // Get the actual reserved memory block, along with the required status block in front of it. |
| item->_storage = _arena->alloc(span.n + sizeof(ReservedStatus), alignof(ReservedStatus)); |
| memset(item->_storage, 0); |
| item->_storage.remove_prefix(sizeof(ReservedStatus)); |
| |
| return item->_storage; |
| } |
| |
| swoc::MemSpan<char> |
| Context::transient_buffer(size_t required) |
| { |
| this->commit_transient(); |
| auto span{_arena->require(required).remnant().rebind<char>()}; |
| _transient = TRANSIENT_ACTIVE; |
| return span; |
| } |
| |
| Context::self_type & |
| Context::transient_require(size_t n) |
| { |
| this->commit_transient(); |
| _arena->require(n); |
| return *this; |
| } |
| |
| Context::self_type & |
| Context::commit_transient() |
| { |
| if (_transient == TRANSIENT_ACTIVE) { |
| throw std::logic_error("Recursive use of transient buffer in context"); |
| } else if (_transient) { |
| _arena->alloc(_transient); |
| _transient = 0; |
| } |
| return *this; |
| } |
| |
| TextView |
| Context::active_group(int idx) |
| { |
| auto ovector = pcre2_get_ovector_pointer(_rxp_active); |
| idx *= 2; // To account for offset pairs. |
| TS_DBG("Access match group %d at offsets %ld:%ld", idx / 2, ovector[idx], ovector[idx + 1]); |
| return _rxp_src.substr(ovector[idx], ovector[idx + 1] - ovector[idx]); |
| } |
| |
| unsigned |
| Context::ArgPack::count() const |
| { |
| return pcre2_get_ovector_count(_ctx._rxp_active); |
| } |
| |
| BufferWriter & |
| Context::ArgPack::print(BufferWriter &w, swoc::bwf::Spec const &spec, unsigned idx) const |
| { |
| return bwformat(w, spec, _ctx.active_group(idx)); |
| } |
| |
| std::any |
| Context::ArgPack::capture(unsigned) const |
| { |
| return "Bogus"_sv; |
| } |