diff --git a/src/client.cpp b/src/client.cpp index a877883a..397cc423 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -87,10 +87,13 @@ RemoteError::~RemoteError() {} Finished::~Finished() {} -Connected::Connected(const std::string& peerName) +Connected::Connected(const std::string& peerName, + const epicsTime& time, + const std::shared_ptr &cred) :std::runtime_error("Connected") ,peerName(peerName) - ,time(epicsTime::getCurrent()) + ,time(time) + ,cred(cred) {} Connected::~Connected() {} @@ -273,10 +276,13 @@ std::shared_ptr ConnectBuilder::exec() op->chan = Channel::build(context, op->_name, server); bool cur = op->_connected = op->chan->state==Channel::Active; - if(cur && op->_onConn) - op->_onConn(); - else if(!cur && op->_onDis) + if(cur && op->_onConn) { + auto& conn = op->chan->conn; + Connected evt(conn->peerName, conn->connTime, conn->cred); + op->_onConn(evt); + } else if(!cur && op->_onDis) { op->_onDis(); + } op->chan->connectors.push_back(op.get()); }); diff --git a/src/clientconn.cpp b/src/clientconn.cpp index e964c185..8c68f276 100644 --- a/src/clientconn.cpp +++ b/src/clientconn.cpp @@ -171,6 +171,34 @@ void Connection::bevEvent(short events) if(bev && (events&BEV_EVENT_CONNECTED)) { log_debug_printf(io, "Connected to %s\n", peerName.c_str()); + connTime = epicsTime::getCurrent(); + + auto peerCred(std::make_shared()); + peerCred->peer = peerName; + peerCred->isTLS = isTLS; + +#ifdef PVXS_ENABLE_OPENSSL + if(isTLS) { + auto ctx = bufferevent_openssl_get_ssl(bev.get()); + assert(ctx); + auto cert = SSL_get0_peer_certificate(ctx); + // SSL_get0_verified_chain() + if(cert) { + auto subj = X509_get_subject_name(cert); + char name[64]; + if(subj && X509_NAME_get_text_by_NID(subj, NID_commonName, name, sizeof(name)-1)) { + name[sizeof(name)-1] = '\0'; + log_debug_printf(io, "Peer CN=%s\n", name); + peerCred->method = "x509"; + peerCred->account = name; + } + } + } else +#endif + { + peerCred->method = "anonymous"; + } + cred = std::move(peerCred); if(bufferevent_enable(bev.get(), EV_READ|EV_WRITE)) throw std::logic_error("Unable to enable BEV"); @@ -425,9 +453,10 @@ void Connection::handle_CREATE_CHANNEL() auto conns(chan->connectors); // copy list + struct Connected connEvt(peerName, connTime, cred); for(auto& conn : conns) { if(!conn->_connected.exchange(true, std::memory_order_relaxed) && conn->_onConn) - conn->_onConn(); + conn->_onConn(connEvt); } } } diff --git a/src/clientimpl.h b/src/clientimpl.h index 92cce304..cd13e630 100644 --- a/src/clientimpl.h +++ b/src/clientimpl.h @@ -103,6 +103,9 @@ struct Connection final : public ConnBase, public std::enable_shared_from_this cred; + INST_COUNTER(Connection); Connection(const std::shared_ptr& context, @@ -159,7 +162,7 @@ struct ConnectImpl final : public Connect std::shared_ptr chan; const std::string _name; std::atomic _connected; - std::function _onConn; + std::function _onConn; std::function _onDis; ConnectImpl(const evbase& loop, const std::string& name) diff --git a/src/clientmon.cpp b/src/clientmon.cpp index 8bf249da..9fed1ca5 100644 --- a/src/clientmon.cpp +++ b/src/clientmon.cpp @@ -359,7 +359,9 @@ struct SubscriptionImpl final : public OperationBase, public Subscription if(!maskConn) { notify = queue.empty() && wantToNotify(); - queue.emplace_back(std::make_exception_ptr(Connected(conn->peerName))); + queue.emplace_back(std::make_exception_ptr(Connected(conn->peerName, + conn->connTime, + conn->cred))); log_debug_printf(io, "Server %s channel %s monitor PUSH Connected\n", chan->conn ? chan->conn->peerName.c_str() : "", diff --git a/src/ossl.cpp b/src/ossl.cpp index c9dbb57e..dcafb5bf 100644 --- a/src/ossl.cpp +++ b/src/ossl.cpp @@ -237,22 +237,21 @@ ossl_setup_common(const SSL_METHOD *method, const impl::ConfigCommon &conf) #endif // TODO: SSL_CTX_set_options(), SSL_CTX_set_mode() ? - // TODO: CONF_modules_load_file(), SSL_CTX_config(ctx.ctx, "myiocname") ? // we mandate TLS >= 1.3 (void)SSL_CTX_set_min_proto_version(ctx.ctx, TLS1_3_VERSION); (void)SSL_CTX_set_max_proto_version(ctx.ctx, 0); // up to max. // populate SSL_CTX::cert_store - for(auto& ca : conf.tls_authority_files) { - log_debug_printf(_setup, "Read TLS CAs from %s\n", ca.c_str()); - if(1!=SSL_CTX_load_verify_locations(ctx.ctx, ca.c_str(), nullptr)) - throw SSLError("oops"); + for(auto& file : conf.tls_authority_files) { + log_debug_printf(_setup, "Read TLS CAs from %s\n", file.c_str()); + if(1!=SSL_CTX_load_verify_locations(ctx.ctx, file.c_str(), nullptr)) + throw SSLError("SSL_CTX_load_verify_locations"); } - for(auto& ca : conf.tls_authority_dirs) { - log_debug_printf(_setup, "Read TLS CAs in %s\n", ca.c_str()); - if(1!=SSL_CTX_load_verify_locations(ctx.ctx, nullptr, ca.c_str())) - throw SSLError("oops"); + for(auto& dir : conf.tls_authority_dirs) { + log_debug_printf(_setup, "Read TLS CAs in %s\n", dir.c_str()); + if(1!=SSL_CTX_load_verify_locations(ctx.ctx, nullptr, dir.c_str())) + throw SSLError("SSL_CTX_load_verify_locations"); } if(!conf.tls_keychain_file.empty()) { diff --git a/src/pvxs/client.h b/src/pvxs/client.h index 93980f79..07731eb0 100644 --- a/src/pvxs/client.h +++ b/src/pvxs/client.h @@ -28,6 +28,7 @@ namespace client { class Context; struct Config; +struct ServerCredentials; //! Operation failed because of connection to server was lost struct PVXS_API Disconnect : public std::runtime_error @@ -54,14 +55,25 @@ struct PVXS_API Finished : public Disconnect virtual ~Finished(); }; -//! For monitor only. Subscription has (re)connected. +//! Indication of connection to a server struct PVXS_API Connected : public std::runtime_error { - Connected(const std::string& peerName); + Connected(const std::string& peerName, + const epicsTime& time, + const std::shared_ptr& cred); + Connected(const std::string& peerName, + const std::shared_ptr& cred) + :Connected(peerName, epicsTime::getCurrent(), cred) // legacy + {} virtual ~Connected(); + //! Server IP address const std::string peerName; + //! Local time of connection const epicsTime time; + //! Identity of server. + //! @since UNRELEASED + const std::shared_ptr cred; }; struct PVXS_API Interrupted : public std::runtime_error @@ -610,6 +622,22 @@ class PVXS_API Context { std::shared_ptr pvt; }; +//! Identity of a server +//! @since UNRELEASED +struct ServerCredentials { + //! Peer address (eg. numeric IPv4) + std::string peer; + //! The local interface address (eg. numeric IPv4) through which this client is connected. + //! May be a wildcard address (eg. 0.0.0.0) if the receiving socket is so bound. + std::string iface; + //! How account was authenticated. ("anonymous" or "x509") + std::string method; + //! Remote user account name. Meaning depends upon method. + std::string account; + //! Operation over secure transport + bool isTLS = false; +}; + namespace detail { struct PVRParser; @@ -930,7 +958,7 @@ class ConnectBuilder std::shared_ptr ctx; std::string _pvname; std::string _server; - std::function _onConn; + std::function _onConn; std::function _onDis; bool _syncCancel = true; public: @@ -941,7 +969,16 @@ class ConnectBuilder {} //! Handler to be invoked when channel becomes connected. - ConnectBuilder& onConnect(std::function&& cb) { _onConn = std::move(cb); return *this; } + //! @since UNRELEASED + ConnectBuilder& onConnect(std::function&& cb) + { _onConn = std::move(cb); return *this; } + //! Handler to be invoked when channel becomes connected. + //! @since UNRELEASED Prefer void(const Connected&) in new code. + ConnectBuilder& onConnect(std::function&& cb) + { + _onConn = [cb](const Connected&) { cb(); }; + return *this; + } //! Handler to be invoked when channel becomes disconnected. ConnectBuilder& onDisconnect(std::function&& cb) { _onDis = std::move(cb); return *this; } diff --git a/src/pvxs/srvcommon.h b/src/pvxs/srvcommon.h index 0458828e..cfac7451 100644 --- a/src/pvxs/srvcommon.h +++ b/src/pvxs/srvcommon.h @@ -33,7 +33,7 @@ struct PVXS_API ClientCredentials { //! The local interface address (eg. numeric IPv4) through which this client is connected. //! May be a wildcard address (eg. 0.0.0.0) if the receiving socket is so bound. std::string iface; - //! Authentication "method" + //! How account was authenticated. ("anonymous", "ca", or "x509") std::string method; //! Remote user account name. Meaning depends upon method. std::string account; @@ -47,6 +47,10 @@ struct PVXS_API ClientCredentials { * On other targets, an empty list is returned. */ std::set roles() const; + /** Operation over secure transport + * @since UNRELEASED + */ + bool isTLS = false; }; PVXS_API diff --git a/src/serverconn.cpp b/src/serverconn.cpp index 2da56b72..b02080d2 100644 --- a/src/serverconn.cpp +++ b/src/serverconn.cpp @@ -218,6 +218,7 @@ void ServerConn::handle_CONNECTION_VALIDATION() std::string(SB()<(*cred)); + C->isTLS = iface->isTLS; if(selected=="ca") { auth["user"].as([&C, &selected](const std::string& user) { @@ -230,7 +231,7 @@ void ServerConn::handle_CONNECTION_VALIDATION() auto ctx = bufferevent_openssl_get_ssl(bev.get()); assert(ctx); auto cert = SSL_get0_peer_certificate(ctx); - // SSL_get_peer_cert_chain() + // SSL_get0_verified_chain() if(cert) { auto subj = X509_get_subject_name(cert); char name[64]; diff --git a/test/testtls.cpp b/test/testtls.cpp index 0f4152ab..60d3537c 100644 --- a/test/testtls.cpp +++ b/test/testtls.cpp @@ -135,7 +135,7 @@ void testClientReconfig() { testShow()<<__func__; auto serv_conf(server::Config::isolated()); - serv_conf.tls_keychain_file = "../O.Common/server1.p12"; + serv_conf.tls_keychain_file = "../O.Common/ioc1.p12"; auto serv(serv_conf.build() .addSource("whoami", std::make_shared())); @@ -149,13 +149,24 @@ void testClientReconfig() { epicsEvent evt; auto sub(cli.monitor("whoami") - .maskConnected(true) + .maskConnected(false) .maskDisconnected(false) .event([&evt](client::Subscription&) { evt.signal(); }).exec()); Value update; + try { + pop(sub, evt); + testFail("Unexpected success"); + testSkip(2, "oops"); + } catch(client::Connected& e) { + testTrue(e.cred->isTLS); + testEq(e.cred->method, "x509"); + testEq(e.cred->account, "ioc1"); + } + testDiag("Connect"); + update = pop(sub, evt); testEq(update["value"].as(), "x509/client1"); @@ -169,20 +180,90 @@ void testClientReconfig() { }); testDiag("Disconnect"); + testThrows([&sub, &evt]{ + pop(sub, evt); + }); + testDiag("Reconnect"); + update = pop(sub, evt); testEq(update["value"].as(), "x509/client2"); } +void testServerReconfig() { + testShow()<<__func__; + + auto serv_conf(server::Config::isolated()); + serv_conf.tls_keychain_file = "../O.Common/server1.p12"; + + auto serv(serv_conf.build() + .addSource("whoami", std::make_shared())); + + auto cli_conf(serv.clientConfig()); + cli_conf.tls_keychain_file = "../O.Common/ioc1.p12"; + + auto cli(cli_conf.build()); + + serv.start(); + + epicsEvent evt; + auto sub(cli.monitor("whoami") + .maskConnected(false) + .maskDisconnected(false) + .event([&evt](client::Subscription&) { + evt.signal(); + }).exec()); + Value update; + + try { + pop(sub, evt); + testFail("Unexpected success"); + testSkip(2, "oops"); + } catch(client::Connected& e) { + testTrue(e.cred->isTLS); + testEq(e.cred->method, "x509"); + testEq(e.cred->account, "server1"); + } + testDiag("Connect"); + + update = pop(sub, evt); + testEq(update["value"].as(), "x509/ioc1"); + + serv_conf = serv.config(); + serv_conf.tls_keychain_file = "../O.Common/ioc1.p12"; + testDiag("serv.reconfigure()"); + serv.reconfigure(serv_conf); + + testThrows([&sub, &evt]{ + pop(sub, evt); + }); + testDiag("Disconnect"); + + try { + pop(sub, evt); + testFail("Unexpected success"); + testSkip(2, "oops"); + } catch(client::Connected& e) { + testTrue(e.cred->isTLS); + testEq(e.cred->method, "x509"); + testEq(e.cred->account, "ioc1"); + } + testDiag("Reconnect"); + + update = pop(sub, evt); + testEq(update["value"].as(), "x509/ioc1"); +} + } // namespace MAIN(testtls) { - testPlan(0); + testPlan(18); testSetup(); logger_config_env(); testGetSuper(); testGetIntermediate(); testClientReconfig(); + testServerReconfig(); cleanup_for_valgrind(); return testDone(); }