| /** @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. |
| */ |
| |
| /*************************** -*- Mod: C++ -*- ****************************** |
| SSLSNIConfig.cc |
| Created On : 05/02/2017 |
| |
| Description: |
| SNI based Configuration in ATS |
| ****************************************************************************/ |
| |
| #include "SSLSNIConfig.h" |
| #include "P_SNIActionPerformer.h" |
| |
| #include "PreWarmManager.h" |
| |
| #include "tscore/Diags.h" |
| #include "tscore/SimpleTokenizer.h" |
| #include "tscore/ink_memory.h" |
| #include "tscore/I_Layout.h" |
| |
| #include "tscpp/util/TextView.h" |
| |
| #include <sstream> |
| #include <utility> |
| #include <pcre.h> |
| |
| static constexpr int OVECSIZE{30}; |
| |
| //// |
| // NamedElement |
| // |
| NamedElement::NamedElement(NamedElement &&other) |
| { |
| *this = std::move(other); |
| } |
| |
| NamedElement & |
| NamedElement::operator=(NamedElement &&other) |
| { |
| if (this != &other) { |
| match = std::move(other.match); |
| } |
| return *this; |
| } |
| |
| void |
| NamedElement::set_glob_name(std::string name) |
| { |
| std::string::size_type pos = 0; |
| while ((pos = name.find('.', pos)) != std::string::npos) { |
| name.replace(pos, 1, "\\."); |
| pos += 2; |
| } |
| pos = 0; |
| while ((pos = name.find('*', pos)) != std::string::npos) { |
| name.replace(pos, 1, "(.{0,})"); |
| } |
| Debug("ssl_sni", "Regexed fqdn=%s", name.c_str()); |
| set_regex_name(name); |
| } |
| |
| void |
| NamedElement::set_regex_name(const std::string ®ex_name) |
| { |
| const char *err_ptr; |
| int err_offset = 0; |
| if (!regex_name.empty()) { |
| match.reset(pcre_compile(regex_name.c_str(), PCRE_ANCHORED | PCRE_CASELESS, &err_ptr, &err_offset, nullptr)); |
| } |
| } |
| |
| //// |
| // SNIConfigParams |
| // |
| const NextHopProperty * |
| SNIConfigParams::get_property_config(const std::string &servername) const |
| { |
| const NextHopProperty *nps = nullptr; |
| for (auto &&item : next_hop_list) { |
| if (pcre_exec(item.match.get(), nullptr, servername.c_str(), servername.length(), 0, 0, nullptr, 0) >= 0) { |
| // Found a match |
| nps = &item.prop; |
| break; |
| } |
| } |
| return nps; |
| } |
| |
| int |
| SNIConfigParams::load_sni_config() |
| { |
| for (auto &item : yaml_sni.items) { |
| auto ai = sni_action_list.emplace(sni_action_list.end()); |
| ai->set_glob_name(item.fqdn); |
| Debug("ssl", "name: %s", item.fqdn.data()); |
| |
| // set SNI based actions to be called in the ssl_servername_only callback |
| if (item.offer_h2.has_value()) { |
| ai->actions.push_back(std::make_unique<ControlH2>(item.offer_h2.value())); |
| } |
| if (item.verify_client_level != 255) { |
| ai->actions.push_back( |
| std::make_unique<VerifyClient>(item.verify_client_level, item.verify_client_ca_file, item.verify_client_ca_dir)); |
| } |
| if (item.host_sni_policy != 255) { |
| ai->actions.push_back(std::make_unique<HostSniPolicy>(item.host_sni_policy)); |
| } |
| if (item.valid_tls_version_min_in >= 0 || item.valid_tls_version_max_in >= 0) { |
| ai->actions.push_back(std::make_unique<TLSValidProtocols>(item.valid_tls_version_min_in, item.valid_tls_version_max_in)); |
| } else { |
| if (!item.protocol_unset) { |
| ai->actions.push_back(std::make_unique<TLSValidProtocols>(item.protocol_mask)); |
| } |
| } |
| if (item.tunnel_destination.length() > 0) { |
| ai->actions.push_back( |
| std::make_unique<TunnelDestination>(item.tunnel_destination, item.tunnel_type, item.tunnel_prewarm, item.tunnel_alpn)); |
| } |
| if (!item.client_sni_policy.empty()) { |
| ai->actions.push_back(std::make_unique<OutboundSNIPolicy>(item.client_sni_policy)); |
| } |
| if (item.http2_buffer_water_mark.has_value()) { |
| ai->actions.push_back(std::make_unique<HTTP2BufferWaterMark>(item.http2_buffer_water_mark.value())); |
| } |
| |
| ai->actions.push_back(std::make_unique<ServerMaxEarlyData>(item.server_max_early_data)); |
| |
| ai->actions.push_back(std::make_unique<SNI_IpAllow>(item.ip_allow, item.fqdn)); |
| |
| // set the next hop properties |
| auto nps = next_hop_list.emplace(next_hop_list.end()); |
| |
| SSLConfig::scoped_config params; |
| // Load if we have at least specified the client certificate |
| if (!item.client_cert.empty()) { |
| nps->prop.client_cert_file = Layout::get()->relative_to(params->clientCertPathOnly, item.client_cert.data()); |
| if (!item.client_key.empty()) { |
| nps->prop.client_key_file = Layout::get()->relative_to(params->clientKeyPathOnly, item.client_key.data()); |
| } |
| |
| auto ctx = params->getCTX(nps->prop.client_cert_file, nps->prop.client_key_file, params->clientCACertFilename, |
| params->clientCACertPath); |
| if (ctx.get() == nullptr) { |
| return 1; |
| } |
| } |
| |
| nps->set_glob_name(item.fqdn); |
| nps->prop.verify_server_policy = item.verify_server_policy; |
| nps->prop.verify_server_properties = item.verify_server_properties; |
| } // end for |
| |
| return 0; |
| } |
| |
| std::pair<const ActionVector *, ActionItem::Context> |
| SNIConfigParams::get(std::string_view servername) const |
| { |
| int ovector[OVECSIZE]; |
| |
| for (const auto &retval : sni_action_list) { |
| int length = servername.length(); |
| if (retval.match == nullptr && length == 0) { |
| return {&retval.actions, {}}; |
| } else if (auto offset = pcre_exec(retval.match.get(), nullptr, servername.data(), length, 0, 0, ovector, OVECSIZE); |
| offset >= 0) { |
| if (offset == 1) { |
| // first pair identify the portion of the subject string matched by the entire pattern |
| if (ovector[0] == 0 && ovector[1] == length) { |
| // full match |
| return {&retval.actions, {}}; |
| } else { |
| continue; |
| } |
| } |
| // If contains groups |
| if (offset == 0) { |
| // reset to max if too many. |
| offset = OVECSIZE / 3; |
| } |
| |
| ActionItem::Context::CapturedGroupViewVec groups; |
| groups.reserve(offset); |
| for (int strnum = 1; strnum < offset; strnum++) { |
| const std::size_t start = ovector[2 * strnum]; |
| const std::size_t length = ovector[2 * strnum + 1] - start; |
| |
| groups.emplace_back(servername.data() + start, length); |
| } |
| return {&retval.actions, {std::move(groups)}}; |
| } |
| } |
| return {nullptr, {}}; |
| } |
| |
| int |
| SNIConfigParams::initialize() |
| { |
| std::string sni_filename = RecConfigReadConfigPath("proxy.config.ssl.servername.filename"); |
| |
| Note("%s loading ...", sni_filename.c_str()); |
| |
| struct stat sbuf; |
| if (stat(sni_filename.c_str(), &sbuf) == -1 && errno == ENOENT) { |
| Note("%s failed to load", sni_filename.c_str()); |
| Warning("Loading SNI configuration - filename: %s doesn't exist", sni_filename.c_str()); |
| |
| return 0; |
| } |
| |
| YamlSNIConfig yaml_sni_tmp; |
| ts::Errata zret = yaml_sni_tmp.loader(sni_filename); |
| if (!zret.isOK()) { |
| std::stringstream errMsg; |
| errMsg << zret; |
| if (TSSystemState::is_initializing()) { |
| Emergency("%s failed to load: %s", sni_filename.c_str(), errMsg.str().c_str()); |
| } else { |
| Error("%s failed to load: %s", sni_filename.c_str(), errMsg.str().c_str()); |
| } |
| return 1; |
| } |
| yaml_sni = std::move(yaml_sni_tmp); |
| |
| return load_sni_config(); |
| } |
| |
| SNIConfigParams::~SNIConfigParams() |
| { |
| // sni_action_list and next_hop_list should cleanup with the params object |
| } |
| |
| //// |
| // SNIConfig |
| // |
| int SNIConfig::_configid = 0; |
| |
| void |
| SNIConfig::startup() |
| { |
| if (!reconfigure()) { |
| std::string sni_filename = RecConfigReadConfigPath("proxy.config.ssl.servername.filename"); |
| Fatal("failed to load %s", sni_filename.c_str()); |
| } |
| } |
| |
| int |
| SNIConfig::reconfigure() |
| { |
| Debug("ssl", "Reload SNI file"); |
| SNIConfigParams *params = new SNIConfigParams; |
| |
| int retStatus = params->initialize(); |
| if (!retStatus) { |
| _configid = configProcessor.set(_configid, params); |
| prewarmManager.reconfigure(); |
| } else { |
| delete params; |
| } |
| |
| std::string sni_filename = RecConfigReadConfigPath("proxy.config.ssl.servername.filename"); |
| if (!retStatus || TSSystemState::is_initializing()) { |
| Note("%s finished loading", sni_filename.c_str()); |
| } else { |
| Error("%s failed to load", sni_filename.c_str()); |
| } |
| |
| return !retStatus; |
| } |
| |
| SNIConfigParams * |
| SNIConfig::acquire() |
| { |
| return static_cast<SNIConfigParams *>(configProcessor.get(_configid)); |
| } |
| |
| void |
| SNIConfig::release(SNIConfigParams *params) |
| { |
| configProcessor.release(_configid, params); |
| } |
| |
| // See if any of the client-side actions would trigger for this combination of servername and client IP |
| // host_sni_policy is an in/out parameter. It starts with the global policy from the records.yaml |
| // setting proxy.config.http.host_sni_policy and is possibly overridden if the sni policy |
| // contains a host_sni_policy entry |
| bool |
| SNIConfig::test_client_action(const char *servername, const IpEndpoint &ep, int &host_sni_policy) |
| { |
| bool retval = false; |
| SNIConfig::scoped_config params; |
| |
| const auto &actions = params->get(servername); |
| if (actions.first) { |
| for (auto &&item : *actions.first) { |
| retval |= item->TestClientSNIAction(servername, ep, host_sni_policy); |
| } |
| } |
| return retval; |
| } |