| /* | |
| * 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 <catch.hpp> | |
| #include <algorithm> | |
| #include <cstdlib> | |
| #include <memory> | |
| #include <string> | |
| #include "Session.h" | |
| #include "SessionBuilder.h" | |
| #include "SessionC.h" | |
| #include "SessionDataSet.h" | |
| #include "SessionImpl.h" | |
| #include "SessionPool.h" | |
| #include "TableSessionBuilder.h" | |
| class SessionTestAccessor { | |
| public: | |
| static std::vector<TEndPoint> availableNodes(Session& session) { | |
| return session.impl_->nodesSupplier_->getEndPointList(); | |
| } | |
| static bool hasConnectionTo(Session& session, const TEndPoint& endpoint) { | |
| return session.impl_->endPointToSessionConnection.find(endpoint) != | |
| session.impl_->endPointToSessionConnection.end(); | |
| } | |
| static void makeInitialConnectionUnavailable(Session& session, | |
| const TEndPoint& bootstrapEndpoint) { | |
| auto initialConnection = session.impl_->defaultSessionConnection_; | |
| initialConnection->close(); | |
| session.impl_->endPointToSessionConnection.clear(); | |
| session.impl_->endPointToSessionConnection.emplace(bootstrapEndpoint, initialConnection); | |
| } | |
| }; | |
| namespace { | |
| std::string fixture(const std::string& name) { | |
| return std::string(IOTDB_TEST_FIXTURES_DIR) + "/tls/" + name; | |
| } | |
| bool mutualTlsEnabled() { | |
| const char* value = std::getenv("IOTDB_CPP_SSL_MUTUAL_AUTH"); | |
| return value != nullptr && std::string(value) == "1"; | |
| } | |
| template <typename Builder> void configureTls(Builder& builder) { | |
| builder.useSSL(true)->trustCertFilePath(fixture("ca.crt")); | |
| if (mutualTlsEnabled()) { | |
| builder.clientCertificateFilePath(fixture("client.crt")) | |
| ->clientPrivateKeyFilePath(fixture("client.key")); | |
| } | |
| } | |
| SslConfig sslConfig() { | |
| SslConfig config; | |
| config.useSsl = true; | |
| config.trustCertFilePath = fixture("ca.crt"); | |
| if (mutualTlsEnabled()) { | |
| config.clientCertificateFilePath = fixture("client.crt"); | |
| config.clientPrivateKeyFilePath = fixture("client.key"); | |
| } | |
| return config; | |
| } | |
| void requireDataSet(std::unique_ptr<SessionDataSet> dataSet) { | |
| REQUIRE(dataSet != nullptr); | |
| REQUIRE(dataSet->hasNext()); | |
| REQUIRE(dataSet->next() != nullptr); | |
| dataSet->closeOperationHandle(); | |
| } | |
| void requireCDataSet(CSessionDataSet* dataSet) { | |
| REQUIRE(dataSet != nullptr); | |
| REQUIRE(ts_dataset_has_next(dataSet)); | |
| CRowRecord* row = ts_dataset_next(dataSet); | |
| REQUIRE(row != nullptr); | |
| ts_row_record_destroy(row); | |
| ts_dataset_destroy(dataSet); | |
| } | |
| } // namespace | |
| TEST_CASE("C++ APIs communicate with a TLS-enabled IoTDB", "[tls]") { | |
| SessionBuilder treeBuilder; | |
| treeBuilder.host("127.0.0.1")->rpcPort(6667)->username("root")->password("root"); | |
| configureTls(treeBuilder); | |
| auto treeSession = treeBuilder.build(); | |
| requireDataSet(treeSession->executeQueryStatement("SHOW VERSION")); | |
| treeSession->close(); | |
| TableSessionBuilder tableBuilder; | |
| tableBuilder.host("127.0.0.1")->rpcPort(6667)->username("root")->password("root"); | |
| configureTls(tableBuilder); | |
| auto tableSession = tableBuilder.build(); | |
| requireDataSet(tableSession->executeQueryStatement("SHOW VERSION")); | |
| tableSession->close(); | |
| SessionPoolBuilder poolBuilder; | |
| poolBuilder.host("127.0.0.1")->rpcPort(6667)->username("root")->password("root"); | |
| configureTls(poolBuilder); | |
| auto pool = poolBuilder.build(); | |
| auto pooledDataSet = pool->executeQueryStatement("SHOW VERSION"); | |
| REQUIRE(pooledDataSet->hasNext()); | |
| REQUIRE(pooledDataSet->next() != nullptr); | |
| pool->close(); | |
| } | |
| TEST_CASE("C APIs communicate with a TLS-enabled IoTDB", "[tls]") { | |
| const std::string certificatePath = fixture("client.crt"); | |
| const std::string privateKeyPath = fixture("client.key"); | |
| const char* cert = mutualTlsEnabled() ? certificatePath.c_str() : nullptr; | |
| const char* key = mutualTlsEnabled() ? privateKeyPath.c_str() : nullptr; | |
| CSession* treeSession = ts_session_new("127.0.0.1", 6667, "root", "root"); | |
| REQUIRE(treeSession != nullptr); | |
| REQUIRE(ts_session_set_ssl_config(treeSession, fixture("ca.crt").c_str(), cert, key) == TS_OK); | |
| REQUIRE(ts_session_open(treeSession) == TS_OK); | |
| CSessionDataSet* treeDataSet = nullptr; | |
| REQUIRE(ts_session_execute_query(treeSession, "SHOW VERSION", &treeDataSet) == TS_OK); | |
| requireCDataSet(treeDataSet); | |
| REQUIRE(ts_session_close(treeSession) == TS_OK); | |
| ts_session_destroy(treeSession); | |
| CTableSession* tableSession = ts_table_session_new_with_ssl("127.0.0.1", 6667, "root", "root", "", | |
| fixture("ca.crt").c_str(), cert, key); | |
| REQUIRE(tableSession != nullptr); | |
| CSessionDataSet* tableDataSet = nullptr; | |
| REQUIRE(ts_table_session_execute_query(tableSession, "SHOW VERSION", &tableDataSet) == TS_OK); | |
| requireCDataSet(tableDataSet); | |
| REQUIRE(ts_table_session_close(tableSession) == TS_OK); | |
| ts_table_session_destroy(tableSession); | |
| } | |
| TEST_CASE("TLS node discovery uses the final SSL configuration and supports failover", "[tls]") { | |
| std::vector<std::string> bootstrapNodes = {"127.0.0.1:6667"}; | |
| Session session(bootstrapNodes, "root", "root"); | |
| session.setSslConfig(sslConfig()); | |
| session.open(); | |
| auto discoveredNodes = SessionTestAccessor::availableNodes(session); | |
| auto discovered = | |
| std::find_if(discoveredNodes.begin(), discoveredNodes.end(), [](const TEndPoint& node) { | |
| return node.ip == "localhost" && node.port == 6667; | |
| }); | |
| REQUIRE(discovered != discoveredNodes.end()); | |
| TEndPoint bootstrapEndpoint; | |
| bootstrapEndpoint.ip = "127.0.0.1"; | |
| bootstrapEndpoint.port = 6667; | |
| REQUIRE(std::none_of(discoveredNodes.begin(), discoveredNodes.end(), | |
| [&bootstrapEndpoint](const TEndPoint& node) { | |
| return node.ip == bootstrapEndpoint.ip && | |
| node.port == bootstrapEndpoint.port; | |
| })); | |
| // Model an unavailable initial bootstrap after discovery. The query must | |
| // establish a new TLS connection to the endpoint learned from the server. | |
| SessionTestAccessor::makeInitialConnectionUnavailable(session, bootstrapEndpoint); | |
| REQUIRE_FALSE(SessionTestAccessor::hasConnectionTo(session, *discovered)); | |
| requireDataSet(session.executeQueryStatement("SHOW VERSION")); | |
| REQUIRE(SessionTestAccessor::hasConnectionTo(session, *discovered)); | |
| session.close(); | |
| } | |
| TEST_CASE("mTLS server rejects a client without a certificate", "[mtls]") { | |
| SessionBuilder builder; | |
| builder.host("127.0.0.1") | |
| ->rpcPort(6667) | |
| ->username("root") | |
| ->password("root") | |
| ->useSSL(true) | |
| ->trustCertFilePath(fixture("ca.crt")); | |
| REQUIRE_THROWS(builder.build()); | |
| } |