/* -*- Mode: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */ /* * Copyright 2020-2021 Couchbase, Inc. * * Licensed 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 "connection_string.hxx" #include "core/logger/logger.hxx" #include "core/meta/version.hxx" #include "duration_parser.hxx" #include "url_codec.hxx" #include #include namespace couchbase::core::utils { namespace priv { using namespace tao::pegtl; struct bucket_name : seq { }; using param_key = star>>; using param_value = star>, one<'/'>>>; struct param : seq, istring<'g', 'c', 'c', 'c', 'p'>, istring<'h', 't', 't', 'p'>, istring<'m', 'c', 'd'>> { }; using node = seq, opt, mode>>; using opt_bucket_name = opt_must, bucket_name>; using opt_params = opt_must, list_must>>; using opt_nodes = seq>, opt_bucket_name>; struct scheme : seq, uri::dslash> { }; using opt_scheme = opt; using grammar = must>; template struct action { }; template<> struct action { template static void apply(const ActionInput& in, connection_string& cs, connection_string::node& /* cur_node */) { cs.scheme = in.string().substr(0, in.string().rfind(':')); if (cs.scheme == "couchbase") { cs.default_port = 11210; cs.default_mode = connection_string::bootstrap_mode::gcccp; cs.tls = false; } else if (cs.scheme == "couchbases") { cs.default_port = 11207; cs.default_mode = connection_string::bootstrap_mode::gcccp; cs.tls = true; } else if (cs.scheme == "http") { cs.default_port = 8091; cs.default_mode = connection_string::bootstrap_mode::http; cs.tls = false; } else if (cs.scheme == "https") { cs.default_port = 18091; cs.default_mode = connection_string::bootstrap_mode::http; cs.tls = true; } else { cs.default_mode = connection_string::bootstrap_mode::unspecified; cs.default_port = 0; } } }; template<> struct action { template static void apply(const ActionInput& in, connection_string& cs, connection_string::node& /* cur_node */) { const auto& pair = in.string(); auto eq = pair.find('='); std::string key = pair.substr(0, eq); cs.params[key] = (eq == std::string::npos) ? "" : pair.substr(eq + 1); } }; template<> struct action { template static void apply(const ActionInput& in, connection_string& /* cs */, connection_string::node& cur_node) { cur_node.type = connection_string::address_type::dns; cur_node.address = in.string_view(); } }; template<> struct action { template static void apply(const ActionInput& in, connection_string& /* cs */, connection_string::node& cur_node) { cur_node.type = connection_string::address_type::ipv4; cur_node.address = in.string_view(); } }; template<> struct action { template static void apply(const ActionInput& in, connection_string& /* cs */, connection_string::node& cur_node) { cur_node.type = connection_string::address_type::ipv6; cur_node.address = in.string_view(); } }; template<> struct action { template static void apply(const ActionInput& /* in */, connection_string& cs, connection_string::node& cur_node) { if (!cur_node.address.empty()) { cs.bootstrap_nodes.push_back(cur_node); } cur_node = {}; } }; template<> struct action { template static void apply(const ActionInput& in, connection_string& /* cs */, connection_string::node& cur_node) { if (in.empty()) { return; } cur_node.port = static_cast(std::stoul(in.string())); } }; template<> struct action { template static void apply(const ActionInput& in, connection_string& /* cs */, connection_string::node& cur_node) { std::string mode = in.string(); std::transform(mode.begin(), mode.end(), mode.begin(), [](unsigned char c) { return std::tolower(c); }); if (mode == "mcd" || mode == "gcccp" || mode == "cccp") { cur_node.mode = connection_string::bootstrap_mode::gcccp; } else if (mode == "http") { cur_node.mode = connection_string::bootstrap_mode::http; } } }; template<> struct action { template static void apply(const ActionInput& in, connection_string& cs, connection_string::node& /* cur_node */) { cs.default_bucket_name = in.string(); } }; } // namespace priv void parse_option(std::string& receiver, const std::string& /* name */, const std::string& value, std::vector& /* warnings */) { receiver = string_codec::url_decode(value); } void parse_option(bool& receiver, const std::string& name, const std::string& value, std::vector& warnings) { if (value == "true" || value == "yes" || value == "on") { receiver = true; } else if (value == "false" || value == "no" || value == "off") { receiver = false; } else { warnings.push_back(fmt::format( R"(unable to parse "{}" parameter in connection string (value "{}" cannot be interpreted as a boolean))", name, value)); } } void parse_option(tls_verify_mode& receiver, const std::string& name, const std::string& value, std::vector& warnings) { if (value == "none") { receiver = tls_verify_mode::none; } else if (value == "peer") { receiver = tls_verify_mode::peer; } else { warnings.push_back(fmt::format( R"(unable to parse "{}" parameter in connection string (value "{}" is not a valid TLS verification mode))", name, value)); } } void parse_option(io::ip_protocol& receiver, const std::string& name, const std::string& value, std::vector& warnings) { if (value == "any") { receiver = io::ip_protocol::any; } else if (value == "force_ipv4") { receiver = io::ip_protocol::force_ipv4; } else if (value == "force_ipv6") { receiver = io::ip_protocol::force_ipv6; } else { warnings.push_back(fmt::format( R"(unable to parse "{}" parameter in connection string (value "{}" is not a valid IP protocol preference))", name, value)); } } void parse_option(std::size_t& receiver, const std::string& name, const std::string& value, std::vector& warnings) { try { receiver = std::stoull(value, nullptr, 10); } catch (const std::invalid_argument& ex1) { warnings.push_back( fmt::format(R"(unable to parse "{}" parameter in connection string (value "{}" is not a number): {})", name, value, ex1.what())); } catch (const std::out_of_range& ex2) { warnings.push_back( fmt::format(R"(unable to parse "{}" parameter in connection string (value "{}" is out of range): {})", name, value, ex2.what())); } } void parse_option(std::chrono::milliseconds& receiver, const std::string& name, const std::string& value, std::vector& warnings) { try { receiver = std::chrono::duration_cast(parse_duration(value)); } catch (const duration_parse_error&) { try { receiver = std::chrono::milliseconds(std::stoull(value, nullptr, 10)); } catch (const std::invalid_argument& ex1) { warnings.push_back(fmt::format( R"(unable to parse "{}" parameter in connection string (value "{}" is not a number): {})", name, value, ex1.what())); } catch (const std::out_of_range& ex2) { warnings.push_back(fmt::format( R"(unable to parse "{}" parameter in connection string (value "{}" is out of range): {})", name, value, ex2.what())); } } } static void extract_options(connection_string& connstr) { connstr.options.enable_tls = connstr.tls; if (connstr.bootstrap_nodes.size() != 1 || connstr.bootstrap_nodes[0].type != connection_string::address_type::dns) { connstr.options.enable_dns_srv = false; } for (const auto& [name, value] : connstr.params) { if (name == "kv_connect_timeout") { /** * Number of seconds the client should wait while attempting to connect to a node’s KV service via a socket. Initial * connection, reconnecting, node added, etc. */ parse_option(connstr.options.connect_timeout, name, value, connstr.warnings); } else if (name == "kv_timeout" || name == "key_value_timeout") { /** * Number of milliseconds to wait before timing out a KV operation by the client. */ parse_option(connstr.options.key_value_timeout, name, value, connstr.warnings); } else if (name == "kv_durable_timeout" || name == "key_value_durable_timeout") { /** * Number of milliseconds to wait before timing out a KV operation that is either using synchronous durability or * observe-based durability. */ parse_option(connstr.options.key_value_durable_timeout, name, value, connstr.warnings); } else if (name == "view_timeout") { /** * Number of seconds to wait before timing out a View request by the client.. */ parse_option(connstr.options.view_timeout, name, value, connstr.warnings); } else if (name == "query_timeout") { /** * Number of seconds to wait before timing out a Query or N1QL request by the client. */ parse_option(connstr.options.query_timeout, name, value, connstr.warnings); } else if (name == "analytics_timeout") { /** * Number of seconds to wait before timing out an Analytics request by the client. */ parse_option(connstr.options.analytics_timeout, name, value, connstr.warnings); } else if (name == "search_timeout") { /** * Number of seconds to wait before timing out a Search request by the client. */ parse_option(connstr.options.search_timeout, name, value, connstr.warnings); } else if (name == "management_timeout") { /** * Number of seconds to wait before timing out a Management API request by the client. */ parse_option(connstr.options.management_timeout, name, value, connstr.warnings); } else if (name == "trust_certificate") { parse_option(connstr.options.trust_certificate, name, value, connstr.warnings); } else if (name == "enable_mutation_tokens") { /** * Request mutation tokens at connection negotiation time. Turning this off will save 16 bytes per operation response. */ parse_option(connstr.options.enable_mutation_tokens, name, value, connstr.warnings); } else if (name == "enable_tcp_keep_alive") { /** * Gets or sets a value indicating whether enable TCP keep-alive. */ parse_option(connstr.options.enable_tcp_keep_alive, name, value, connstr.warnings); } else if (name == "tcp_keep_alive_interval") { /** * Specifies the timeout, in milliseconds, with no activity until the first keep-alive packet is sent. This applies to all * services, but is advisory: if the underlying platform does not support this on all connections, it will be applied only * on those it can be. */ parse_option(connstr.options.tcp_keep_alive_interval, name, value, connstr.warnings); } else if (name == "force_ipv4") { /** * Sets the SDK configuration to do IPv4 Name Resolution */ bool force_ipv4 = false; parse_option(force_ipv4, name, value, connstr.warnings); if (force_ipv4) { connstr.options.use_ip_protocol = io::ip_protocol::force_ipv4; } } else if (name == "ip_protocol") { /** * Controls preference of IP protocol for name resolution */ parse_option(connstr.options.use_ip_protocol, name, value, connstr.warnings); } else if (name == "config_poll_interval") { parse_option(connstr.options.config_poll_interval, name, value, connstr.warnings); } else if (name == "config_poll_floor") { parse_option(connstr.options.config_poll_floor, name, value, connstr.warnings); } else if (name == "max_http_connections") { /** * The maximum number of HTTP connections allowed on a per-host and per-port basis. 0 indicates an unlimited number of * connections are permitted. */ parse_option(connstr.options.max_http_connections, name, value, connstr.warnings); } else if (name == "idle_http_connection_timeout") { /** * The period of time an HTTP connection can be idle before it is forcefully disconnected. */ parse_option(connstr.options.idle_http_connection_timeout, name, value, connstr.warnings); } else if (name == "bootstrap_timeout") { /** * The period of time allocated to complete bootstrap */ parse_option(connstr.options.bootstrap_timeout, name, value, connstr.warnings); } else if (name == "resolve_timeout") { /** * The period of time to resolve DNS name of the node to IP address */ parse_option(connstr.options.resolve_timeout, name, value, connstr.warnings); } else if (name == "enable_dns_srv") { if (connstr.bootstrap_nodes.size() == 1) { parse_option(connstr.options.enable_dns_srv, name, value, connstr.warnings); } else { connstr.warnings.push_back(fmt::format( R"(parameter "{}" requires single entry in bootstrap nodes list of the connection string, ignoring (value "{}"))", name, value)); } } else if (name == "network") { connstr.options.network = value; /* current known values are "auto", "default" and "external" */ } else if (name == "show_queries") { /** * Whether to display N1QL, Analytics, Search queries on info level (default false) */ parse_option(connstr.options.show_queries, name, value, connstr.warnings); } else if (name == "enable_clustermap_notification") { /** * Allow the server to push configuration updates asynchronously. */ parse_option(connstr.options.enable_clustermap_notification, name, value, connstr.warnings); } else if (name == "enable_unordered_execution") { /** * Allow the server to reorder commands */ parse_option(connstr.options.enable_unordered_execution, name, value, connstr.warnings); } else if (name == "enable_compression") { /** * Announce support of compression (snappy) to server */ parse_option(connstr.options.enable_compression, name, value, connstr.warnings); } else if (name == "enable_tracing") { /** * true - use threshold_logging_tracer * false - use noop_tracer */ parse_option(connstr.options.enable_tracing, name, value, connstr.warnings); } else if (name == "enable_metrics") { /** * true - use logging_meter * false - use noop_meter */ parse_option(connstr.options.enable_metrics, name, value, connstr.warnings); } else if (name == "tls_verify") { parse_option(connstr.options.tls_verify, name, value, connstr.warnings); } else if (name == "disable_mozilla_ca_certificates") { parse_option(connstr.options.disable_mozilla_ca_certificates, name, value, connstr.warnings); } else if (name == "tls_disable_deprecated_protocols") { parse_option(connstr.options.tls_disable_deprecated_protocols, name, value, connstr.warnings); } else if (name == "tls_disable_v1_2") { parse_option(connstr.options.tls_disable_v1_2, name, value, connstr.warnings); } else if (name == "user_agent_extra") { /** * string, that will be appended to identification fields of the server protocols (key in HELO packet for MCBP, "user-agent" * header for HTTP) */ parse_option(connstr.options.user_agent_extra, name, value, connstr.warnings); } else if (name == "dump_configuration") { /** * Whether to dump every new configuration on TRACE level */ parse_option(connstr.options.dump_configuration, name, value, connstr.warnings); } else { connstr.warnings.push_back(fmt::format(R"(unknown parameter "{}" in connection string (value "{}"))", name, value)); } } } connection_string parse_connection_string(const std::string& input, cluster_options options) { connection_string res{}; res.options = std::move(options); if (input.empty()) { res.error = "failed to parse connection string: empty input"; return res; } auto in = tao::pegtl::memory_input(input, __FUNCTION__); try { connection_string::node node{}; tao::pegtl::parse(in, res, node); } catch (const tao::pegtl::parse_error& e) { for (const auto& position : e.positions()) { if (position.source == __FUNCTION__) { res.error = fmt::format( "failed to parse connection string (column: {}, trailer: \"{}\")", position.column, input.substr(position.byte)); break; } } if (!res.error) { res.error = e.what(); } } extract_options(res); return res; } } // namespace couchbase::core::utils