From 6d533b1ae1615632ecb02081feb383096713aadb Mon Sep 17 00:00:00 2001 From: Tobias Blomberg Date: Thu, 11 Jul 2024 14:31:21 +0200 Subject: [PATCH] Add X.509 certificate authentication to reflector Support for X.509 certificate handling and TLS encryption has been added to SvxLink/Async. That is used to authenticate as well as encrypt connections to the SvxReflector server. The TCP connection is encrypted using TLS and the UDP connection is encrypted using a custom shared key scheme. The cipher used by default is AES128 GCM. The implementation is based on OpenSSL. Both the server and the client requires authentication. The AUTH_KEY authentication method that were previously used is deprecated. The certificate generation process is mostly automated. The contents of the certificate can be customized using the configuration variables with name prefix CERT_. Default values are good in most cases. CERT_EMAIL may be good to specify so that the SvxLink node owner can be contacted. Ask the reflector sysop what the convention is. The reflector server need to be updated to use this version of SvxLink on a reflector client. If the server is older and only support the version 2 protocol, you must use TYPE=ReflectorV2 in the ReflectorLogic config. All PKI (Public Key Infrastructure) files will be stored in the directory given by the CERT_PKI_DIR configuration variable. The path defaults to something like /var/lib/svxlink/pki but the exact default path depend on build configuration. In any case, that path need to be created and made writable for SvxLink. That should be done automatically by "make install". Signing client certificates are done on the reflector server via a PTY command (e.g. echo CA SIGN SM0XYZ >/dev/shm/reflector_ctrl). There are more subcommands for listing and removing certificates/CSRs but those have not been fully implemented yet. However, those operations can be performed using standard OS utilities like 'ls' and 'rm' within the PKI subdirectory. In short, this is how to implement the new authentication scheme in a reflector network: - Update the reflector server - Ensure that COMMAND_PTY is set up in the reflector config - Set up SERVER_CERT/COMMON_NAME to be the public hostname of the reflector server - When the reflector server start it will generate a CA root certificate, an issuing (signing) certificate and a signed server certificate for the reflector server. The root certificate will be used as the CA bundle by default. - Update reflector clients - When the client starts it will generate a private key and a CSR (certificate signing request) - When the client connect to the server it will download the CA bundle and send the CSR to the reflector server - The reflector sysop use the "CA SIGN callsign" command to sign the CSR, creating a certificate - The signed certificate will be sent to the client - The client will reconnect using the signed client certificate - The reflector sysop can now remove the specific user configuration for that client in the reflector. Both older clients and updated clients will be able to connect to the updated reflector. AUTH_KEY will still work for both old and updated clients. Building SvxLink require a new dependency on OpenSSL so the development package for that library need to be installed (e.g. install package libssl-dev if on a Debian based distro). These are some features of the new implementation: - TLS encryption implemented in the Async::TcpConnection class - Subject Alternative Name support - Client check remote host name of reflector server - New classes: Async::Digest, Async::SslKeypair, Async::SslCertSigningReq, Async::SslContext, Async::X509, Async::X509Extensions, Async::X509ExtSubjectAltName - Send signed CA bundle to client if requested - Add SSL to the AsyncHttpServer_demo - Intermediate signing cert support - OpenSSL 1.1.0l compatibility - The reflector server use CN in the cert as callsign - The CSR CN is checked so that it really looks like a callsign - Ensure that the public key is the same for an updated CSR - Create the PKI subdirectory with "make install" - UDP encryption - Reflector server send signed certificate to client - The reflector client autogenerate key and csr And also some other fixes: - Don't show unauthenticated nodes in HTTP /status - Better info message for client side reflector protocol downgrade - Implement more robust protocol version negotiation in reflector - Bugfix in TcpConnection constructor - Support connecting v3 client to v2 server through the new ReflectorV2 logic plugin --- INSTALL.adoc | 1 + README.adoc | 7 +- src/CMakeLists.txt | 5 + src/async/ChangeLog | 4 +- src/async/core/AsyncDigest.cpp | 134 ++ src/async/core/AsyncDigest.h | 489 +++++ src/async/core/AsyncEncryptedUdpSocket.cpp | 447 ++++ src/async/core/AsyncEncryptedUdpSocket.h | 368 ++++ src/async/core/AsyncFramedTcpConnection.cpp | 8 +- src/async/core/AsyncFramedTcpConnection.h | 2 +- src/async/core/AsyncHttpServerConnection.cpp | 6 + src/async/core/AsyncHttpServerConnection.h | 2 +- src/async/core/AsyncSslCertSigningReq.h | 537 +++++ src/async/core/AsyncSslContext.h | 261 +++ src/async/core/AsyncSslKeypair.h | 405 ++++ src/async/core/AsyncSslX509.h | 755 +++++++ .../core/AsyncSslX509ExtSubjectAltName.h | 318 +++ src/async/core/AsyncSslX509Extensions.h | 263 +++ src/async/core/AsyncTcpClient.h | 5 + src/async/core/AsyncTcpClientBase.cpp | 2 + src/async/core/AsyncTcpClientBase.h | 15 + src/async/core/AsyncTcpConnection.cpp | 654 ++++-- src/async/core/AsyncTcpConnection.h | 223 +- src/async/core/AsyncTcpPrioClient.h | 20 +- src/async/core/AsyncTcpPrioClientBase.cpp | 85 +- src/async/core/AsyncTcpPrioClientBase.h | 42 +- src/async/core/AsyncTcpServerBase.cpp | 12 +- src/async/core/AsyncTcpServerBase.h | 18 + src/async/core/AsyncUdpSocket.cpp | 69 +- src/async/core/AsyncUdpSocket.h | 82 +- src/async/core/CMakeLists.txt | 10 +- src/async/demo/AsyncDigest_demo.cpp | 118 ++ src/async/demo/AsyncHttpServer_demo.cpp | 38 + src/async/demo/AsyncSslTcpClient_demo.cpp | 89 + src/async/demo/AsyncSslTcpServer_demo.cpp | 183 ++ src/async/demo/AsyncSslX509_demo.cpp | 121 ++ src/async/demo/AsyncTcpClient_demo.cpp | 13 +- src/async/demo/CMakeLists.txt | 2 + src/config.h.in | 1 + src/doc/man/svxlink.conf.5 | 101 +- src/doc/man/svxreflector.conf.5 | 99 +- src/echolib/EchoLinkDirectory.cpp | 6 +- src/svxlink/ChangeLog | 22 + src/svxlink/modules/frn/QsoFrn.cpp | 12 +- src/svxlink/reflector/CMakeLists.txt | 11 +- src/svxlink/reflector/Reflector.cpp | 1343 +++++++++++- src/svxlink/reflector/Reflector.h | 82 +- src/svxlink/reflector/ReflectorClient.cpp | 593 +++++- src/svxlink/reflector/ReflectorClient.h | 126 +- src/svxlink/reflector/ReflectorMsg.h | 480 ++++- src/svxlink/reflector/svxreflector-ca | 300 +++ src/svxlink/reflector/svxreflector.conf | 42 + src/svxlink/reflector/svxreflector.cpp | 24 +- src/svxlink/svxlink/CMakeLists.txt | 11 +- src/svxlink/svxlink/ReflectorLogic.cpp | 1009 ++++++++- src/svxlink/svxlink/ReflectorLogic.h | 58 +- src/svxlink/svxlink/ReflectorV2Logic.cpp | 1856 +++++++++++++++++ src/svxlink/svxlink/ReflectorV2Logic.h | 317 +++ src/svxlink/svxlink/svxlink.conf.in | 18 +- src/template.h | 2 +- src/valgrind.supp | 9 + src/versions | 6 +- 62 files changed, 11703 insertions(+), 638 deletions(-) create mode 100644 src/async/core/AsyncDigest.cpp create mode 100644 src/async/core/AsyncDigest.h create mode 100644 src/async/core/AsyncEncryptedUdpSocket.cpp create mode 100644 src/async/core/AsyncEncryptedUdpSocket.h create mode 100644 src/async/core/AsyncSslCertSigningReq.h create mode 100644 src/async/core/AsyncSslContext.h create mode 100644 src/async/core/AsyncSslKeypair.h create mode 100644 src/async/core/AsyncSslX509.h create mode 100644 src/async/core/AsyncSslX509ExtSubjectAltName.h create mode 100644 src/async/core/AsyncSslX509Extensions.h create mode 100644 src/async/demo/AsyncDigest_demo.cpp create mode 100644 src/async/demo/AsyncSslTcpClient_demo.cpp create mode 100644 src/async/demo/AsyncSslTcpServer_demo.cpp create mode 100644 src/async/demo/AsyncSslX509_demo.cpp create mode 100755 src/svxlink/reflector/svxreflector-ca create mode 100644 src/svxlink/svxlink/ReflectorV2Logic.cpp create mode 100644 src/svxlink/svxlink/ReflectorV2Logic.h diff --git a/INSTALL.adoc b/INSTALL.adoc index 289718421..63e88fdab 100644 --- a/INSTALL.adoc +++ b/INSTALL.adoc @@ -31,6 +31,7 @@ or "-devel". * *librtlsdr*: Support for RTL2832U DVB-T/SDR USB dongles (Optional) * *libgpiod*: More modern approach for GPIO support (Optional) * *libqt*: Version 4. Framework for graphical applications (Optional) +* *libssl*: OpenSSL Cryptography and SSL/TLS Toolkit There also are some runtime dependencies which normally is needed to run a SvxLink system. diff --git a/README.adoc b/README.adoc index 49732770b..7e5511512 100644 --- a/README.adoc +++ b/README.adoc @@ -26,6 +26,7 @@ either C++ or TCL. Examples of modules are: * *SelCall* -- Send selective calling sequences by entering DTMF codes * *MetarInformation* -- Play airport weather information * *Frn* -- Connect to Free Radio Network (FRN) servers +* *Trx* -- Remote control tranceivers using DTMF == Qtel == Qtel, the Qt EchoLink client, is a graphical application used to access the @@ -39,9 +40,8 @@ These are some of the resources connected to SvxLink: :gh_issues: https://github.com/sm0svx/svxlink/issues :gh_releases: https://github.com/sm0svx/svxlink/releases :gh_sndclips: https://github.com/sm0svx/svxlink-sounds-en_US-heather/releases -:sf_lists: http://sourceforge.net/p/svxlink/mailman :gh_main: https://github.com/sm0svx/svxlink -:sf_summary: https://sourceforge.net/projects/svxlink +:gi_svxlink: https://groups.io/g/svxlink * {gh_pages}[Project Home Page] -- The main project page * {gh_wiki}[Wiki Pages] -- Main documentation @@ -49,6 +49,5 @@ These are some of the resources connected to SvxLink: * {gh_releases}[Download Releases] -- Download source code releases here * {gh_sndclips}[Download Sound Clips] -- Download English sound clip files for SvxLink Server from here -* {sf_lists}[Mailing Lists] -- Communicate with other SvxLink users * {gh_main}[GitHub Main Page] -- The project site on GitHub -* {sf_summary}[The SvxLink SourcForge Site] -- Old project site +* {gi_svxlink}[Groups.io SvxLink] -- Communicate with other SvxLink users diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ffe571a22..06e65e69d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -161,6 +161,11 @@ if(NOT DEFINED SVX_SPOOL_INSTALL_DIR) set(SVX_SPOOL_INSTALL_DIR ${LOCAL_STATE_DIR}/spool/svxlink) endif(NOT DEFINED SVX_SPOOL_INSTALL_DIR) +# Where to put SvxLink variable files +if(NOT DEFINED SVX_LOCAL_STATE_DIR) + set(SVX_LOCAL_STATE_DIR ${LOCAL_STATE_DIR}/lib/svxlink) +endif(NOT DEFINED SVX_LOCAL_STATE_DIR) + # Where to install SvxLink architecture independent files if(NOT DEFINED SVX_SHARE_INSTALL_DIR) set(SVX_SHARE_INSTALL_DIR ${SHARE_INSTALL_PREFIX}/svxlink) diff --git a/src/async/ChangeLog b/src/async/ChangeLog index 2364c09a4..27b5799fc 100644 --- a/src/async/ChangeLog +++ b/src/async/ChangeLog @@ -1,8 +1,10 @@ - 1.7.1 -- ?? ??? ???? + 1.8.0 -- ?? ??? ???? ---------------------- * Code cleanup of Async::Pty, fixing a small memory leak. +* TLS connection support added. Build require OpenSSL development files. + 1.7.0 -- 25 Feb 2024 diff --git a/src/async/core/AsyncDigest.cpp b/src/async/core/AsyncDigest.cpp new file mode 100644 index 000000000..7c1c5e1e2 --- /dev/null +++ b/src/async/core/AsyncDigest.cpp @@ -0,0 +1,134 @@ +/** +@file MyNamespaceTemplate.cpp +@brief A_brief_description_for_this_file +@author Tobias Blomberg / SM0SVX +@date 2024- + +A_detailed_description_for_this_file + +\verbatim + +Copyright (C) 2003-2024 Tobias Blomberg / SM0SVX + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +\endverbatim +*/ + +/**************************************************************************** + * + * System Includes + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Project Includes + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Local Includes + * + ****************************************************************************/ + +#include "MyNamespaceTemplate.h" + + +/**************************************************************************** + * + * Namespaces to use + * + ****************************************************************************/ + +using namespace MyNamespace; + + +/**************************************************************************** + * + * Defines & typedefs + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Static class variables + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Local class definitions + * + ****************************************************************************/ + +namespace { + + +/**************************************************************************** + * + * Local functions + * + ****************************************************************************/ + + + +}; /* End of anonymous namespace */ + +/**************************************************************************** + * + * Public member functions + * + ****************************************************************************/ + +Template::Template(void) +{ + +} /* Template::Template */ + + +Template::~Template(void) +{ + +} /* Template::~Template */ + + +/**************************************************************************** + * + * Protected member functions + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Private member functions + * + ****************************************************************************/ + + + +/* + * This file has not been truncated + */ diff --git a/src/async/core/AsyncDigest.h b/src/async/core/AsyncDigest.h new file mode 100644 index 000000000..f92a44f35 --- /dev/null +++ b/src/async/core/AsyncDigest.h @@ -0,0 +1,489 @@ +/** +@file AsyncDigest.h +@brief A_brief_description_for_this_file +@author Tobias Blomberg / SM0SVX +@date 2024-04-27 + +\verbatim + +Copyright (C) 2003-2024 Tobias Blomberg / SM0SVX + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +\endverbatim +*/ + +/** @example AsyncDigest_demo.cpp +An example of how to use the Async::Digest class +*/ + +#ifndef ASYNC_DIGEST_INCLUDED +#define ASYNC_DIGEST_INCLUDED + + +/**************************************************************************** + * + * System Includes + * + ****************************************************************************/ + +#include +#include +#include + +#include + + +/**************************************************************************** + * + * Project Includes + * + ****************************************************************************/ + +#include + + +/**************************************************************************** + * + * Local Includes + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Forward declarations + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Namespace + * + ****************************************************************************/ + +namespace Async +{ + + +/**************************************************************************** + * + * Forward declarations of classes inside of the declared namespace + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Defines & typedefs + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Exported Global Variables + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Class definitions + * + ****************************************************************************/ + +/** +@brief A_brief_class_description +@author Tobias Blomberg / SM0SVX +@date 2024-04-27 + +A_detailed_class_description + +\include AsyncDigest_demo.cpp +*/ +class Digest +{ + public: + using Signature = std::vector; + using MsgDigest = std::vector; + + static bool sigEqual(const Signature& s1, const Signature& s2) + { + return (s1.size() == s2.size()) && + (CRYPTO_memcmp(s1.data(), s2.data(), s1.size()) == 0); + } + + /** + * @brief Default constructor + */ + Digest(void) + { +#if OPENSSL_VERSION_MAJOR < 3 + static bool global_is_initialized = false; + if (!global_is_initialized) + { + //std::cout << "### Digest::Digest: OpenSSL_add_all_digests" + // << std::endl; + OpenSSL_add_all_digests(); + global_is_initialized = true; + } +#endif + m_ctx = EVP_MD_CTX_new(); + if (m_ctx == nullptr) + { + std::cerr << "*** ERROR: EVP_MD_CTX_new failed, error " + << ERR_get_error() << std::endl; + abort(); + } + } + + /** + * @brief Disallow copy construction + */ + Digest(const Digest&) = delete; + + /** + * @brief Disallow copy assignment + */ + Digest& operator=(const Digest&) = delete; + + /** + * @brief Destructor + */ + ~Digest(void) + { + EVP_MD_CTX_free(m_ctx); + m_ctx = nullptr; + } + + /** + * @brief A_brief_member_function_description + * @param param1 Description_of_param1 + * @return Return_value_of_this_member_function + */ + + bool mdInit(const std::string& md_alg) + { + MessageDigest md(md_alg); + if (md == nullptr) + { + std::cerr << "*** ERROR: EVP_MD_fetch failed, error " + << ERR_get_error() << std::endl; + return false; + } + int rc = EVP_DigestInit_ex(m_ctx, md, nullptr); + //EVP_MD_free(md); + if (rc != 1) + { + std::cerr << "*** ERROR: EVP_DigestInit_ex failed, error " + << ERR_get_error() << std::endl; + return false; + } + return true; + } + + bool mdUpdate(const void* d, size_t dlen) + { + int rc = EVP_DigestUpdate(m_ctx, d, dlen); + if (rc != 1) + { + std::cerr << "*** ERROR: EVP_DigestUpdate failed, error " + << ERR_get_error() << std::endl; + return false; + } + return true; + } + + template + bool mdUpdate(const T& d) + { + return mdUpdate(d.data(), d.size()); + } + + bool mdFinal(MsgDigest& md) + { + unsigned int mdlen = EVP_MAX_MD_SIZE; + md.resize(mdlen); + int rc = EVP_DigestFinal_ex(m_ctx, md.data(), &mdlen); + if (rc != 1) + { + std::cerr << "*** ERROR: EVP_DigestFinal_ex failed, error " + << ERR_get_error() << std::endl; + md.clear(); + return false; + } + md.resize(mdlen); + return true; + } + + MsgDigest mdFinal(void) + { + MsgDigest digest; + (void)mdFinal(digest); + return digest; + } + + bool md(MsgDigest& digest, const std::string& md_alg, + const void* d, size_t dlen) + { + return mdInit(md_alg) && mdUpdate(d, dlen) && mdFinal(digest); + } + + template + bool md(MsgDigest& digest, const std::string& md_alg, const T& d) + { + return md(digest, md_alg, d.data(), d.size()); + } + + template + MsgDigest md(const std::string& md_alg, const T& d) + { + MsgDigest digest; + (void)md(digest, md_alg, d); + return digest; + } + + + bool signInit(const std::string& md_alg, SslKeypair& pkey) + { + //EVP_MD* md = EVP_MD_fetch(nullptr, md_alg.c_str(), nullptr); + MessageDigest md(md_alg); + if (md == nullptr) + { + std::cerr << "*** ERROR: EVP_MD_fetch failed, error " + << ERR_get_error() << std::endl; + return false; + } + int rc = EVP_DigestSignInit(m_ctx, NULL, md, NULL, pkey); + //EVP_MD_free(md); + if (rc != 1) + { + std::cerr << "*** ERROR: EVP_DigestSignInit failed, error " + << ERR_get_error() << std::endl; + return false; + } + return true; + } + + bool signUpdate(const void* msg, size_t mlen) + { + int rc = EVP_DigestSignUpdate(m_ctx, + reinterpret_cast(msg), mlen); + if (rc != 1) + { + std::cerr << "*** ERROR: EVP_DigestSignUpdate failed, error " + << ERR_get_error() << std::endl; + return false; + } + return true; + } + + template + bool signUpdate(const T& msg) + { + return signUpdate(msg.data(), msg.size()); + } + + bool signFinal(Signature& sig) + { + sig.clear(); + size_t req = 0; + int rc = EVP_DigestSignFinal(m_ctx, NULL, &req); + if (rc != 1) + { + std::cerr << "*** ERROR: EVP_DigestSignFinal (1) failed , error " + << ERR_get_error() << std::endl; + return false; + } + sig.resize(req); + rc = EVP_DigestSignFinal(m_ctx, sig.data(), &req); + if (rc != 1) + { + std::cerr << "*** ERROR: EVP_DigestSignFinal (2) failed, error " + << ERR_get_error() << std::endl; + sig.clear(); + return false; + } + return true; + } + + Signature signFinal(void) + { + Signature sig; + (void)signFinal(sig); + return sig; + } + + bool sign(Signature& sig, const void* msg, size_t mlen) + { + sig.clear(); + +#if OPENSSL_VERSION_MAJOR >= 3 + size_t siglen = 0; + int rc = EVP_DigestSign(m_ctx, nullptr, &siglen, + reinterpret_cast(msg), mlen); + if (rc != 1) + { + std::cerr << "*** ERROR: EVP_DigestSign (1) failed, error " + << ERR_get_error() << std::endl; + return false; + } + sig.resize(siglen); + rc = EVP_DigestSign(m_ctx, sig.data(), &siglen, + reinterpret_cast(msg), mlen); + if (rc != 1) + { + std::cerr << "*** ERROR: EVP_DigestSign (2) failed, error " + << ERR_get_error() << std::endl; + sig.clear(); + return false; + } + return true; +#else + return signUpdate(msg, mlen) && signFinal(sig); +#endif + } + + template + bool sign(Signature& sig, const T& msg) + { + return sign(sig, msg.data(), msg.size()); + } + + Signature sign(const void* msg, size_t mlen) + { + Signature sig; + (void)sign(sig, msg, mlen); + return sig; + } + + template + Signature sign(const T& msg) + { + return sign(msg.data(), msg.size()); + } + + bool signVerifyInit(const std::string& md_alg, SslKeypair& pkey) + { + assert(!pkey.isNull()); + //EVP_MD* md = EVP_MD_fetch(nullptr, md_alg.c_str(), nullptr); + MessageDigest md(md_alg); + if (md == nullptr) + { + return false; + } + int rc = EVP_DigestVerifyInit(m_ctx, NULL, md, NULL, pkey); + //EVP_MD_free(md); + if (rc != 1) + { + std::cerr << "*** ERROR: EVP_DigestVerifyInit failed, error " + << ERR_get_error() << std::endl; + return false; + } + return true; + } + + bool signVerifyUpdate(const void* msg, size_t mlen) + { + assert((msg != nullptr) && (mlen > 0)); + int rc = EVP_DigestVerifyUpdate(m_ctx, + reinterpret_cast(msg), mlen); + if (rc != 1) + { + std::cerr << "*** ERROR: EVP_DigestVerifyUpdate failed, error " + << ERR_get_error() << std::endl; + return false; + } + return true; + } + + template + bool signVerifyUpdate(const T& msg) + { + return signVerifyUpdate(msg.data(), msg.size()); + } + + bool signVerifyFinal(const Signature& sig) + { + int rc = EVP_DigestVerifyFinal(m_ctx, sig.data(), sig.size()); + return (rc == 1); + } + + bool signVerify(const Signature& sig, const void* msg, size_t mlen) + { +#if OPENSSL_VERSION_MAJOR >= 3 + int rc = EVP_DigestVerify(m_ctx, sig.data(), sig.size(), + reinterpret_cast(msg), mlen); + return (rc == 1); +#else + return signVerifyUpdate(msg, mlen) && signVerifyFinal(sig); +#endif + } + + template + bool signVerify(const Signature& sig, const T& msg) + { + return signVerify(sig, msg.data(), msg.size()); + } + + protected: + + private: + class MessageDigest + { + public: + MessageDigest(const std::string& md_alg) + { +#if OPENSSL_VERSION_MAJOR >= 3 + m_md = EVP_MD_fetch(nullptr, md_alg.c_str(), nullptr); +#else + m_md = EVP_get_digestbyname(md_alg.c_str()); +#endif + } + ~MessageDigest(void) + { +#if OPENSSL_VERSION_MAJOR >= 3 + EVP_MD_free(m_md); +#endif + m_md = nullptr; + } + operator const EVP_MD*() const { return m_md; } + bool operator==(nullptr_t) const { return (m_md == nullptr); } + private: +#if OPENSSL_VERSION_MAJOR >= 3 + EVP_MD* m_md = nullptr; +#else + const EVP_MD* m_md = nullptr; +#endif + }; + + EVP_MD_CTX* m_ctx = nullptr; + +}; /* class Digest */ + + +} /* namespace Async */ + +#endif /* ASYNC_DIGEST_INCLUDED */ + +/* + * This file has not been truncated + */ diff --git a/src/async/core/AsyncEncryptedUdpSocket.cpp b/src/async/core/AsyncEncryptedUdpSocket.cpp new file mode 100644 index 000000000..f52924b24 --- /dev/null +++ b/src/async/core/AsyncEncryptedUdpSocket.cpp @@ -0,0 +1,447 @@ +/** +@file AsyncEncryptedUdpSocket.cpp +@brief Contains a class for sending encrypted UDP datagrams +@author Tobias Blomberg / SM0SVX +@date 2023-07-23 + +\verbatim +Async - A library for programming event driven applications +Copyright (C) 2003-2023 Tobias Blomberg / SM0SVX + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +\endverbatim +*/ + +/**************************************************************************** + * + * System Includes + * + ****************************************************************************/ + +#include +#include + +#include +#include +#include + + +/**************************************************************************** + * + * Project Includes + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Local Includes + * + ****************************************************************************/ + +#include "AsyncEncryptedUdpSocket.h" + + +/**************************************************************************** + * + * Namespaces to use + * + ****************************************************************************/ + +using namespace Async; + + +/**************************************************************************** + * + * Defines & typedefs + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Static class variables + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Local class definitions + * + ****************************************************************************/ + +namespace { + + +/**************************************************************************** + * + * Local functions + * + ****************************************************************************/ + + + +}; /* End of anonymous namespace */ + +/**************************************************************************** + * + * Public member functions + * + ****************************************************************************/ + +const EncryptedUdpSocket::Cipher* EncryptedUdpSocket::fetchCipher( + const std::string& type) +{ +//#if OPENSSL_VERSION_MAJOR >= 3 +// return EVP_CIPHER_fetch(NULL, type.c_str(), NULL); +//#else + return EVP_get_cipherbyname(type.c_str()); +//#endif +} /* EncryptedUdpSocket::fetchCipher */ + + +void EncryptedUdpSocket::freeCipher(Cipher* cipher) +{ +#if OPENSSL_VERSION_MAJOR >= 3 + EVP_CIPHER_free(cipher); +#endif +} /* EncryptedUdpSocket::freeCipher */ + + +const std::string EncryptedUdpSocket::cipherName( + const EncryptedUdpSocket::Cipher* cipher) +{ + return EVP_CIPHER_name(cipher); +} /* EncryptedUdpSocket::cipherName */ + + +bool EncryptedUdpSocket::randomBytes(std::vector& bytes) +{ + if (bytes.size() == 0) + { + return true; + } + return (RAND_bytes(bytes.data(), bytes.size()) == 1); +} /* EncryptedUdpSocket::randomBytes */ + + +EncryptedUdpSocket::EncryptedUdpSocket(uint16_t local_port, + const IpAddress &bind_ip) + : UdpSocket(local_port, bind_ip) +{ + m_cipher_ctx = EVP_CIPHER_CTX_new(); +} /* EncryptedUdpSocket::EncryptedUdpSocket */ + + +EncryptedUdpSocket::~EncryptedUdpSocket(void) +{ + EVP_CIPHER_CTX_free(m_cipher_ctx); + m_cipher_ctx = nullptr; +} /* EncryptedUdpSocket::~EncryptedUdpSocket */ + + +bool EncryptedUdpSocket::setCipher(const std::string& type) +{ + //std::cout << "### EncryptedUdpSocket::setCipher: type=" << type << std::endl; + return setCipher(fetchCipher(type)); +} /* EncryptedUdpSocket::setCipher */ + + +bool EncryptedUdpSocket::setCipher(const EncryptedUdpSocket::Cipher* cipher) +{ + // Clean up the context, free all memory except the context itself + if (!EVP_CIPHER_CTX_reset(m_cipher_ctx)) + { + std::cout << "### EVP_CIPHER_CTX_reset failed" << std::endl; + return false; + } + + if (cipher == nullptr) + { + return true; + } + + // Set cipher type in the cipher context + if (!EVP_EncryptInit_ex(m_cipher_ctx, cipher, NULL, NULL, NULL) || + !EVP_DecryptInit_ex(m_cipher_ctx, cipher, NULL, NULL, NULL)) + { + std::cout << "### EVP_EncryptInit_ex failed" << std::endl; + return false; + } + + return true; +} /* EncryptedUdpSocket::setCipher */ + + +bool EncryptedUdpSocket::setCipherIV(std::vector iv) +{ + m_cipher_iv = iv; + size_t iv_length = EVP_CIPHER_CTX_iv_length(m_cipher_ctx); + //std::cout << "### EncryptedUdpSocket::setCipherIV: iv_length=" + // << iv_length << " iv.size()=" << iv.size() << std::endl; + return (iv.size() == iv_length); +} /* EncryptedUdpSocket::setCipherIV */ + + +const std::vector EncryptedUdpSocket::cipherIV(void) const +{ + return m_cipher_iv; +} /* EncryptedUdpSocket::cipherIV */ + + +bool EncryptedUdpSocket::setCipherKey(std::vector key) +{ + //std::cout << "### EncryptedUdpSocket::setCipherKey: key.size()=" + // << key.size() << std::endl; + m_cipher_key = key; + size_t key_length = EVP_CIPHER_CTX_key_length(m_cipher_ctx); + return (key.size() == key_length); +} /* EncryptedUdpSocket::setCipherKey */ + + +bool EncryptedUdpSocket::setCipherKey(void) +{ + std::vector cipher_key(EVP_CIPHER_CTX_key_length(m_cipher_ctx)); + //std::cout << "### EncryptedUdpSocket::setCipherKey: cipher_key.size()=" + // << cipher_key.size() << std::endl; + if (!randomBytes(cipher_key)) + { + return false; + } + return setCipherKey(cipher_key); +} /* EncryptedUdpSocket::setCipherKey */ + + +const std::vector EncryptedUdpSocket::cipherKey(void) const +{ + return m_cipher_key; +} /* EncryptedUdpSocket::cipherKey */ + + +bool EncryptedUdpSocket::write(const IpAddress& remote_ip, int remote_port, + const void *buf, int count) +{ + return write(remote_ip, remote_port, nullptr, 0, buf, count); +} /* EncryptedUdpSocket::write */ + + +bool EncryptedUdpSocket::write(const IpAddress& remote_ip, int remote_port, + const void *aad, int aadlen, + const void *buf, int cnt) +{ + //std::cout << "### EncryptedUdpSocket::write: " + // << "aadlen=" << aadlen << " cnt=" << cnt + // << " iv="; + //std::copy(m_cipher_iv.begin(), m_cipher_iv.end(), + // std::ostream_iterator(std::cout << std::hex, " ")); + //std::cout << std::dec << std::endl; + + assert(m_cipher_ctx != nullptr); + assert((aad == nullptr) == (aadlen <= 0)); + + auto inbuf = static_cast(buf); + auto aadbuf = static_cast(aad); + + auto key_length = EVP_CIPHER_CTX_key_length(m_cipher_ctx); + //auto iv_length = EVP_CIPHER_CTX_iv_length(m_cipher_ctx); + //std::cout << "### key_length=" << key_length << std::endl; + //std::cout << "### iv_length=" << iv_length << std::endl; + if (key_length > 0) + { + //OPENSSL_assert(key_length == m_cipher_key.size()); + //OPENSSL_assert(iv_length == m_cipher_iv.size()); + + // Set key and IV in the cipher context + EVP_EncryptInit_ex(m_cipher_ctx, NULL, NULL, m_cipher_key.data(), + m_cipher_iv.data()); + } + + //auto taglen = EVP_CIPHER_CTX_get_tag_length(m_cipher_ctx); + //std::cout << "### taglen=" << m_taglen << std::endl; + + // Allow enough space in output buffer for AAD, tag, encrypted plaintext + // and one additional block + uint8_t outbuf[aadlen + m_taglen + cnt + EVP_MAX_BLOCK_LENGTH]; + auto outbufp = outbuf; + int outlen = 0; + int totoutlen = aadlen + m_taglen; + if (aadlen > 0) + { + std::memcpy(outbufp, aadbuf, aadlen); + if(!EVP_EncryptUpdate(m_cipher_ctx, nullptr, &outlen, aadbuf, aadlen)) + { + std::cout << "### EVP_EncryptUpdate with AAD failed" << std::endl; + ERR_print_errors_fp(stderr); + return false; + } + } + outbufp += aadlen + m_taglen; + + if(!EVP_EncryptUpdate(m_cipher_ctx, outbufp, &outlen, inbuf, cnt)) + { + std::cout << "### EVP_EncryptUpdate failed" << std::endl; + return false; + } + outbufp += outlen; + totoutlen += outlen; + + if(!EVP_EncryptFinal_ex(m_cipher_ctx, outbufp, &outlen)) + { + std::cout << "### EVP_EncryptFinal failed" << std::endl; + return false; + } + totoutlen += outlen; + + if (m_taglen > 0) + { + outbufp = outbuf + aadlen; + if (!EVP_CIPHER_CTX_ctrl(m_cipher_ctx, EVP_CTRL_AEAD_GET_TAG, + m_taglen, outbufp)) + { + std::cout << "### EVP_CIPHER_CTX_ctrl(EVP_CTRL_AEAD_GET_TAG) failed" + << std::endl; + return false; + } + } + + //std::cout << "### EncryptedUdpSocket::write: totoutlen=" << totoutlen + // << " data="; + //std::copy(outbuf, outbuf+totoutlen, + // std::ostream_iterator(std::cout << std::hex, " ")); + //std::cout << std::dec << std::endl; + + return UdpSocket::write(remote_ip, remote_port, outbuf, totoutlen); + +} /* EncryptedUdpSocket::write */ + + +/**************************************************************************** + * + * Protected member functions + * + ****************************************************************************/ + +void EncryptedUdpSocket::onDataReceived(const IpAddress& ip, uint16_t port, + void* buf, int count) +{ + if ((count < 0) || cipherDataReceived(ip, port, buf, count)) + { + return; + } + + assert(m_cipher_ctx != nullptr); + //std::cout << "### EncryptedUdpSocket::onDataReceived: count=" + // << count << " iv="; + //std::copy(m_cipher_iv.begin(), m_cipher_iv.end(), + // std::ostream_iterator(std::cout << std::hex, " ")); + //std::cout << std::dec << std::endl; + + auto inbuf = static_cast(buf); + + /* Allow enough space in output buffer for additional block */ + unsigned char outbuf[count + EVP_MAX_BLOCK_LENGTH]; + + auto key_length = EVP_CIPHER_CTX_key_length(m_cipher_ctx); + //auto iv_length = EVP_CIPHER_CTX_iv_length(m_cipher_ctx); + //std::cout << "### key_length=" << key_length << std::endl; + //std::cout << "### iv_length=" << iv_length << std::endl; + if (key_length > 0) + { + //OPENSSL_assert(key_length == m_cipher_key.size()); + //OPENSSL_assert(iv_length == m_cipher_iv.size()); + + // Set key and IV in the cipher context + EVP_DecryptInit_ex(m_cipher_ctx, NULL, NULL, m_cipher_key.data(), + m_cipher_iv.data()); + } + + int outlen = 0; + void* aad = nullptr; + if (m_aadlen > 0) + { + if (static_cast(count) < m_aadlen) + { + std::cout << "### EncryptedUdpSocket::onDataReceived: count=" << count + << " m_aadlen=" << m_aadlen << std::endl; + return; + } + if(!EVP_DecryptUpdate(m_cipher_ctx, nullptr, &outlen, inbuf, m_aadlen)) + { + std::cout << "### : EVP_DecryptUpdate AAD failed" << std::endl; + return; + } + assert(static_cast(outlen) == m_aadlen); + aad = inbuf; + inbuf += m_aadlen; + count -= m_aadlen; + } + + //auto taglen = EVP_CIPHER_CTX_get_tag_length(m_cipher_ctx); + //std::cout << "### taglen=" << m_taglen << std::endl; + if (m_taglen > 0) + { + if (static_cast(count) < m_taglen) + { + std::cout << "### Required tag does not fit within incoming data" + << std::endl; + return; + } + if (!EVP_CIPHER_CTX_ctrl( + m_cipher_ctx, EVP_CTRL_AEAD_SET_TAG, m_taglen, inbuf)) + { + std::cout << "### EVP_CIPHER_CTX_ctrl(EVP_CTRL_AEAD_SET_TAG) failed" + << std::endl; + return; + } + inbuf += m_taglen; + count -= m_taglen; + } + + if(!EVP_DecryptUpdate(m_cipher_ctx, outbuf, &outlen, inbuf, count)) + { + std::cout << "### EVP_DecryptUpdate failed" << std::endl; + return; + } + + int totoutlen = outlen; + if(!EVP_DecryptFinal_ex(m_cipher_ctx, outbuf+outlen, &outlen)) + { + std::cout << "### EVP_DecryptFinal_ex failed" << std::endl; + return; + } + totoutlen += outlen; + + //std::cout << "### EncryptedUdpSocket::onDataReceived: totoutlen=" + // << totoutlen << std::endl; + + dataReceived(ip, port, aad, outbuf, totoutlen); +} /* EncryptedUdpSocket::onDataReceived */ + + +/**************************************************************************** + * + * Private member functions + * + ****************************************************************************/ + + + +/* + * This file has not been truncated + */ diff --git a/src/async/core/AsyncEncryptedUdpSocket.h b/src/async/core/AsyncEncryptedUdpSocket.h new file mode 100644 index 000000000..e6f06fcc3 --- /dev/null +++ b/src/async/core/AsyncEncryptedUdpSocket.h @@ -0,0 +1,368 @@ +/** +@file AsyncEncryptedUdpSocket.h +@brief Contains a class for sending encrypted UDP datagrams +@author Tobias Blomberg / SM0SVX +@date 2023-07-23 + +\verbatim +Async - A library for programming event driven applications +Copyright (C) 2003-2023 Tobias Blomberg / SM0SVX + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +\endverbatim +*/ + +/** @example AsyncEncryptedUdpSocket_demo.cpp +An example of how to use the Async::EncryptedUdpSocket class +*/ + +#ifndef ASYNC_ENCRYPTED_UDP_SOCKET_INCLUDED +#define ASYNC_ENCRYPTED_UDP_SOCKET_INCLUDED + + +/**************************************************************************** + * + * System Includes + * + ****************************************************************************/ + +#include +#include + + +/**************************************************************************** + * + * Project Includes + * + ****************************************************************************/ + +#include + + +/**************************************************************************** + * + * Local Includes + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Forward declarations + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Namespace + * + ****************************************************************************/ + +namespace Async +{ + + +/**************************************************************************** + * + * Forward declarations of classes inside of the declared namespace + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Defines & typedefs + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Exported Global Variables + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Class definitions + * + ****************************************************************************/ + +/** +@brief A class for sending encrypted UDP datagrams +@author Tobias Blomberg / SM0SVX +@date 2023-07-23 + +Use this class to create a UDP socket that is used for sending and receiving +encrypted UDP datagrams. The available ciphers are the block ciphers provided +by the OpenSSL library, e.g. AES-128-GCM. + +\include AsyncEncryptedUdpSocket_demo.cpp +*/ +class EncryptedUdpSocket : public UdpSocket +{ + public: + using Cipher = EVP_CIPHER; + + /** + * @brief Fetch a named cipher object + * @param name The name of the cipher + * @return Return a pointer to a cipher object + * + * Use this function to fetch a cipher object using its name, e.g. + * AES-128-GCM. The returned object must be freed using the freeCipher + * function if not used with the setCipher function. If the setCipher + * function has been used, the object does not have to be freed. + */ + static const Cipher* fetchCipher(const std::string& name); + + /** + * @brief Free memory for a previously allocated cipher object + * @param cipher The cipher object to free + */ + static void freeCipher(Cipher* cipher); + + /** + * @brief Get the name of a cipher from a cipher object + * @param cipher The cipher object + * @return Returns the name of the cipher + */ + static const std::string cipherName(const Cipher* cipher); + + /** + * @brief Fill a vector with random bytes + * @param bytes The vector to fill + * @return Returns \em true on success + * + * This function will fill the given vector with random bytes. Set the + * vector size to the number of bytes that should be generated. A zero + * length vector is valid and will always return true. + * A cryptographically secure pseudo random generator (CSPRNG) is used to + * generate the bytes. + */ + static bool randomBytes(std::vector& bytes); + + /** + * @brief Constructor + * @param local_port The local UDP port to bind to, 0=ephemeral + * @param bind_ip The local interface (IP) to bind to + */ + EncryptedUdpSocket(uint16_t local_port=0, + const IpAddress &bind_ip=IpAddress()); + + /** + * @brief Disallow copy construction + */ + //EncryptedUdpSocket(const EncryptedUdpSocket&) = delete; + + /** + * @brief Disallow copy assignment + */ + //EncryptedUdpSocket& operator=(const EncryptedUdpSocket&) = delete; + + /** + * @brief Destructor + */ + ~EncryptedUdpSocket(void) override; + + /** + * @brief Check if the initialization was ok + * @return Returns \em true if everything went fine during initialization + * or \em false if something went wrong + * + * This function should always be called after constructing the object to + * see if everything went fine. + */ + bool initOk(void) const override + { + return UdpSocket::initOk() && (m_cipher_ctx != nullptr); + } + + /** + * @brief Set which cipher algorithm type to use + * @param type The algorithm type + * @return Return \em true on success + * + * This function must be called before sending or receiving any datagrams. + * Use this function to set which block cipher algorithm to use, e.g. + * AES-128-GCM, ChaCha20, NULL. + */ + bool setCipher(const std::string& type); + + /** + * @brief Set which cipher algorithm type to use + * @param cipher A pre-created cipher object + * @return Return \em true on success + * + * The setCipher function must be called before sending or receiving any + * datagrams. Use this function to set which block cipher algorithm to use. + */ + bool setCipher(const Cipher* cipher); + + /** + * @brief Set the initialization vector to use with the cipher + * @param iv The initialization vector + * @return Returns \em true on success + * + * This function will set the initialization vector (IV) to use with the + * selected cipher. Different ciphers require different IVs. Find and read + * the requirements for a specific cipher for constructing a safe IV. + * The setCipher function must be called before calling this function. + */ + bool setCipherIV(std::vector iv); + + /** + * @brief Get a previously set initialization vector (IV) + * @return Returns the IV or an empty vector if not set + */ + const std::vector cipherIV(void) const; + + /** + * @brief Set the cipher key to use + * @param key The cipher key + * @return Returns \em true on success + * + * This function will set the key to use with the selected cipher. + * Different ciphers require different keys. Find and read the requirements + * for a specific cipher for constructing a key. The setCipher function + * must be called before calling this function. + */ + bool setCipherKey(std::vector key); + + /** + * @brief Set a random cipher key to use + * @return Returns \em true on success + * + * This function will set a random key to use with the selected cipher. A + * cryptographically secure pseudo random generator (CSPRNG) is used to + * generate the key. + * The setCipher function must be called before calling this function. + */ + bool setCipherKey(void); + + /** + * @brief Get the currently set cipher key + * @return Returns the key or an empty vector if the key is not set + */ + const std::vector cipherKey(void) const; + + /** + * @brief Set the length of the AEAD tag + * @param taglen The length of the tag in bytes + * + * Some ciphers, like AES-128-GCM, support AEAD (Authenticated Encryption + * with Associated Data). A tag is then sent with the encrypted data to + * authenticate the sender of the data. The tag can have differing lengths + * for different applications and different levels of security. + */ + void setTagLength(int taglen) { m_taglen = taglen; } + + /** + * @brief Get the currently set up tag length + * @return Returns the tag length + */ + int tagLength(void) const { return m_taglen; } + + /** + * @brief Set the length of the associated data for AEAD ciphers + * @param aadlen The length of the additional associated data + * + * Some ciphers, like AES-128-GCM, support AEAD (Authenticated Encryption + * with Associated Data). A tag is then sent with the encrypted data to + * authenticate the sender of the data. Associated data, which is not + * encypted, can be sent along with the encrypted data. The associated data + * will be protected by the authentication present in AEAD ciphers if a tag + * is sent along with the encrypted data (@see setTagLength). + */ + void setCipherAADLength(int aadlen) { m_aadlen = aadlen; } + + /** + * @brief The currently set up length of the additional associated data + * @return Returns the length of the associated data + */ + size_t cipherAADLength(void) const { return m_aadlen; } + + /** + * @brief Write data to the remote host + * @param remote_ip The IP-address of the remote host + * @param remote_port The remote port to use + * @param buf A buffer containing the data to send + * @param count The number of bytes to write + * @return Return \em true on success or \em false on failure + */ + bool write(const IpAddress& remote_ip, int remote_port, + const void *buf, int count) override; + + /** + * @brief Write data to the remote host + * @param remote_ip The IP-address of the remote host + * @param remote_port The remote port to use + * @param aad Prepended unencrypted data + * @param buf A buffer containing the data to send + * @param count The number of bytes to write + * @return Return \em true on success or \em false on failure + */ + bool write(const IpAddress& remote_ip, int remote_port, + const void *aad, int aadlen, const void *buf, int cnt); + + /** + * @brief A signal that is emitted when cipher data has been received + * @param ip The IP-address the data was received from + * @param port The remote port number + * @param buf The buffer containing the read cipher data + * @param count The number of bytes read + */ + sigc::signal cipherDataReceived; + + /** + * @brief A signal that is emitted when cipher data has been decrypted + * @param ip The IP-address the data was received from + * @param port The remote port number + * @param aad Additional Associated Data + * @param buf The buffer containing the read data + * @param count The number of bytes read + */ + sigc::signal dataReceived; + + protected: + void onDataReceived(const IpAddress& ip, uint16_t port, void* buf, + int count) override; + + private: + EVP_CIPHER_CTX* m_cipher_ctx = nullptr; + std::vector m_cipher_iv; + std::vector m_cipher_key; + size_t m_taglen = 0; + size_t m_aadlen = 0; + +}; /* class EncryptedUdpSocket */ + + +} /* namespace Async */ + +#endif /* ASYNC_ENCRYPTED_UDP_SOCKET_INCLUDED */ + +/* + * This file has not been truncated + */ diff --git a/src/async/core/AsyncFramedTcpConnection.cpp b/src/async/core/AsyncFramedTcpConnection.cpp index edd261133..5adc8c773 100644 --- a/src/async/core/AsyncFramedTcpConnection.cpp +++ b/src/async/core/AsyncFramedTcpConnection.cpp @@ -116,8 +116,10 @@ FramedTcpConnection::FramedTcpConnection(size_t recv_buf_len) : TcpConnection(recv_buf_len), m_max_frame_size(DEFAULT_MAX_FRAME_SIZE), m_size_received(false) { +#if 0 TcpConnection::sendBufferFull.connect( sigc::mem_fun(*this, &FramedTcpConnection::onSendBufferFull)); +#endif } /* FramedTcpConnection::FramedTcpConnection */ @@ -127,8 +129,10 @@ FramedTcpConnection::FramedTcpConnection( : TcpConnection(sock, remote_addr, remote_port, recv_buf_len), m_max_frame_size(DEFAULT_MAX_FRAME_SIZE), m_size_received(false) { +#if 0 TcpConnection::sendBufferFull.connect( sigc::mem_fun(*this, &FramedTcpConnection::onSendBufferFull)); +#endif } /* FramedTcpConnection::FramedTcpConnection */ @@ -273,8 +277,8 @@ int FramedTcpConnection::onDataReceived(void *buf, int count) ptr += copy_cnt; if (m_frame.size() == m_frame_size) { - frameReceived(this, m_frame); m_size_received = false; + frameReceived(this, m_frame); } } } @@ -289,6 +293,7 @@ int FramedTcpConnection::onDataReceived(void *buf, int count) * ****************************************************************************/ +#if 0 void FramedTcpConnection::onSendBufferFull(bool is_full) { //cout << "### FramedTcpConnection::onSendBufferFull: is_full=" @@ -315,6 +320,7 @@ void FramedTcpConnection::onSendBufferFull(bool is_full) } } } /* FramedTcpConnection::onSendBufferFull */ +#endif void FramedTcpConnection::disconnectCleanup(void) diff --git a/src/async/core/AsyncFramedTcpConnection.h b/src/async/core/AsyncFramedTcpConnection.h index 28476906b..ea8af1845 100644 --- a/src/async/core/AsyncFramedTcpConnection.h +++ b/src/async/core/AsyncFramedTcpConnection.h @@ -285,7 +285,7 @@ class FramedTcpConnection : public TcpConnection TxQueue m_txq; FramedTcpConnection(const FramedTcpConnection&) = delete; - void onSendBufferFull(bool is_full); + //void onSendBufferFull(bool is_full); void disconnectCleanup(void); }; /* class FramedTcpConnection */ diff --git a/src/async/core/AsyncHttpServerConnection.cpp b/src/async/core/AsyncHttpServerConnection.cpp index e02bdef2b..d8092c7fa 100644 --- a/src/async/core/AsyncHttpServerConnection.cpp +++ b/src/async/core/AsyncHttpServerConnection.cpp @@ -116,8 +116,10 @@ HttpServerConnection::HttpServerConnection(size_t recv_buf_len) : TcpConnection(recv_buf_len), m_state(STATE_DISCONNECTED), m_chunked(false) { +#if 0 TcpConnection::sendBufferFull.connect( sigc::mem_fun(*this, &HttpServerConnection::onSendBufferFull)); +#endif } /* HttpServerConnection::HttpServerConnection */ @@ -127,8 +129,10 @@ HttpServerConnection::HttpServerConnection( : TcpConnection(sock, remote_addr, remote_port, recv_buf_len), m_state(STATE_EXPECT_START_LINE), m_chunked(false) { +#if 0 TcpConnection::sendBufferFull.connect( sigc::mem_fun(*this, &HttpServerConnection::onSendBufferFull)); +#endif } /* HttpServerConnection::HttpServerConnection */ @@ -413,6 +417,7 @@ void HttpServerConnection::handleHeader(void) } /* HttpServerConnection::handleHeader */ +#if 0 void HttpServerConnection::onSendBufferFull(bool is_full) { //cout << "### HttpServerConnection::onSendBufferFull: is_full=" @@ -439,6 +444,7 @@ void HttpServerConnection::onSendBufferFull(bool is_full) // } //} } /* HttpServerConnection::onSendBufferFull */ +#endif void HttpServerConnection::disconnectCleanup(void) diff --git a/src/async/core/AsyncHttpServerConnection.h b/src/async/core/AsyncHttpServerConnection.h index 72e44fe92..0bf9775ce 100644 --- a/src/async/core/AsyncHttpServerConnection.h +++ b/src/async/core/AsyncHttpServerConnection.h @@ -349,7 +349,7 @@ class HttpServerConnection : public TcpConnection using TcpConnection::write; void handleStartLine(void); void handleHeader(void); - void onSendBufferFull(bool is_full); + //void onSendBufferFull(bool is_full); void disconnectCleanup(void); const char* codeToString(unsigned code); diff --git a/src/async/core/AsyncSslCertSigningReq.h b/src/async/core/AsyncSslCertSigningReq.h new file mode 100644 index 000000000..4df51311f --- /dev/null +++ b/src/async/core/AsyncSslCertSigningReq.h @@ -0,0 +1,537 @@ +/** +@file AsyncCertSigningReq.h +@brief A_brief_description_for_this_file +@author Tobias Blomberg / SM0SVX +@date 2020-08-03 + +\verbatim + +Copyright (C) 2003-2024 Tobias Blomberg / SM0SVX + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +\endverbatim +*/ + +/** @example AsyncSslCertSigningReq_demo.cpp +An example of how to use the SslCertSigningReq class +*/ + +#ifndef ASYNC_CERT_SIGNING_REQ_INCLUDED +#define ASYNC_CERT_SIGNING_REQ_INCLUDED + + +/**************************************************************************** + * + * System Includes + * + ****************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include + + +/**************************************************************************** + * + * Project Includes + * + ****************************************************************************/ + +#include +#include + + +/**************************************************************************** + * + * Local Includes + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Forward declarations + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Namespace + * + ****************************************************************************/ + +namespace Async +{ + + +/**************************************************************************** + * + * Forward declarations of classes inside of the declared namespace + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Defines & typedefs + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Exported Global Variables + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Class definitions + * + ****************************************************************************/ + +/** +@brief A_brief_class_description +@author Tobias Blomberg / SM0SVX +@date 2020-08-03 + +A_detailed_class_description + +\include AsyncSslCertSigningReq_demo.cpp +*/ +class SslCertSigningReq +{ + public: + enum : long + { + VERSION_1 = 0 + }; + + /** + * @brief Default constructor + */ + SslCertSigningReq(void) + { + m_req = X509_REQ_new(); + assert(m_req != nullptr); + } + + /** + * @brief Constructor using existing X509_REQ + * @param req An existing X509_REQ + * + * This object will take ownership of the X509_REQ and so it will be freed + * at the destruction of this object. + */ + SslCertSigningReq(X509_REQ* req) : m_req(req) {} + + /** + * @brief Move constructor + * @param other The other object to move data from + */ + SslCertSigningReq(SslCertSigningReq&& other) + : m_req(other.m_req), m_file_path(other.m_file_path) + { + other.m_req = nullptr; + other.m_file_path.clear(); + } + + /** + * @brief Copy constructor + * @param other The other object to copy data from + */ + SslCertSigningReq(SslCertSigningReq& other) + { +#if OPENSSL_VERSION_MAJOR >= 3 + m_req = X509_REQ_dup(other); +#else + m_req = X509_REQ_dup(const_cast(other.m_req)); +#endif + m_file_path = other.m_file_path; + } + + /** + * @brief Constructor taking PEM data + * @param pem The PEM data to parse into a CSR object + */ + //SslCertSigningReq(const std::string& pem) + //{ + // readPem(pem); + //} + + /** + * @brief Destructor + */ + ~SslCertSigningReq(void) + { + if (m_req != nullptr) + { + X509_REQ_free(m_req); + m_req = nullptr; + } + m_file_path.clear(); + } + + + /** + * @brief A_brief_member_function_description + * @param param1 Description_of_param1 + * @return Return_value_of_this_member_function + */ + SslCertSigningReq& operator=(SslCertSigningReq& other) + { +#if OPENSSL_VERSION_MAJOR >= 3 + m_req = X509_REQ_dup(other); +#else + m_req = X509_REQ_dup(const_cast(other.m_req)); +#endif + m_file_path = other.m_file_path; + return *this; + } + + SslCertSigningReq& operator=(SslCertSigningReq&& other) + { + m_req = other.m_req; + m_file_path = other.m_file_path; + other.m_req = nullptr; + other.m_file_path.clear(); + return *this; + } + + operator const X509_REQ*() const { return m_req; } + + void set(X509_REQ* req) + { + if (m_req != nullptr) + { + X509_REQ_free(m_req); + } + m_req = req; + } + + void clear(void) + { + if (m_req != nullptr) + { + X509_REQ_free(m_req); + } + m_req = X509_REQ_new(); + } + + bool isNull(void) const { return (m_req == nullptr); } + + bool setVersion(long version) + { + assert(m_req != nullptr); + return (X509_REQ_set_version(m_req, version) == 1); + } + + bool addSubjectName(const std::string& field, const std::string& value) + { + assert(m_req != nullptr); + X509_NAME* name = X509_REQ_get_subject_name(m_req); + if (name == nullptr) + { + name = X509_NAME_new(); + } + assert(name != nullptr); + bool success = (X509_NAME_add_entry_by_txt(name, field.c_str(), + MBSTRING_UTF8, + reinterpret_cast(value.c_str()), + value.size(), -1, 0) == 1); + success = success && (X509_REQ_set_subject_name(m_req, name) == 1); + return success; + } + + bool setSubjectName(X509_NAME* name) + { + assert(m_req != nullptr); + return X509_REQ_set_subject_name(m_req, name); + } + + const X509_NAME* subjectName(void) const + { + if (m_req == nullptr) return nullptr; + return X509_REQ_get_subject_name(m_req); + } + + std::vector subjectDigest(void) const + { + std::vector md; + auto subj = subjectName(); + if (subj != nullptr) + { + auto mdtype = EVP_sha256(); + //unsigned int mdlen = EVP_MD_meth_get_result_size(mdtype); + //unsigned int mdlen = EVP_MD_get_size(mdtype); + unsigned int mdlen = EVP_MD_size(mdtype); + md.resize(mdlen); + if (X509_NAME_digest(subj, mdtype, md.data(), &mdlen) != 1) + { + md.resize(0); + } + } + return md; + } + + std::string subjectNameString(void) const + { + std::string str; + const X509_NAME* nm = subjectName(); + if (nm != nullptr) + { + BIO *mem = BIO_new(BIO_s_mem()); + assert(mem != nullptr); + // Print all subject names on one line. Don't escape multibyte chars. + int len = X509_NAME_print_ex(mem, nm, 0, + XN_FLAG_ONELINE & ~ASN1_STRFLGS_ESC_MSB); + if (len > 0) + { + char buf[len+1]; + len = BIO_read(mem, buf, sizeof(buf)); + if (len > 0) + { + str = std::string(buf, len); + } + } + BIO_free(mem); + } + return str; + } + + std::string commonName(void) const + { + std::string cn; + + const auto subj = subjectName(); + if (subj == nullptr) + { + return cn; + } + + // Assume there is only one CN +#if OPENSSL_VERSION_MAJOR >= 3 + int lastpos = X509_NAME_get_index_by_NID(subj, NID_commonName, -1); +#else + auto s = X509_NAME_dup(const_cast(subj)); + int lastpos = X509_NAME_get_index_by_NID(s, NID_commonName, -1); + X509_NAME_free(s); +#endif + //int lastpos = X509_NAME_get_index_by_NID(subj, NID_commonName, -1); + if (lastpos >= 0) + { + X509_NAME_ENTRY *e = X509_NAME_get_entry(subj, lastpos); + ASN1_STRING *d = X509_NAME_ENTRY_get_data(e); + cn = reinterpret_cast(ASN1_STRING_get0_data(d)); + } + return cn; + } + + void addExtensions(SslX509Extensions& exts) + { + assert(m_req != nullptr); +#if OPENSSL_VERSION_MAJOR >= 3 + X509_REQ_add_extensions(m_req, exts); +#else + auto e = sk_X509_EXTENSION_dup(exts); + X509_REQ_add_extensions(m_req, e); + sk_X509_EXTENSION_free(e); +#endif + } + + SslX509Extensions extensions(void) const + { + assert(m_req != nullptr); + return SslX509Extensions(X509_REQ_get_extensions(m_req)); + } + + SslKeypair publicKey(void) const + { + assert(m_req != nullptr); + return SslKeypair(X509_REQ_get_pubkey(m_req)); + } + + bool setPublicKey(SslKeypair& pubkey) + { + assert(m_req != nullptr); + return (X509_REQ_set_pubkey(m_req, pubkey) == 1); + } + + bool sign(SslKeypair& privkey) + { + assert(m_req != nullptr); + auto md = EVP_sha256(); + //auto md_size = EVP_MD_get_size(md); + auto md_size = EVP_MD_size(md); + return (X509_REQ_sign(m_req, privkey, md) == md_size); + } + + bool verify(SslKeypair& pubkey) const + { + assert(m_req != nullptr); + return (X509_REQ_verify(m_req, pubkey) == 1); + } + + std::vector digest(void) const + { + assert(m_req != nullptr); + std::vector md; + auto mdtype = EVP_sha256(); + //unsigned int mdlen = EVP_MD_get_size(mdtype); + unsigned int mdlen = EVP_MD_size(mdtype); + md.resize(mdlen); + unsigned int len = md.size(); + if (X509_REQ_digest(m_req, mdtype, md.data(), &len) != 1) + { + md.resize(0); + } + return md; + } + + bool readPem(const std::string& pem) + { + BIO *mem = BIO_new(BIO_s_mem()); + BIO_puts(mem, pem.c_str()); + if (m_req != nullptr) + { + X509_REQ_free(m_req); + } + m_req = PEM_read_bio_X509_REQ(mem, nullptr, nullptr, nullptr); + BIO_free(mem); + return (m_req != nullptr); + } + + bool readPemFile(const std::string& filename) + { + m_file_path = filename; + if (m_req != nullptr) + { + X509_REQ_free(m_req); + m_req = nullptr; + } + FILE *p_file = fopen(filename.c_str(), "r"); + if (p_file == nullptr) + { + return false; + } + m_req = PEM_read_X509_REQ(p_file, nullptr, nullptr, nullptr); + fclose(p_file); + return (m_req != nullptr); + } + + const std::string& filePath(void) const { return m_file_path; } + + bool writePemFile(FILE* f) + { + assert(m_req != nullptr); + if (f == nullptr) + { + return false; + } + int ret = PEM_write_X509_REQ(f, m_req); + fclose(f); + return (ret == 1); + } + + bool writePemFile(const std::string& filename) + { + return writePemFile(fopen(filename.c_str(), "w")); + } + + bool appendPemFile(const std::string& filename) + { + return writePemFile(fopen(filename.c_str(), "a")); + } + + std::string pem(void) const + { + assert(m_req != nullptr); + BIO *mem = BIO_new(BIO_s_mem()); + int ret = PEM_write_bio_X509_REQ(mem, m_req); + assert(ret == 1); + char buf[16384]; + int len = BIO_read(mem, buf, sizeof(buf)); + assert(len > 0); + BIO_free(mem); + return std::string(buf, len); + } + + long version(void) const { return X509_REQ_get_version(m_req); } + + void print(const std::string& prefix="") const + { + //int ext_idx = X509_REQ_get_ext_by_NID(m_req, NID_subject_alt_name, -1); + auto sanstr = extensions().subjectAltName().toString(); + //const auto csr_digest = byteVecToString(digest()); + //const auto subj_digest = byteVecToString(subjectDigest()); + std::cout + //<< prefix << "Version : " << (version()+1) << "\n" + << prefix << "Subject : " << subjectNameString() << "\n" + //<< prefix << "Subject Digest : " << subj_digest << "\n" + ; + if (!sanstr.empty()) + { + std::cout << prefix << "Subject Alt Name : " << sanstr << "\n"; + } + std::cout + //<< prefix << "SHA256 Digest : " << csr_digest << "\n" + << std::flush; + } + + protected: + + private: + X509_REQ* m_req = nullptr; + std::string m_file_path; + + std::string byteVecToString(const std::vector vec) const + { + if (vec.empty()) + { + return std::string(); + } + std::ostringstream oss; + oss << std::hex << std::setfill('0'); + oss << std::setw(2) << static_cast(vec.front()); + std::for_each( + std::next(vec.begin()), + vec.end(), + [&](unsigned char x) + { + oss << ":" << std::setw(2) << static_cast(x); + }); + return oss.str(); + } +}; /* class SslCertSigningReq */ + + +} /* namespace */ + +#endif /* ASYNC_CERT_SIGNING_REQ_INCLUDED */ + +/* + * This file has not been truncated + */ diff --git a/src/async/core/AsyncSslContext.h b/src/async/core/AsyncSslContext.h new file mode 100644 index 000000000..206a28a66 --- /dev/null +++ b/src/async/core/AsyncSslContext.h @@ -0,0 +1,261 @@ +/** +@file AsyncSslContext.h +@brief SSL context meant to be used with TcpConnection and friends +@author Tobias Blomberg / SM0SVX +@date 2020-08-01 + +\verbatim +Async - A library for programming event driven applications +Copyright (C) 2003-2020 Tobias Blomberg / SM0SVX + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +\endverbatim +*/ + +#ifndef ASYNC_SSL_CONTEXT +#define ASYNC_SSL_CONTEXT + + +/**************************************************************************** + * + * System Includes + * + ****************************************************************************/ + +#include +#include +#include + +#include +#include + + +/**************************************************************************** + * + * Project Includes + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Local Includes + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Forward declarations + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Namespace + * + ****************************************************************************/ + +namespace Async +{ + + +/**************************************************************************** + * + * Forward declarations of classes inside of the declared namespace + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Defines & typedefs + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Exported Global Variables + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Class definitions + * + ****************************************************************************/ + +/** +@brief SSL context meant to be used with TcpConnection and friends +@author Tobias Blomberg / SM0SVX +@date 2020-08-01 + +A_detailed_class_description + +\include MyNamespaceTemplate_demo.cpp +*/ +class SslContext +{ + public: + /** + * @brief Default constructor + */ + SslContext(void) + { + initializeGlobals(); + + // Create the SSL server context + //m_ctx = SSL_CTX_new(SSLv23_method()); + m_ctx = SSL_CTX_new(TLS_method()); + assert(m_ctx != nullptr); + + // Recommended to avoid SSLv2 & SSLv3 + //SSL_CTX_set_options(m_ctx, SSL_OP_ALL|SSL_OP_NO_SSLv2|SSL_OP_NO_SSLv3); + SSL_CTX_set_options(m_ctx, SSL_OP_ALL); + SSL_CTX_set_min_proto_version(m_ctx, TLS1_2_VERSION); + + SSL_CTX_set_verify(m_ctx, SSL_VERIFY_PEER, NULL); + + // Set up OpenSSL to look for CA certs in the default locations + SSL_CTX_set_default_verify_paths(m_ctx); + //int SSL_CTX_set_default_verify_dir(SSL_CTX *ctx); + //int SSL_CTX_set_default_verify_file(SSL_CTX *ctx); + } + + /** + * @brief Constructor + * @param ctx Use this existing context + */ + SslContext(SSL_CTX* ctx) : m_ctx(ctx) {} + + /** + * @brief Do not allow copy construction + */ + SslContext(const SslContext&) = delete; + + /** + * @brief Do not allow assignment + */ + SslContext& operator=(const SslContext&) = delete; + + /** + * @brief Destructor + */ + ~SslContext(void) + { + SSL_CTX_free(m_ctx); + m_ctx = nullptr; + } + + /** + * @brief A_brief_member_function_description + * @param param1 Description_of_param1 + * @return Return_value_of_this_member_function + */ + bool setCertificateFiles(const std::string& keyfile, + const std::string& crtfile) + { + if (crtfile.empty() || keyfile.empty()) return false; + + // Load certificate chain and private key files, and check consistency + //if (SSL_CTX_use_certificate_file( + // m_ctx, crtfile.c_str(), SSL_FILETYPE_PEM) != 1) + if (SSL_CTX_use_certificate_chain_file(m_ctx, crtfile.c_str()) != 1) + { + sslPrintErrors("SSL_CTX_use_certificate_chain_file failed"); + return false; + } + + if (SSL_CTX_use_PrivateKey_file( + m_ctx, keyfile.c_str(), SSL_FILETYPE_PEM) != 1) + { + sslPrintErrors("SSL_CTX_use_PrivateKey_file failed"); + return false; + } + + // Make sure that the key and certificate files match + if (SSL_CTX_check_private_key(m_ctx) != 1) + { + sslPrintErrors("SSL_CTX_check_private_key failed"); + return false; + } + //std::cout << "### SslContext::setCertificateFiles: " + // "Certificate and private key loaded and verified" + // << std::endl; + + return true; + } + + bool caCertificateFileIsSet(void) const { return m_cafile_set; } + + bool setCaCertificateFile(const std::string& cafile) + { + int ret = SSL_CTX_load_verify_locations(m_ctx, cafile.c_str(), NULL); + m_cafile_set = (ret == 1); + return m_cafile_set; + } + + void sslPrintErrors(const char* fname) + { + std::cerr << "*** ERROR: OpenSSL failed: "; + ERR_print_errors_fp(stderr); + } /* sslPrintErrors */ + + operator SSL_CTX*(void) { return m_ctx; } + operator const SSL_CTX*(void) const { return m_ctx; } + + protected: + + private: + SSL_CTX* m_ctx = nullptr; + bool m_cafile_set = false; + + static void initializeGlobals(void) + { + static bool is_initialized = false; + if (!is_initialized) + { + SSL_library_init(); +#if OPENSSL_VERSION_NUMBER < 0x10100000L + OpenSSL_add_all_algorithms(); + SSL_load_error_strings(); +#if OPENSSL_VERSION_MAJOR < 3 + ERR_load_BIO_strings(); +#endif + ERR_load_crypto_strings(); +#endif + is_initialized = true; + } + } + +}; /* class SslContext */ + + +} /* namespace */ + +#endif /* ASYNC_SSL_CONTEXT */ + +/* + * This file has not been truncated + */ diff --git a/src/async/core/AsyncSslKeypair.h b/src/async/core/AsyncSslKeypair.h new file mode 100644 index 000000000..4efabbf6b --- /dev/null +++ b/src/async/core/AsyncSslKeypair.h @@ -0,0 +1,405 @@ +/** +@file MyNamespaceTemplate.h +@brief A_brief_description_for_this_file +@author Tobias Blomberg / SM0SVX +@date 2020-08-03 + +A_detailed_description_for_this_file + +\verbatim + +Copyright (C) 2003-2020 Tobias Blomberg / SM0SVX + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +\endverbatim +*/ + +/** @example MyNamespaceTemplate_demo.cpp +An example of how to use the SslKeypair class +*/ + +#ifndef ASYNC_SSL_KEYPAIR_INCLUDED +#define ASYNC_SSL_KEYPAIR_INCLUDED + + +/**************************************************************************** + * + * System Includes + * + ****************************************************************************/ + +#include +#include +#include +#include +#include +#include + +#include +#include + + +/**************************************************************************** + * + * Project Includes + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Local Includes + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Forward declarations + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Namespace + * + ****************************************************************************/ + +namespace Async +{ + + +/**************************************************************************** + * + * Forward declarations of classes inside of the declared namespace + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Defines & typedefs + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Exported Global Variables + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Class definitions + * + ****************************************************************************/ + +/** +@brief A_brief_class_description +@author Tobias Blomberg / SM0SVX +@date 2020- + +A_detailed_class_description + +\include MyNamespaceTemplate_demo.cpp +*/ +class SslKeypair +{ + public: + /** + * @brief Default constructor + */ + SslKeypair(void) {} + + explicit SslKeypair(EVP_PKEY* pkey) + { + m_pkey = pkey; + } + + SslKeypair(SslKeypair&& other) + { + m_pkey = other.m_pkey; + other.m_pkey = nullptr; + } + + SslKeypair(SslKeypair& other) + { + EVP_PKEY_up_ref(other.m_pkey); + m_pkey = other.m_pkey; + } + + SslKeypair& operator=(SslKeypair& other) + { + EVP_PKEY_up_ref(other.m_pkey); + m_pkey = other.m_pkey; + return *this; + } + + /** + * @brief Destructor + */ + ~SslKeypair(void) + { + EVP_PKEY_free(m_pkey); + m_pkey = nullptr; + } + + /** + * @brief A_brief_member_function_description + * @param param1 Description_of_param1 + * @return Return_value_of_this_member_function + */ + bool isNull(void) const { return (m_pkey == nullptr); } + + bool generate(unsigned int bits) + { + EVP_PKEY_free(m_pkey); +#if OPENSSL_VERSION_MAJOR >= 3 + m_pkey = EVP_RSA_gen(bits); + return (m_pkey != nullptr); +#else + m_pkey = EVP_PKEY_new(); + if (m_pkey == nullptr) + { + return false; + } + + BIGNUM* rsa_f4 = BN_new(); + if (rsa_f4 == nullptr) + { + EVP_PKEY_free(m_pkey); + m_pkey = nullptr; + return false; + } + BN_set_word(rsa_f4, RSA_F4); + + // FIXME: Accoring to the manual page we need to seed the random number + // generator. How?! + RSA* rsa = RSA_new(); + if (rsa == nullptr) + { + BN_free(rsa_f4); + EVP_PKEY_free(m_pkey); + m_pkey = nullptr; + return false; + } + int ret = RSA_generate_key_ex( + rsa, /* the RSA object to fill in */ + bits, /* number of bits for the key - 2048 is a sensible value */ + rsa_f4, /* exponent - RSA_F4 is defined as 0x10001L */ + NULL /* callback - can be NULL if we aren't displaying progress */ + ); + if (ret != 1) + { + RSA_free(rsa); + BN_free(rsa_f4); + EVP_PKEY_free(m_pkey); + m_pkey = nullptr; + return false; + } + ret = EVP_PKEY_assign_RSA(m_pkey, rsa); + if (ret != 1) + { + RSA_free(rsa); + BN_free(rsa_f4); + EVP_PKEY_free(m_pkey); + m_pkey = nullptr; + return false; + } + BN_free(rsa_f4); + return true; +#endif + } + + template + bool newRawPrivateKey(int type, const T& key) + { + EVP_PKEY_free(m_pkey); +#if OPENSSL_VERSION_MAJOR >= 3 + m_pkey = EVP_PKEY_new_raw_private_key(type, nullptr, + reinterpret_cast(key.data()), key.size()); +#else + m_pkey = EVP_PKEY_new_mac_key(type, nullptr, + reinterpret_cast(key.data()), key.size()); +#endif + return (m_pkey != nullptr); + } + + std::string privateKeyPem(void) const + { + assert(m_pkey != nullptr); + BIO *mem = BIO_new(BIO_s_mem()); + assert(mem != nullptr); + int ret = PEM_write_bio_PrivateKey( + mem, /* use the FILE* that was opened */ + m_pkey, /* EVP_PKEY structure */ + NULL, /* default cipher for encrypting the key on disk */ + NULL, /* passphrase required for decrypting the key on disk */ + 0, /* length of the passphrase string */ + NULL, /* callback for requesting a password */ + NULL /* data to pass to the callback */ + ); + std::string pem; + if (ret == 1) + { + char buf[16384]; + int len = BIO_read(mem, buf, sizeof(buf)); + assert(len > 0); + pem = std::string(buf, len); + } + BIO_free(mem); + return pem; + } + + bool privateKeyFromPem(const std::string& pem) + { + BIO *mem = BIO_new(BIO_s_mem()); + BIO_puts(mem, pem.c_str()); + if (m_pkey != nullptr) + { + EVP_PKEY_free(m_pkey); + } + m_pkey = PEM_read_bio_PrivateKey(mem, nullptr, nullptr, nullptr); + BIO_free(mem); + return (m_pkey != nullptr); + } + + bool writePrivateKeyFile(const std::string& filename) + { + FILE* f = fopen(filename.c_str(), "wb"); + if (f == nullptr) + { + return false; + } + if (fchmod(fileno(f), 0600) != 0) + { + fclose(f); + return false; + } + int ret = PEM_write_PrivateKey( + f, /* use the FILE* that was opened */ + m_pkey, /* EVP_PKEY structure */ + NULL, /* default cipher for encrypting the key on disk */ + NULL, /* passphrase required for decrypting the key on disk */ + 0, /* length of the passphrase string */ + NULL, /* callback for requesting a password */ + NULL /* data to pass to the callback */ + ); + if (ret != 1) + { + fclose(f); + return false; + } + if (fclose(f) != 0) + { + return false; + } + return true; + } + + bool readPrivateKeyFile(const std::string& filename) + { + FILE* f = fopen(filename.c_str(), "rb"); + if (f == nullptr) + { + return false; + } + EVP_PKEY* pkey = PEM_read_PrivateKey(f, NULL, NULL, NULL); + if (pkey == nullptr) + { + fclose(f); + return false; + } + if (fclose(f) != 0) + { + return false; + } + EVP_PKEY_free(m_pkey); + m_pkey = pkey; + return true; + } + + std::string publicKeyPem(void) const + { + assert(m_pkey != nullptr); + BIO *mem = BIO_new(BIO_s_mem()); + assert(mem != nullptr); + int ret = PEM_write_bio_PUBKEY(mem, m_pkey); + std::string pem; + if (ret == 1) + { + char buf[16384]; + int len = BIO_read(mem, buf, sizeof(buf)); + assert(len > 0); + pem = std::string(buf, len); + } + BIO_free(mem); + return pem; + } + + bool publicKeyFromPem(const std::string& pem) + { + if (m_pkey != nullptr) + { + EVP_PKEY_free(m_pkey); + } + BIO* mem = BIO_new(BIO_s_mem()); + assert(mem != nullptr); + int rc = BIO_puts(mem, pem.c_str()); + std::cout << "### rc=" << rc << std::endl; + if (rc > 0) + { + m_pkey = PEM_read_bio_PUBKEY(mem, nullptr, nullptr, nullptr); + } + BIO_free(mem); + return (m_pkey != nullptr); + } + + operator EVP_PKEY*(void) { return m_pkey; } + operator const EVP_PKEY*(void) const { return m_pkey; } + + bool operator!=(const SslKeypair& other) const + { +#if OPENSSL_VERSION_MAJOR >= 3 + return (EVP_PKEY_eq(m_pkey, other.m_pkey) != 1); +#else + return (EVP_PKEY_cmp(m_pkey, other.m_pkey) != 1); +#endif + } + + protected: + + private: + EVP_PKEY* m_pkey = nullptr; + +}; /* class SslKeypair */ + + +} /* namespace */ + +#endif /* ASYNC_SSL_KEYPAIR_INCLUDED */ + +/* + * This file has not been truncated + */ diff --git a/src/async/core/AsyncSslX509.h b/src/async/core/AsyncSslX509.h new file mode 100644 index 000000000..b629c0e70 --- /dev/null +++ b/src/async/core/AsyncSslX509.h @@ -0,0 +1,755 @@ +/** +@file AsyncSslX509.h +@brief A_brief_description_for_this_file +@author Tobias Blomberg / SM0SVX +@date 2020-08-03 + +\verbatim + +Copyright (C) 2003-2020 Tobias Blomberg / SM0SVX + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +\endverbatim +*/ + +/** @example MyNamespaceTemplate_demo.cpp +An example of how to use the SslX509 class +*/ + +#ifndef ASYNC_SSL_X509_INCLUDED +#define ASYNC_SSL_X509_INCLUDED + + +/**************************************************************************** + * + * System Includes + * + ****************************************************************************/ + +#include +#include + +#include +#include +#include + + +/**************************************************************************** + * + * Project Includes + * + ****************************************************************************/ + +#include +#include + + +/**************************************************************************** + * + * Local Includes + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Forward declarations + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Namespace + * + ****************************************************************************/ + +namespace Async +{ + + +/**************************************************************************** + * + * Forward declarations of classes inside of the declared namespace + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Defines & typedefs + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Exported Global Variables + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Class definitions + * + ****************************************************************************/ + +/** +@brief A_brief_class_description +@author Tobias Blomberg / SM0SVX +@date 2020-08-03 + +A_detailed_class_description + +\include MyNamespaceTemplate_demo.cpp +*/ +class SslX509 +{ + public: + enum : long + { + VERSION_1 = 0, + VERSION_2 = 1, + VERSION_3 = 2 + }; + + /** + * @brief Default constructor + */ + SslX509(void) : m_cert(X509_new()) + { + //std::cout << "### SslX509()" << std::endl; + } + + /** + * @brief Constructor + */ + SslX509(X509* cert, bool managed=true) + : m_cert(cert), m_managed(managed) + { + //std::cout << "### SslX509(X509*)" << std::endl; + } + + /** + * @brief Constructor + */ + explicit SslX509(X509_STORE_CTX& ctx) + : m_cert(X509_STORE_CTX_get_current_cert(&ctx)), m_managed(false) + { + //std::cout << "### SslX509(X509_STORE_CTX&)" << std::endl; + //int ret = X509_up_ref(m_cert); + //assert(ret == 1); + } + + /** + * @brief Move constructor + * @param other The object to move from + */ + SslX509(SslX509&& other) + { + //std::cout << "### SslX509(SslX509&&)" << std::endl; + set(other.m_cert, other.m_managed); + //if (m_managed && (m_cert != nullptr)) + //{ + // X509_free(m_cert); + //} + //m_cert = other.m_cert; + //m_managed = other.m_managed; + other.m_cert = nullptr; + other.m_managed = true; + } + + SslX509& operator=(SslX509&& other) + { + //std::cout << "### SslX509::operator=(SslX509&&)" << std::endl; + set(other.m_cert, other.m_managed); + //if (m_cert != nullptr) + //{ + // X509_free(m_cert); + //} + //m_cert = other.m_cert; + //m_managed = other.m_managed; + other.m_cert = nullptr; + other.m_managed = true; + return *this; + } + + SslX509(const SslX509&) = delete; + + /** + * @brief Constructor taking PEM data + * @param pem The PEM data to parse into a CSR object + */ + //explicit SslX509(const std::string& pem) + //{ + // std::cout << "### SslX509(const std::string&)" << std::endl; + // readPem(pem); + //} + + /** + * @brief Destructor + */ + ~SslX509(void) + { + //std::cout << "### ~SslX509()" << std::endl; + set(nullptr); + } + + void set(X509* cert, bool managed=true) + { + if (m_managed && (m_cert != nullptr)) + { + X509_free(m_cert); + } + m_cert = cert; + m_managed = managed; + } + + void clear(void) + { + if (m_managed && (m_cert != nullptr)) + { + X509_free(m_cert); + } + m_cert = X509_new(); + } + + bool isNull(void) const { return m_cert == nullptr; } + + /** + * @brief A_brief_member_function_description + * @param param1 Description_of_param1 + * @return Return_value_of_this_member_function + */ + SslX509& operator=(const SslX509&) = delete; + + bool setIssuerName(const X509_NAME* name) + { + assert(m_cert != nullptr); +#if OPENSSL_VERSION_MAJOR >= 3 + return (X509_set_issuer_name(m_cert, name) == 1); +#else + auto n = X509_NAME_dup(const_cast(name)); + int ret = X509_set_issuer_name(m_cert, n); + X509_NAME_free(n); + return (ret == 1); +#endif + } + + const X509_NAME* issuerName(void) const + { + assert(m_cert != nullptr); + return X509_get_issuer_name(m_cert); + } + + bool setSubjectName(const X509_NAME* name) + { + assert(m_cert != nullptr); +#if OPENSSL_VERSION_MAJOR >= 3 + return (X509_set_subject_name(m_cert, name) == 1); +#else + auto n = X509_NAME_dup(const_cast(name)); + int ret = X509_set_subject_name(m_cert, n); + X509_NAME_free(n); + return (ret == 1); +#endif + } + + const X509_NAME* subjectName(void) const + { + assert(m_cert != nullptr); + return X509_get_subject_name(m_cert); + } + + operator const X509*(void) const { return m_cert; } + + std::string commonName(void) const + { + std::string cn; + + const X509_NAME* subj = subjectName(); + if (subj == nullptr) + { + return cn; + } + + // Assume there is only one CN +#if OPENSSL_VERSION_MAJOR >= 3 + int lastpos = X509_NAME_get_index_by_NID(subj, NID_commonName, -1); +#else + auto s = X509_NAME_dup(const_cast(subj)); + int lastpos = X509_NAME_get_index_by_NID(s, NID_commonName, -1); + X509_NAME_free(s); +#endif + if (lastpos >= 0) + { + X509_NAME_ENTRY *e = X509_NAME_get_entry(subj, lastpos); + ASN1_STRING *d = X509_NAME_ENTRY_get_data(e); + cn = reinterpret_cast(ASN1_STRING_get0_data(d)); + } + return cn; + } + + bool verify(SslKeypair& keypair) + { + assert(m_cert != nullptr); + return (X509_verify(m_cert, keypair) == 1); + } + + bool readPem(const std::string& pem) + { + BIO *mem = BIO_new(BIO_s_mem()); + BIO_puts(mem, pem.c_str()); + if (m_managed && (m_cert != nullptr)) + { + X509_free(m_cert); + } + m_cert = PEM_read_bio_X509(mem, nullptr, nullptr, nullptr); + BIO_free(mem); + return (m_cert != nullptr); + } + + std::string pem(void) const + { + assert(m_cert != nullptr); + BIO *mem = BIO_new(BIO_s_mem()); + int ret = PEM_write_bio_X509(mem, m_cert); + assert(ret == 1); + char buf[16384]; + int len = BIO_read(mem, buf, sizeof(buf)); + assert(len > 0); + BIO_free(mem); + return std::string(buf, len); + } + + bool readPemFile(const std::string& filename) + { + FILE *p_file = fopen(filename.c_str(), "r"); + if (p_file == nullptr) + { + //std::cerr << "### Failed to open file '" << filename + // << "' for reading certificate" << std::endl; + return false; + } + if (m_managed && (m_cert != nullptr)) + { + X509_free(m_cert); + } + m_cert = PEM_read_X509(p_file, nullptr, nullptr, nullptr); + fclose(p_file); + return (m_cert != nullptr); + } + + bool writePemFile(FILE* f) + { + assert(m_cert != nullptr); + if (f == nullptr) + { + //std::cerr << "### Failed to open file '" << filename + // << "' for writing certificate" << std::endl; + return false; + } + int ret = PEM_write_X509(f, m_cert); + fclose(f); + return (ret == 1); + } + + bool writePemFile(const std::string& filename) + { + return writePemFile(fopen(filename.c_str(), "w")); + } + + bool appendPemFile(const std::string& filename) + { + return writePemFile(fopen(filename.c_str(), "a")); + } + + bool setVersion(long version) + { + return (X509_set_version(m_cert, version) == 1); + } + + long version(void) const { return X509_get_version(m_cert); } + + void setNotBefore(std::time_t in_time) + { + X509_time_adj_ex(X509_get_notBefore(m_cert), 0L, 0L, &in_time); + } + + std::time_t notBefore(void) const + { + ASN1_TIME* epoch = ASN1_TIME_set(nullptr, 0); + const ASN1_TIME* not_before = X509_get0_notBefore(m_cert); + int pday=0, psec=0; + ASN1_TIME_diff(&pday, &psec, epoch, not_before); + ASN1_STRING_free(epoch); + return static_cast(pday)*24*3600 + psec; + } + + std::string notBeforeString(void) const + { + const ASN1_TIME* t = X509_get_notBefore(m_cert); + int len = ASN1_STRING_length(t); + if (t == nullptr) + { + return std::string(); + } + const unsigned char* data = ASN1_STRING_get0_data(t); + return std::string(data, data+len); + } + + std::string notBeforeLocaltimeString(void) const + { + std::time_t t = notBefore(); + std::ostringstream ss; + ss << std::put_time(std::localtime(&t), "%c"); + return ss.str(); + } + + void setNotAfter(std::time_t in_time) + { + X509_time_adj_ex(X509_get_notAfter(m_cert), 0L, 0L, &in_time); + } + + std::time_t notAfter(void) const + { + ASN1_TIME* epoch = ASN1_TIME_set(nullptr, 0); + const ASN1_TIME* not_after = X509_get0_notAfter(m_cert); + int pday=0, psec=0; + ASN1_TIME_diff(&pday, &psec, epoch, not_after); + ASN1_STRING_free(epoch); + return static_cast(pday)*24*3600 + psec; + } + + std::string notAfterString(void) const + { + const ASN1_TIME* t = X509_get_notAfter(m_cert); + const unsigned char* data = ASN1_STRING_get0_data(t); + int len = ASN1_STRING_length(t); + if (t == nullptr) + { + return std::string(); + } + return std::string(data, data+len); + } + + std::string notAfterLocaltimeString(void) const + { + std::time_t t = notAfter(); + std::ostringstream ss; + ss << std::put_time(std::localtime(&t), "%c"); + return ss.str(); + } + + void timeSpan(int& days, int& seconds) const + { + const ASN1_TIME* not_before = X509_get_notBefore(m_cert); + const ASN1_TIME* not_after = X509_get_notAfter(m_cert); + ASN1_TIME_diff(&days, &seconds, not_before, not_after); + } + + bool timeIsWithinRange(std::time_t tbegin=time(NULL), + std::time_t tend=time(NULL)) const + { + const ASN1_TIME* not_before = X509_get_notBefore(m_cert); + const ASN1_TIME* not_after = X509_get_notAfter(m_cert); + return ((not_before == nullptr) || + (X509_cmp_time(not_before, &tbegin) == -1)) && + ((not_after == nullptr) || + (X509_cmp_time(not_after, &tend) == 1)); + } + + int signatureType(void) const + { + return X509_get_signature_type(m_cert); + } + + void setSerialNumber(long serial_number=-1) + { + // FIXME: Error handling + ASN1_INTEGER *p_serial_number = ASN1_INTEGER_new(); + if (serial_number < 0) + { + randSerial(p_serial_number); + } + else + { + ASN1_INTEGER_set(p_serial_number, serial_number); + } + X509_set_serialNumber(m_cert, p_serial_number); + ASN1_INTEGER_free(p_serial_number); + } + + std::string serialNumberString(void) const + { + const ASN1_INTEGER* i = X509_get0_serialNumber(m_cert); + if (i == nullptr) + { + return std::string(); + } + BIGNUM *bn = ASN1_INTEGER_to_BN(i, nullptr); + if (bn == nullptr) + { + return std::string(); + } + char *hex = BN_bn2hex(bn); + BN_free(bn); + if (hex == nullptr) + { + return std::string(); + } + std::string ret(hex, hex+strlen(hex)); + ret = std::string("0x") + ret; + OPENSSL_free(hex); + return ret; + } + + //void setIssuerName() + //{ + // X509_set_issuer_name(p_generated_cert, X509_REQ_get_subject_name(pCertReq)); + //} + + void addIssuerName(const std::string& field, const std::string& value) + { + assert(m_cert != nullptr); + X509_NAME* name = X509_get_issuer_name(m_cert); + if (name == nullptr) + { + name = X509_NAME_new(); + } + assert(name != nullptr); + int ret = X509_NAME_add_entry_by_txt(name, field.c_str(), MBSTRING_UTF8, + reinterpret_cast(value.c_str()), + value.size(), -1, 0); + assert(ret == 1); + ret = X509_set_issuer_name(m_cert, name); + assert(ret == 1); + } + + void addSubjectName(const std::string& field, const std::string& value) + { + assert(m_cert != nullptr); + X509_NAME* name = X509_get_subject_name(m_cert); + if (name == nullptr) + { + name = X509_NAME_new(); + } + assert(name != nullptr); + int ret = X509_NAME_add_entry_by_txt(name, field.c_str(), MBSTRING_UTF8, + reinterpret_cast(value.c_str()), + value.size(), -1, 0); + assert(ret == 1); + ret = X509_set_subject_name(m_cert, name); + assert(ret == 1); + } + + std::string issuerNameString(void) const + { + std::string str; + const X509_NAME* nm = issuerName(); + if (nm != nullptr) + { + BIO *mem = BIO_new(BIO_s_mem()); + assert(mem != nullptr); + // Print all subject names on one line. Don't escape multibyte chars. + int len = X509_NAME_print_ex(mem, nm, 0, + XN_FLAG_ONELINE & ~ASN1_STRFLGS_ESC_MSB); + if (len > 0) + { + char buf[len+1]; + len = BIO_read(mem, buf, sizeof(buf)); + if (len > 0) + { + str = std::string(buf, len); + } + } + BIO_free(mem); + } + return str; + } + + std::string subjectNameString(void) const + { + std::string str; + const X509_NAME* nm = subjectName(); + if (nm != nullptr) + { + BIO *mem = BIO_new(BIO_s_mem()); + assert(mem != nullptr); + // Print all subject names on one line. Don't escape multibyte chars. + int len = X509_NAME_print_ex(mem, nm, 0, + XN_FLAG_ONELINE & ~ASN1_STRFLGS_ESC_MSB); + if (len > 0) + { + char buf[len+1]; + len = BIO_read(mem, buf, sizeof(buf)); + if (len > 0) + { + str = std::string(buf, len); + } + } + BIO_free(mem); + } + return str; + } + + void addExtensions(const SslX509Extensions& exts) + { + for (int i=0; i digest(void) const + { + assert(m_cert != nullptr); + std::vector md(EVP_MAX_MD_SIZE); + unsigned int len = md.size(); + if (X509_digest(m_cert, EVP_sha256(), md.data(), &len) != 1) + { + len = 0; + } + md.resize(len); + return md; + } + +#if 0 + int certificateType(void) const + { + auto pkey = X509_get0_pubkey(m_cert); + if (pkey == nullptr) + { + return -1; + } + return X509_certificate_type(m_cert, pkey); + } +#endif + + bool matchHost(const std::string& name) const + { + int chk = X509_check_host(m_cert, name.c_str(), name.size(), 0, nullptr); + return chk > 0; + } + + bool matchIp(const IpAddress& ip) const + { + int chk = X509_check_ip_asc(m_cert, ip.toString().c_str(), 0); + return chk > 0; + } + + void print(const std::string& prefix="") const + { + if (isNull()) + { + std::cout << "NULL" << std::endl; + return; + } + + int ext_idx = X509_get_ext_by_NID(m_cert, NID_subject_alt_name, -1); + auto ext = X509_get_ext(m_cert, ext_idx); + auto sanstr = SslX509ExtSubjectAltName(ext).toString(); + const auto md = digest(); + std::cout + //<< prefix << "Version : " << (version()+1) << "\n" + << prefix << "Serial No. : " << serialNumberString() << "\n" + << prefix << "Issuer : " << issuerNameString() << "\n" + << prefix << "Subject : " << subjectNameString() << "\n" + << prefix << "Not Before : " + << notBeforeLocaltimeString() << "\n" + << prefix << "Not After : " + << notAfterLocaltimeString() << "\n" + //<< prefix << "Signature Type : " << signatureType() << "\n" + ; + if (!sanstr.empty()) + { + std::cout << prefix << "Subject Alt Name : " << sanstr << "\n"; + } + //std::cout << prefix << "SHA256 Digest : " << std::accumulate( + // md.begin(), + // md.end(), + // std::ostringstream() << std::hex << std::setfill('0'), + // [](std::ostringstream& ss, unsigned char x) + // { + // if (!ss.str().empty()) { ss << ":"; } + // return std::move(ss) << std::setw(2) << unsigned(x); + // }).str() + "\n" + std::cout << std::flush; + } + + protected: + + private: + X509* m_cert = nullptr; + bool m_managed = true; + + int randSerial(ASN1_INTEGER *ai) + { + BIGNUM *p_bignum = NULL; + int ret = -1; + + if (NULL == (p_bignum = BN_new())) { + goto CLEANUP; + } + + if (!BN_rand(p_bignum, 159, 0, 0)) { + goto CLEANUP; + } + + if (ai && !BN_to_ASN1_INTEGER(p_bignum, ai)) { + goto CLEANUP; + } + + ret = 1; + + CLEANUP: + BN_free(p_bignum); + return ret; + } +}; /* class SslX509 */ + + +} /* namespace */ + +#endif /* ASYNC_SSL_X509_INCLUDED */ + +/* + * This file has not been truncated + */ diff --git a/src/async/core/AsyncSslX509ExtSubjectAltName.h b/src/async/core/AsyncSslX509ExtSubjectAltName.h new file mode 100644 index 000000000..698a6f029 --- /dev/null +++ b/src/async/core/AsyncSslX509ExtSubjectAltName.h @@ -0,0 +1,318 @@ +/** +@file AsyncSslExtX509SubjectAltName.h +@brief A_brief_description_for_this_file +@author Tobias Blomberg / SM0SVX +@date 2022-05-27 + +\verbatim + +Copyright (C) 2003-2022 Tobias Blomberg / SM0SVX + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +\endverbatim +*/ + +/** @example AsyncSslExtX509SubjectAltName_demo.cpp +An example of how to use the SslX509ExtSubjectAltName class +*/ + +#ifndef ASYNC_SSL_X509_EXT_SUBJECT_ALT_NAME_INCLUDED +#define ASYNC_SSL_X509_EXT_SUBJECT_ALT_NAME_INCLUDED + + +/**************************************************************************** + * + * System Includes + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Project Includes + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Local Includes + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Forward declarations + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Namespace + * + ****************************************************************************/ + +namespace Async +{ + + +/**************************************************************************** + * + * Forward declarations of classes inside of the declared namespace + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Defines & typedefs + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Exported Global Variables + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Class definitions + * + ****************************************************************************/ + +/** +@brief A_brief_class_description +@author Tobias Blomberg / SM0SVX +@date 2022-05-27 + +A_detailed_class_description + +\include AsyncSslExtX509SubjectAltName_demo.cpp +*/ +class SslX509ExtSubjectAltName +{ + public: + /** + * @brief Default constructor + */ + //SslX509ExtSubjectAltName(void); + + explicit SslX509ExtSubjectAltName(const std::string& names) + { + m_ext = X509V3_EXT_conf_nid(NULL, NULL, NID_subject_alt_name, + names.c_str()); + } + + /** + * @brief Constructor + * @param names An existing X509_EXTENSION object + */ + SslX509ExtSubjectAltName(const X509_EXTENSION* ext) + { +#if OPENSSL_VERSION_MAJOR >= 3 + m_ext = X509_EXTENSION_dup(ext); +#else + m_ext = X509_EXTENSION_dup(const_cast(ext)); +#endif + } + + /** + * @brief Constructor + * @param names An existing GENERAL_NAMES object + */ + explicit SslX509ExtSubjectAltName(GENERAL_NAMES* names) + { + int crit = 0; + m_ext = X509V3_EXT_i2d(NID_subject_alt_name, crit, names); + } + + /** + * @brief Move Constructor + * @param other The object to move from + */ + SslX509ExtSubjectAltName(SslX509ExtSubjectAltName&& other) + { + m_ext = other.m_ext; + other.m_ext = nullptr; + } + + /** + * @brief Disallow copy construction + */ + SslX509ExtSubjectAltName(const SslX509ExtSubjectAltName&) = delete; + + /** + * @brief Disallow copy assignment + */ + SslX509ExtSubjectAltName& operator=(const SslX509ExtSubjectAltName&) + = delete; + + /** + * @brief Destructor + */ + ~SslX509ExtSubjectAltName(void) + { + if (m_ext != nullptr) + { + X509_EXTENSION_free(m_ext); + m_ext = nullptr; + } + } + + /** + * @brief A_brief_member_function_description + * @param param1 Description_of_param1 + * @return Return_value_of_this_member_function + */ + bool isNull(void) const { return m_ext == nullptr; } + +#if 0 + bool add(const std::string& name) + { + auto names = reinterpret_cast(X509V3_EXT_d2i(m_ext)); + if (names == nullptr) + { + names = GENERAL_NAME_new(); + } + auto asn1_str = ASN1_STRING_new(); + ASN1_STRING_set(asn1_str, name.c_str(), name.size()); + // How to create a GENERAL_NAME? + // GENERAL_NAME* general_name = + ASN1_STRING_free(asn1_str); + sk_GENERAL_NAME_push(names, general_name); + } +#endif + + operator const X509_EXTENSION*() const { return m_ext; } + + std::string toString(void) const + { + std::string str; + auto names = reinterpret_cast(X509V3_EXT_d2i(m_ext)); + do + { + if (m_ext == nullptr) + { + break; + } + + int count = sk_GENERAL_NAME_num(names); + if (count == 0) + { + break; + } + + std::string sep; + for (int i = 0; i < count; ++i) + { + GENERAL_NAME* entry = sk_GENERAL_NAME_value(names, i); + if (entry == nullptr) + { + continue; + } + + if (entry->type == GEN_DNS) + { + unsigned char* utf8 = nullptr; + int len = ASN1_STRING_to_UTF8(&utf8, entry->d.dNSName); + if ((utf8 == nullptr) || (len < 1)) + { + break; + } + std::string name(utf8, utf8+len); + OPENSSL_free(utf8); + if (name.empty()) + { + break; + } + str += sep + "DNS:" + name; + } + else if (entry->type == GEN_EMAIL) + { + unsigned char* utf8 = nullptr; + int len = ASN1_STRING_to_UTF8(&utf8, entry->d.rfc822Name); + if ((utf8 == nullptr) || (len < 1)) + { + break; + } + std::string name(utf8, utf8+len); + OPENSSL_free(utf8); + if (name.empty()) + { + break; + } + str += sep + "email:" + name; + } + else if (entry->type == GEN_IPADD) + { + int len = ASN1_STRING_length(entry->d.iPAddress); + if (len == 4) + { + const unsigned char* data = + ASN1_STRING_get0_data(entry->d.iPAddress); + struct in_addr in_addr; + in_addr.s_addr = *reinterpret_cast(data); + Async::IpAddress addr(in_addr); + str += sep + "IP Address:" + addr.toString(); + } + else if (len == 16) + { + // FIXME: IPv6 + continue; + } + else + { + break; + } + } + else + { + continue; + } + + sep = ", "; + } + } while (false); + + GENERAL_NAMES_free(names); + + return str; + } + + private: + X509_EXTENSION* m_ext = nullptr; + +}; /* class SslX509ExtSubjectAltName */ + + +} /* namespace Async */ + +#endif /* ASYNC_SSL_X509_EXT_SUBJECT_ALT_NAME_INCLUDED */ + +/* + * This file has not been truncated + */ diff --git a/src/async/core/AsyncSslX509Extensions.h b/src/async/core/AsyncSslX509Extensions.h new file mode 100644 index 000000000..deb3facf3 --- /dev/null +++ b/src/async/core/AsyncSslX509Extensions.h @@ -0,0 +1,263 @@ +/** +@file AsyncSslX509Extensions.h +@brief A_brief_description_for_this_file +@author Tobias Blomberg / SM0SVX +@date 2022-05-22 + +A_detailed_description_for_this_file + +\verbatim + +Copyright (C) 2003-2022 Tobias Blomberg / SM0SVX + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +\endverbatim +*/ + +/** @example AsyncSslX509Extensions_demo.cpp +An example of how to use the SslX509Extensions class +*/ + +#ifndef ASYNC_SSL_X509_EXTENSIONS_INCLUDED +#define ASYNC_SSL_X509_EXTENSIONS_INCLUDED + + +/**************************************************************************** + * + * System Includes + * + ****************************************************************************/ + +#include +#include +#include + + +/**************************************************************************** + * + * Project Includes + * + ****************************************************************************/ + +#include +#include + + +/**************************************************************************** + * + * Local Includes + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Forward declarations + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Namespace + * + ****************************************************************************/ + +namespace Async +{ + + +/**************************************************************************** + * + * Forward declarations of classes inside of the declared namespace + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Defines & typedefs + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Exported Global Variables + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Class definitions + * + ****************************************************************************/ + +/** +@brief A_brief_class_description +@author Tobias Blomberg / SM0SVX +@date 2022-05-22 + +A_detailed_class_description + +\include AsyncSslX509Extensions_demo.cpp +*/ +class SslX509Extensions +{ + public: + /** + * @brief Default constructor + */ + SslX509Extensions(void) + { + m_exts = sk_X509_EXTENSION_new_null(); + } + + explicit SslX509Extensions(STACK_OF(X509_EXTENSION)* exts) + : m_exts(exts) + { + } + + /** + * @brief Move Constructor + * @param other The object to move from + */ + SslX509Extensions(SslX509Extensions&& other) + { + m_exts = other.m_exts; + other.m_exts = nullptr; + } + + /** + * @brief Copy constructor + */ + explicit SslX509Extensions(const SslX509Extensions& other) + { + std::cout << "### SslX509Extensions copy constructor" << std::endl; + for (int i=0; i= 3 + auto ext = X509_EXTENSION_dup(other_ext); +#else + auto ext = X509_EXTENSION_dup(const_cast(other_ext)); +#endif + return (sk_X509_EXTENSION_push(m_exts, ext) > 0); + } + + operator const STACK_OF(X509_EXTENSION)*() const { return m_exts; } + + private: + STACK_OF(X509_EXTENSION)* m_exts = nullptr; + + bool addExt(int nid, const std::string& value) + { + auto ex = X509V3_EXT_conf_nid(NULL, NULL, nid, value.c_str()); + if (ex == nullptr) + { + return false; + } + sk_X509_EXTENSION_push(m_exts, ex); + return true; + } + + X509_EXTENSION* cloneExtension(int nid) const + { + int ext_idx = X509v3_get_ext_by_NID(m_exts, nid, -1); + if (ext_idx < 0) + { + return nullptr; + } + auto ext = X509v3_get_ext(m_exts, ext_idx); + return X509_EXTENSION_dup(ext); + } + +}; /* SslX509Extensions */ + + + +} /* namespace Async */ + +#endif /* ASYNC_SSL_X509_EXTENSIONS_INCLUDED */ + +/* + * This file has not been truncated + */ diff --git a/src/async/core/AsyncTcpClient.h b/src/async/core/AsyncTcpClient.h index fb05823bf..13b6c4916 100644 --- a/src/async/core/AsyncTcpClient.h +++ b/src/async/core/AsyncTcpClient.h @@ -203,6 +203,11 @@ class TcpClient : public ConT, public TcpClientBase return TcpClientBase::isIdle() && ConT::isIdle(); } + void setSslContext(SslContext& ctx) + { + ConT::setSslContext(ctx, false); + } + protected: /** * @brief Disconnect from the remote peer diff --git a/src/async/core/AsyncTcpClientBase.cpp b/src/async/core/AsyncTcpClientBase.cpp index a029fb575..c4788a0d8 100644 --- a/src/async/core/AsyncTcpClientBase.cpp +++ b/src/async/core/AsyncTcpClientBase.cpp @@ -228,6 +228,8 @@ void TcpClientBase::connect(void) { assert(isIdle() && con->isIdle()); + //m_successful_connect = false; + if (!dns.label().empty()) { dns.lookup(); diff --git a/src/async/core/AsyncTcpClientBase.h b/src/async/core/AsyncTcpClientBase.h index dd190062d..f2f7fdabf 100644 --- a/src/async/core/AsyncTcpClientBase.h +++ b/src/async/core/AsyncTcpClientBase.h @@ -263,6 +263,19 @@ class TcpClientBase : virtual public sigc::trackable sigc::signal connected; protected: + /** + * @brief Check if the connection has been fully connected + * @return Return \em true if the connection was successful + * + * This function return true when the connection has been fully + * established. It will continue to return true even after disconnection + * and will be reset at the moment when a new connection attempt is made. + */ + //virtual bool successfulConnect(void) + //{ + // return m_successful_connect; + //} + /** * @brief Disconnect from the remote peer * @@ -280,6 +293,7 @@ class TcpClientBase : virtual public sigc::trackable */ virtual void connectionEstablished(void) { + //m_successful_connect = true; emitConnected(); } @@ -294,6 +308,7 @@ class TcpClientBase : virtual public sigc::trackable int sock; FdWatch wr_watch; Async::IpAddress bind_ip; + //bool m_successful_connect = false; void dnsResultsReady(DnsLookup& dns_lookup); void connectToRemote(void); diff --git a/src/async/core/AsyncTcpConnection.cpp b/src/async/core/AsyncTcpConnection.cpp index 506316e57..2afab584f 100644 --- a/src/async/core/AsyncTcpConnection.cpp +++ b/src/async/core/AsyncTcpConnection.cpp @@ -42,6 +42,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include #include #include +#include /**************************************************************************** @@ -61,7 +62,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "AsyncFdWatch.h" #include "AsyncDnsLookup.h" #include "AsyncTcpConnection.h" - +#include "AsyncSslX509.h" /**************************************************************************** @@ -74,7 +75,6 @@ using namespace std; using namespace Async; - /**************************************************************************** * * Defines & typedefs @@ -107,13 +107,13 @@ using namespace Async; - /**************************************************************************** * * Local Global Variables * ****************************************************************************/ +std::map TcpConnection::ssl_con_map; /**************************************************************************** @@ -135,9 +135,6 @@ const char *TcpConnection::disconnectReasonStr(DisconnectReason reason) case DR_SYSTEM_ERROR: return strerror(errno); - case DR_RECV_BUFFER_OVERFLOW: - return "Receiver buffer overflow"; - case DR_ORDERED_DISCONNECT: return "Locally ordered disconnect"; @@ -150,24 +147,12 @@ const char *TcpConnection::disconnectReasonStr(DisconnectReason reason) case DR_BAD_STATE: return "Connection in bad state"; } - + return "Unknown disconnect reason"; - + } /* TcpConnection::disconnectReasonStr */ -/* - *------------------------------------------------------------------------ - * Method: - * Purpose: - * Input: - * Output: - * Author: - * Created: - * Remarks: - * Bugs: - *------------------------------------------------------------------------ - */ TcpConnection::TcpConnection(size_t recv_buf_len) : TcpConnection(-1, IpAddress(), 0, recv_buf_len) { @@ -176,13 +161,13 @@ TcpConnection::TcpConnection(size_t recv_buf_len) TcpConnection::TcpConnection(int sock, const IpAddress& remote_addr, uint16_t remote_port, size_t recv_buf_len) - : remote_addr(remote_addr), remote_port(remote_port), - recv_buf_len(recv_buf_len), sock(sock), - recv_buf(0), recv_buf_cnt(0) + : remote_addr(remote_addr), remote_port(remote_port), sock(sock) { - recv_buf = new char[recv_buf_len]; - rd_watch.activity.connect(mem_fun(*this, &TcpConnection::recvHandler)); - wr_watch.activity.connect(mem_fun(*this, &TcpConnection::writeHandler)); + m_recv_buf.reserve(recv_buf_len); + rd_watch.activity.connect( + mem_fun(*this, &TcpConnection::recvHandler)); + m_wr_watch.activity.connect( + mem_fun(*this, &TcpConnection::onWriteSpaceAvailable)); setSocket(sock); } /* TcpConnection::TcpConnection */ @@ -190,9 +175,6 @@ TcpConnection::TcpConnection(int sock, const IpAddress& remote_addr, TcpConnection::~TcpConnection(void) { closeConnection(); - delete [] recv_buf; - recv_buf = 0; - recv_buf_cnt = recv_buf_len = 0; } /* TcpConnection::~TcpConnection */ @@ -200,6 +182,8 @@ TcpConnection& TcpConnection::operator=(TcpConnection&& other) { //std::cout << "### TcpConnection::operator=(TcpConnection&&)" << std::endl; + closeConnection(); + remote_addr = other.remote_addr; other.remote_addr.clear(); @@ -210,17 +194,34 @@ TcpConnection& TcpConnection::operator=(TcpConnection&& other) other.sock = -1; rd_watch = std::move(other.rd_watch); + m_wr_watch = std::move(other.m_wr_watch); + + m_recv_buf = std::move(other.m_recv_buf); + other.m_recv_buf.clear(); + other.m_recv_buf.reserve(m_recv_buf.capacity()); + + m_write_buf = std::move(other.m_write_buf); + other.m_write_buf.clear(); + other.m_write_buf.reserve(m_write_buf.capacity()); + + m_ssl_ctx = other.m_ssl_ctx; + other.m_ssl_ctx = nullptr; - wr_watch = std::move(other.wr_watch); + m_ssl_is_server = other.m_ssl_is_server; + other.m_ssl_is_server = false; - delete [] recv_buf; - recv_buf_len = other.recv_buf_len; - recv_buf = other.recv_buf; - recv_buf_cnt = other.recv_buf_cnt; + m_ssl = other.m_ssl; + other.m_ssl = nullptr; - other.recv_buf_len = DEFAULT_RECV_BUF_LEN; - other.recv_buf = new char[other.recv_buf_len]; - other.recv_buf_cnt = 0; + m_ssl_rd_bio = other.m_ssl_rd_bio; + other.m_ssl_rd_bio = nullptr; + + m_ssl_wr_bio = other.m_ssl_wr_bio; + other.m_ssl_wr_bio = nullptr; + + m_ssl_encrypt_buf = std::move(other.m_ssl_encrypt_buf); + other.m_ssl_encrypt_buf.clear(); + other.m_ssl_encrypt_buf.reserve(m_ssl_encrypt_buf.capacity()); return *this; } /* TcpConnection::operator= */ @@ -228,42 +229,129 @@ TcpConnection& TcpConnection::operator=(TcpConnection&& other) void TcpConnection::setRecvBufLen(size_t recv_buf_len) { - if (recv_buf_cnt > recv_buf_len) + if (recv_buf_len > m_recv_buf.size()) { - // This will on next reception cause an overflow error disconnection - recv_buf_cnt = recv_buf_len; + m_recv_buf.reserve(recv_buf_len); } - char *new_recv_buf = new char[recv_buf_len]; - memcpy(new_recv_buf, recv_buf, recv_buf_cnt); - this->recv_buf_len = recv_buf_len; - delete [] recv_buf; - recv_buf = new_recv_buf; } /* TcpConnection::setRecvBufLen */ int TcpConnection::write(const void *buf, int count) { assert(sock >= 0); - int cnt = ::send(sock, buf, count, MSG_NOSIGNAL); - if (cnt < 0) + if (m_ssl != nullptr) { - if (errno != EAGAIN) + return sslWrite(reinterpret_cast(buf), count); + } + addToWriteBuf(reinterpret_cast(buf), count); + return count; +} /* TcpConnection::write */ + + +void TcpConnection::enableSsl(bool enable) +{ + if (enable) + { + assert(m_ssl_ctx != nullptr); + + m_ssl_rd_bio = BIO_new(BIO_s_mem()); + m_ssl_wr_bio = BIO_new(BIO_s_mem()); + m_ssl = SSL_new(*m_ssl_ctx); + ssl_con_map[m_ssl] = this; + + SSL_set_bio(m_ssl, m_ssl_rd_bio, m_ssl_wr_bio); + + if (m_ssl_is_server) { - return -1; + SSL_set_accept_state(m_ssl); } - cnt = 0; + else + { + //SSL_set_tlsext_host_name(m_ssl, "svxreflector.example.com"); + SSL_set_connect_state(m_ssl); + auto ret = sslDoHandshake(); + assert(ret != SSLSTATUS_FAIL); + } + + //auto data_index = SSL_get_ex_new_index(0, NULL, NULL, NULL, NULL); + //assert (data_index != -1); + //SSL_set_ex_data(m_ssl, data_index, this); + + SSL_set_verify(m_ssl, SSL_VERIFY_PEER, sslVerifyCallback); + } + else + { + // FIXME: Deinitialize } +} /* TcpConnection::enableSsl */ + - if (cnt < count) +void TcpConnection::setSslContext(SslContext& ctx, bool is_server) +{ + m_ssl_ctx = &ctx; + m_ssl_is_server = is_server; +} /* TcpConnection::setSslContext */ + + +Async::SslX509 TcpConnection::sslPeerCertificate(void) +{ +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + return Async::SslX509(SSL_get1_peer_certificate(m_ssl), true); +#else + return Async::SslX509(SSL_get_peer_certificate(m_ssl), true); +#endif +} /* TcpConnection::sslPeerCertificate */ + + +Async::SslX509 TcpConnection::sslCertificate(void) const +{ + auto x509 = SSL_CTX_get0_certificate(*m_ssl_ctx); + return Async::SslX509(x509, false); +} /* TcpConnection::sslCertificate */ + + +long TcpConnection::sslVerifyResult(void) const +{ + return SSL_get_verify_result(m_ssl); +} /* TcpConnection::sslVerifyResult */ + + +#if 0 +std::string TcpConnection::sslCommonName(void) const +{ + std::string cn; + + assert(m_ssl != nullptr); + + Async::SslX509 cert(SSL_get_peer_certificate(m_ssl)); + if(cert == nullptr) { - sendBufferFull(true); - wr_watch.setEnabled(true); + std::cout << "### No certificate for this connection" << std::endl; + return cn; + } + + if (SSL_get_verify_result(m_ssl) == X509_V_OK) + { + X509_NAME* subj = cert.getSubjectName(); + + // Assume there is only one CN + int lastpos = X509_NAME_get_index_by_NID(subj, NID_commonName, -1); + if (lastpos >= 0) + { + X509_NAME_ENTRY *e = X509_NAME_get_entry(subj, lastpos); + ASN1_STRING *d = X509_NAME_ENTRY_get_data(e); + cn = reinterpret_cast(ASN1_STRING_get0_data(d)); + //std::cout << "### CN=" << cn << std::endl; + } + } + else + { + std::cout << "### The certificate did not verify" << std::endl; } - - return cnt; - -} /* TcpConnection::write */ + return cn; +} /* TcpConnection::sslCommonName */ +#endif /**************************************************************************** @@ -272,72 +360,78 @@ int TcpConnection::write(const void *buf, int count) * ****************************************************************************/ - -/* - *------------------------------------------------------------------------ - * Method: TcpConnection::setSocket - * Purpose: - * Input: - * Output: - * Author: Tobias Blomberg - * Created: 2003-12-07 - * Remarks: - * Bugs: - *------------------------------------------------------------------------ - */ void TcpConnection::setSocket(int sock) { this->sock = sock; rd_watch.setFd(sock, FdWatch::FD_WATCH_RD); rd_watch.setEnabled(sock >= 0); - wr_watch.setEnabled(false); - wr_watch.setFd(sock, FdWatch::FD_WATCH_WR); + m_wr_watch.setEnabled(false); + m_wr_watch.setFd(sock, FdWatch::FD_WATCH_WR); } /* TcpConnection::setSocket */ -/* - *------------------------------------------------------------------------ - * Method: TcpConnection::setRemoteAddr - * Purpose: - * Input: - * Output: - * Author: Tobias Blomberg - * Created: 2003-12-07 - * Remarks: - * Bugs: - *------------------------------------------------------------------------ - */ void TcpConnection::setRemoteAddr(const IpAddress& remote_addr) { this->remote_addr = remote_addr; } /* TcpConnection::setRemoteAddr */ -/* - *------------------------------------------------------------------------ - * Method: TcpConnection::setRemotePort - * Purpose: - * Input: - * Output: - * Author: Tobias Blomberg - * Created: 2003-12-07 - * Remarks: - * Bugs: - *------------------------------------------------------------------------ - */ void TcpConnection::setRemotePort(uint16_t remote_port) { this->remote_port = remote_port; } /* TcpConnection::setRemotePort */ +uint16_t TcpConnection::localPort(void) const +{ + struct sockaddr_in addr; + socklen_t len = sizeof(addr); + int ret = getsockname(sock, reinterpret_cast(&addr), + &len); + if ((ret != 0) || (len != sizeof(addr))) + { + perror("getsockname"); + return 0; + } + //std::cout << "### TcpConnection::localPort: sin_port=" + // << ntohs(addr.sin_port) << std::endl; + return ntohs(addr.sin_port); +} /* TcpConnection::localPort */ + + +Async::IpAddress TcpConnection::localHost(void) const +{ + struct sockaddr_in addr; + socklen_t len = sizeof(addr); + int ret = getsockname(sock, reinterpret_cast(&addr), + &len); + if ((ret != 0) || (len != sizeof(addr))) + { + perror("getsockname"); + return Async::IpAddress(); + } + //std::cout << "### TcpConnection::localHost: sin_addr=" + // << Async::IpAddress(addr.sin_addr) << std::endl; + return Async::IpAddress(addr.sin_addr); +} /* TcpConnection::localHost */ + + void TcpConnection::closeConnection(void) { - recv_buf_cnt = 0; + m_recv_buf.clear(); + m_write_buf.clear(); + m_ssl_encrypt_buf.clear(); - wr_watch.setEnabled(false); + m_wr_watch.setEnabled(false); rd_watch.setEnabled(false); + if (m_ssl != nullptr) + { + ssl_con_map.erase(m_ssl); + SSL_free(m_ssl); + m_ssl = nullptr; + } + if (sock >= 0) { close(sock); @@ -347,41 +441,41 @@ void TcpConnection::closeConnection(void) - - /**************************************************************************** * * Private member functions * ****************************************************************************/ +int TcpConnection::sslVerifyCallback(int preverify_ok, + X509_STORE_CTX* x509_store_ctx) +{ + //std::cout << "### TcpConnection::sslVerifyCallback: preverify_ok: " + // << preverify_ok << std::endl; + + SSL* ssl = reinterpret_cast(X509_STORE_CTX_get_ex_data(x509_store_ctx, + SSL_get_ex_data_X509_STORE_CTX_idx())); + assert(ssl != nullptr); + + TcpConnection* con = lookupConnection(ssl); + assert(con != nullptr); + + return con->emitVerifyPeer(preverify_ok, x509_store_ctx); +} /* TcpConnection::sslVerifyCallback */ + -/* - *---------------------------------------------------------------------------- - * Method: - * Purpose: - * Input: - * Output: - * Author: - * Created: - * Remarks: - * Bugs: - *---------------------------------------------------------------------------- - */ void TcpConnection::recvHandler(FdWatch *watch) { - //cout << "recv_buf_cnt=" << recv_buf_cnt << endl; - //cout << "recv_buf_len=" << recv_buf_len << endl; - - if (recv_buf_cnt == recv_buf_len) - { - closeConnection(); - onDisconnected(DR_RECV_BUFFER_OVERFLOW); - return; - } - - int cnt = read(sock, recv_buf+recv_buf_cnt, recv_buf_len-recv_buf_cnt); - if (cnt == -1) + //std::cout << "### TcpConnection::recvHandler:" + // << " m_recv_buf.size()=" << m_recv_buf.size() + // << " m_recv_buf.capacity()=" << m_recv_buf.capacity() + // << std::endl; + + size_t recv_buf_size = m_recv_buf.size(); + int cnt = read(sock, &m_recv_buf[recv_buf_size], + m_recv_buf.capacity()-recv_buf_size); + //std::cout << "### cnt=" << cnt << std::endl; + if (cnt <= -1) { int errno_tmp = errno; closeConnection(); @@ -396,32 +490,316 @@ void TcpConnection::recvHandler(FdWatch *watch) onDisconnected(DR_REMOTE_DISCONNECTED); return; } - - recv_buf_cnt += cnt; - size_t processed = onDataReceived(recv_buf, recv_buf_cnt); - //cout << "processed=" << processed << endl; - if (processed >= recv_buf_cnt) + + m_recv_buf.resize(recv_buf_size + cnt); + //std::cout << "### TcpConnection::recvHandler: size=" + // << m_recv_buf.size() << std::endl; + if (m_recv_buf.size() == m_recv_buf.capacity()) + { + size_t new_capacity = m_recv_buf.capacity() * 2; + //std::cout << "### new_capacity=" << new_capacity << std::endl; + m_recv_buf.reserve(new_capacity); + } + + ssize_t processed = -1; + if (m_ssl != nullptr) { - recv_buf_cnt = 0; + processed = sslRecvHandler(reinterpret_cast(m_recv_buf.data()), + m_recv_buf.size()); } else { - memmove(recv_buf, recv_buf + processed, recv_buf_cnt - processed); - recv_buf_cnt = recv_buf_cnt - processed; + processed = onDataReceived(reinterpret_cast(m_recv_buf.data()), + m_recv_buf.size()); + } + //cout << "### processed=" << processed << endl; + if (processed >= static_cast(m_recv_buf.size())) + { + m_recv_buf.clear(); + } + else if (processed > 0) + { + std::rotate(m_recv_buf.begin(), m_recv_buf.begin()+processed, + m_recv_buf.end()); + m_recv_buf.resize(m_recv_buf.size() - processed); + } + else if (processed < 0) + { + std::cerr << "*** ERROR: Network communication failed with " + << remoteHost() << ":" << remotePort() + << std::endl; + if (isConnected()) + { + closeConnection(); + onDisconnected(DR_PROTOCOL_ERROR); + } } - } /* TcpConnection::recvHandler */ -void TcpConnection::writeHandler(FdWatch *watch) +void TcpConnection::addToWriteBuf(const char *buf, size_t len) +{ + m_write_buf.insert(m_write_buf.end(), buf, buf+len); + m_wr_watch.setEnabled(!m_write_buf.empty()); +} /* TcpConnection::addToWriteBuf */ + + +void TcpConnection::onWriteSpaceAvailable(Async::FdWatch* w) +{ + ssize_t n = rawWrite(m_write_buf.data(), m_write_buf.size()); + //std::cout << "### TcpConnection::onWriteSpaceAvailabe:" + // << " fd=" << w->fd() + // << " n=" << n + // << " bufsize=" << m_write_buf.size() + // << std::endl; + assert(n <= static_cast(m_write_buf.size())); + if (n >= 0) + { + if (n == static_cast(m_write_buf.size())) + { + m_write_buf.clear(); + } + else if (n > 0) + { + std::rotate(m_write_buf.begin(), m_write_buf.begin() + n, + m_write_buf.end()); + m_write_buf.resize(m_write_buf.size() - n); + } + } + else + { + perror("### TcpConnection::onWriteSpaceAvailable: rawWrite()"); + } + w->setEnabled(!m_write_buf.empty()); +} /* TcpConnection::onWriteSpaceAvailable */ + + +int TcpConnection::rawWrite(const void* buf, int count) +{ + assert(sock != -1); + int cnt = ::send(sock, buf, count, MSG_NOSIGNAL); + if (cnt < 0) + { + if (errno != EAGAIN) + { + return -1; + } + cnt = 0; + } + + return cnt; +} /* TcpConnection::rawWrite */ + + +void TcpConnection::sslPrintErrors(const char* fname) { - watch->setEnabled(false); - sendBufferFull(false); -} /* TcpConnection::writeHandler */ + std::cerr << "*** ERROR: OpenSSL \"" << fname << "\" failed: "; + ERR_print_errors_fp(stderr); +} /* TcpConnection::sslPrintErrors */ +TcpConnection::SslStatus TcpConnection::sslGetStatus(int n) +{ + int err = SSL_get_error(m_ssl, n); + switch (err) + { + case SSL_ERROR_NONE: + return SSLSTATUS_OK; + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_READ: + return SSLSTATUS_WANT_IO; + case SSL_ERROR_ZERO_RETURN: + case SSL_ERROR_SYSCALL: + default: + return SSLSTATUS_FAIL; + } +} /* TcpConnection::sslGetStatus */ + + +int TcpConnection::sslRecvHandler(char* src, int count) +{ + //std::cout << "### TcpConnection::sslRecvHandler: count=" << count + // << std::endl; + + SslStatus status; + int n; + + int orig_count = count; + + while (count > 0) + { + n = BIO_write(m_ssl_rd_bio, src, count); + //std::cout << "### BIO_write: n=" << n << std::endl; + if (n <= 0) + { + sslPrintErrors("BIO_write"); + return 0; + } + + src += n; + count -= n; + + if (!SSL_is_init_finished(m_ssl)) + { + if (sslDoHandshake() == SSLSTATUS_FAIL) + { + sslPrintErrors("sslDoHandshake"); + return -1; + } + if ((m_ssl == nullptr) || !SSL_is_init_finished(m_ssl)) + { + //std::cout << "### onDataReceived: init not finished" << std::endl; + return (orig_count - count); + } + } + + /* The encrypted data is now in the input bio so now we can perform actual + * read of unencrypted data. */ + char buf[DEFAULT_BUF_SIZE]; + //while (SSL_pending(m_ssl) > 0) + do + { + if (m_ssl == nullptr) + { + return (orig_count - count); + } + n = SSL_read(m_ssl, buf, sizeof(buf)); + //std::cout << "### SSL_read: n=" << n << std::endl; + if (n > 0) + { + onDataReceived(buf, n); + } + } while (n > 0); + + status = sslGetStatus(n); + + if (status == SSLSTATUS_FAIL) + { + sslPrintErrors("SSL_read/SSL_pending"); + return -1; + } + + /* Did SSL request to write bytes? This can happen if peer has requested SSL + * renegotiation. */ + if (status == SSLSTATUS_WANT_IO) + { + do { + n = BIO_read(m_ssl_wr_bio, buf, sizeof(buf)); + if (n > 0) + { + addToWriteBuf(buf, n); + } + else if (!BIO_should_retry(m_ssl_wr_bio)) + { + sslPrintErrors("BIO_should_retry"); + return -1; + } + } while (n > 0); + } + } + + return (orig_count - count); +} /* TcpConnection::readHandler */ + + +enum TcpConnection::SslStatus TcpConnection::sslDoHandshake(void) +{ + char buf[DEFAULT_BUF_SIZE]; + SslStatus status; + + int n = SSL_do_handshake(m_ssl); + status = sslGetStatus(n); + + /* Did SSL request to write bytes? */ + if (status == SSLSTATUS_WANT_IO) + { + do { + n = BIO_read(m_ssl_wr_bio, buf, sizeof(buf)); + if (n > 0) + { + addToWriteBuf(buf, n); + } + else if (!BIO_should_retry(m_ssl_wr_bio)) + return SSLSTATUS_FAIL; + } while (n>0); + } + + if (SSL_is_init_finished(m_ssl)) + { + sslConnectionReady(this); + sslEncrypt(); + } + + return status; +} /* TcpConnection::sslDoHandshake */ + + +int TcpConnection::sslEncrypt(void) +{ + char buf[DEFAULT_BUF_SIZE]; + SslStatus status; + + if ((m_ssl == nullptr) || !SSL_is_init_finished(m_ssl)) + { + return 0; + } + + while (!m_ssl_encrypt_buf.empty()) + { + int n = SSL_write(m_ssl, m_ssl_encrypt_buf.data(), + m_ssl_encrypt_buf.size()); + status = sslGetStatus(n); + + if (n > 0) + { + if (n == static_cast(m_ssl_encrypt_buf.size())) + { + m_ssl_encrypt_buf.clear(); + } + else + { + std::rotate(m_ssl_encrypt_buf.begin(), m_ssl_encrypt_buf.begin()+n, + m_ssl_encrypt_buf.end()); + m_ssl_encrypt_buf.resize(m_ssl_encrypt_buf.size() - n); + } + + /* take the output of the SSL object and queue it for socket write */ + do { + n = BIO_read(m_ssl_wr_bio, buf, sizeof(buf)); + if (n > 0) + { + addToWriteBuf(buf, n); + } + else if (!BIO_should_retry(m_ssl_wr_bio)) + { + return -1; + } + } while (n > 0); + } + + if (status == SSLSTATUS_FAIL) + { + return -1; + } + + if (n==0) + { + break; + } + } + return 0; +} /* TcpConnection::sslEncrypt */ + + +int TcpConnection::sslWrite(const void* buf, int count) +{ + const char* ptr = reinterpret_cast(buf); + m_ssl_encrypt_buf.insert(m_ssl_encrypt_buf.end(), ptr, ptr+count); + sslEncrypt(); + return count; +} /* TcpConnection::sslWrite */ + /* * This file has not been truncated */ - diff --git a/src/async/core/AsyncTcpConnection.h b/src/async/core/AsyncTcpConnection.h index 13fcba39c..f3c53e2c0 100644 --- a/src/async/core/AsyncTcpConnection.h +++ b/src/async/core/AsyncTcpConnection.h @@ -27,8 +27,6 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA \endverbatim */ - - #ifndef ASYNC_TCP_CONNECTION_INCLUDED #define ASYNC_TCP_CONNECTION_INCLUDED @@ -41,8 +39,16 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include #include +#include +#include +#include +#include #include +#include +#include +#include +#include /**************************************************************************** @@ -53,6 +59,8 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include #include +#include +#include /**************************************************************************** @@ -119,6 +127,13 @@ class IpAddress; This class is used to handle an existing TCP connection. It is not meant to be used directly but could be. It it mainly created to handle connections for Async::TcpClient and Async::TcpServer. + +It can also handle SSL/TLS connections. A raw TCP connection can be switched to +be encrypted using the setSslContext() and enableSsl() functions. + +The reception buffer size given at construction time or using the +setRecvBufLen() function is an initial value. If during the connection a larger +buffer is needed the size will be automatically increased. */ class TcpConnection : virtual public sigc::trackable { @@ -131,15 +146,35 @@ class TcpConnection : virtual public sigc::trackable DR_HOST_NOT_FOUND, ///< The specified host was not found in the DNS DR_REMOTE_DISCONNECTED, ///< The remote host disconnected DR_SYSTEM_ERROR, ///< A system error occured (check errno) - DR_RECV_BUFFER_OVERFLOW, ///< Receiver buffer overflow DR_ORDERED_DISCONNECT, ///< Disconnect ordered locally DR_PROTOCOL_ERROR, ///< Protocol error DR_SWITCH_PEER, ///< A better peer was found so reconnecting DR_BAD_STATE ///< The connection ended up in a bad state } DisconnectReason; - + + /** + * @brief A sigc return value accumulator for signals returning bool + * + * This sigc accumulator will return \em true if all connected slots return + * \em true. + */ + struct if_all_true_acc + { + typedef bool result_type; + template + bool operator()(I first, I last) + { + bool success = true; + for (; first != last; first ++) + { + success &= *first; + } + return success; + } + }; + /** - * @brief The default length of the reception buffer + * @brief The default size of the reception buffer */ static const int DEFAULT_RECV_BUF_LEN = 1024; @@ -150,7 +185,7 @@ class TcpConnection : virtual public sigc::trackable /** * @brief Constructor - * @param recv_buf_len The length of the receiver buffer to use + * @param recv_buf_len The initial size of the receive buffer */ explicit TcpConnection(size_t recv_buf_len = DEFAULT_RECV_BUF_LEN); @@ -159,7 +194,7 @@ class TcpConnection : virtual public sigc::trackable * @param sock The socket for the connection to handle * @param remote_addr The remote IP-address of the connection * @param remote_port The remote TCP-port of the connection - * @param recv_buf_len The length of the receiver buffer to use + * @param recv_buf_len The initial size of the receive buffer */ TcpConnection(int sock, const IpAddress& remote_addr, uint16_t remote_port, @@ -187,11 +222,13 @@ class TcpConnection : virtual public sigc::trackable * * This function will resize the receive buffer to the specified size. * If the buffer size is reduced and there are more bytes in the current - * buffer than can be fitted into the new buffer, an overflow disconnection - * will be issued on the next reception. + * buffer than can be fitted into the new buffer, the buffer resize + * request vill be silently ignored. */ void setRecvBufLen(size_t recv_buf_len); + size_t recvBufLen(void) const { return m_recv_buf.capacity(); } + /** * @brief Disconnect from the remote host * @@ -208,7 +245,19 @@ class TcpConnection : virtual public sigc::trackable * @return Returns the number of bytes written or -1 on failure */ virtual int write(const void *buf, int count); - + + /** + * @brief Get the local IP address associated with this connection + * @return Returns an IP address + */ + IpAddress localHost(void) const; + + /** + * @brief Get the local TCP port associated with this connection + * @return Returns a port number + */ + uint16_t localPort(void) const; + /** * @brief Return the IP-address of the remote host * @return Returns the IP-address of the remote host @@ -237,7 +286,54 @@ class TcpConnection : virtual public sigc::trackable * A connection being idle means that it is not connected */ bool isIdle(void) const { return sock == -1; } - + + /** + * @brief Enable or disable TLS for this connection + * @param enable Set to \em true to enable + */ + void enableSsl(bool enable); + + /** + * @brief Get the peer certificate associated with this connection + * @return Returns the X509 certificate associated with the peer + * + * This function is used to retrieve the peer certificate that the peer has + * sent to us during the setup phase. If no certificate was sent this + * function will return a null object so checking for that condition can be + * done by comparing the returned object with nullptr. + */ + SslX509 sslPeerCertificate(void); + + /** + */ + Async::SslX509 sslCertificate(void) const; + + /** + * @brief Get the result of the certificate verification process + * @return Returns the verification result (e.g. X509_V_OK for ok) + */ + long sslVerifyResult(void) const; + + /** + * @brief Set the OpenSSL context to use when setting up the connection + * @param ctx The context object to use + * @param is_server Set to \em true if this is a server side connection + * + * This function should be called prior to calling enableSsl in order to + * set up a TLS context to use when setting up the connection. + */ + void setSslContext(SslContext& ctx, bool is_server); + + SslContext* sslContext(void) { return m_ssl_ctx; } + + bool isServer(void) const { return m_ssl_is_server; } + + /** + * @brief Get common name for the SSL connection + * @return Returns the common name for the associated X509 certificate + */ + //std::string sslCommonName(void) const; + /** * @brief A signal that is emitted when a connection has been terminated * @param con The connection object @@ -260,15 +356,32 @@ class TcpConnection : virtual public sigc::trackable * will be appended to the old data. */ sigc::signal dataReceived; - + /** - * @brief A signal that is emitted when the send buffer status changes - * @param is_full Set to \em true if the buffer is full or \em false - * if a buffer full condition has been cleared + * @brief A signal that is emitted on SSL/TLS certificate verification + * @param con The connection object + * @param preverify_ok Is \em true if the OpenSSL verification is ok + * @param x509_store_ctx The X509 store context + * + * Connect to this signal to be able to tap in to the certificate + * verification process. All slots that connect to this signal must return + * true for the verification process to succeed. + * + * For more information on the function arguments have a look at the manual + * page for the OpenSSL function SSL_set_verify(). + */ + sigc::signal::accumulated verifyPeer; + + /** + * @brief A signal that is emitted when the SSL connection is ready + * @param con The connection object + * + * This signal is emitted when the SSL initialization and handshake has + * finished after the application has called the enableSsl() function. */ - sigc::signal sendBufferFull; + sigc::signal sslConnectionReady; - protected: /** * @brief Setup information about the connection @@ -351,20 +464,76 @@ class TcpConnection : virtual public sigc::trackable disconnected(this, reason); } + /** + * @brief Emit the verifyPeer signal + * @param preverify_ok The basic certificate verifications passed + * @param x509_store_ctx The OpenSSL X509 store context + * @return Returns 1 on success or 0 if verification fail + */ + virtual int emitVerifyPeer(int preverify_ok, + X509_STORE_CTX* x509_store_ctx) + { + if (verifyPeer.empty()) + { + return preverify_ok; + } + return verifyPeer(this, preverify_ok == 1, x509_store_ctx) ? 1 : 0; + } + private: friend class TcpClientBase; - IpAddress remote_addr; - uint16_t remote_port; - size_t recv_buf_len; - int sock; - FdWatch rd_watch; - FdWatch wr_watch; - char * recv_buf; - size_t recv_buf_cnt; - + enum SslStatus { SSLSTATUS_OK, SSLSTATUS_WANT_IO, SSLSTATUS_FAIL }; + struct Char + { + char value; + Char(void) noexcept + { + // Do nothing to suppress automatic initialization on container resize + // for m_recv_buf. + static_assert(sizeof *this == sizeof value, "invalid size"); + static_assert(__alignof *this == __alignof value, "invalid alignment"); + } + }; + + static constexpr const size_t DEFAULT_BUF_SIZE = 1024; + + static std::map ssl_con_map; + + IpAddress remote_addr; + uint16_t remote_port = 0; + int sock = -1; + FdWatch rd_watch; + std::vector m_recv_buf; + Async::FdWatch m_wr_watch; + std::vector m_write_buf; + + SslContext* m_ssl_ctx = nullptr; + bool m_ssl_is_server = false; + SSL* m_ssl = nullptr; + BIO* m_ssl_rd_bio = nullptr; // SSL reads, we write + BIO* m_ssl_wr_bio = nullptr; // SSL writes, we read + std::vector m_ssl_encrypt_buf; + + static TcpConnection* lookupConnection(SSL* ssl) + { + auto it = ssl_con_map.find(ssl); + return (it != ssl_con_map.end()) ? it->second : nullptr; + } + static int sslVerifyCallback(int preverify_ok, + X509_STORE_CTX* x509_store_ctx); + void recvHandler(FdWatch *watch); - void writeHandler(FdWatch *watch); + void addToWriteBuf(const char *buf, size_t len); + void onWriteSpaceAvailable(Async::FdWatch* w); + int rawWrite(const void* buf, int count); + + void sslPrintErrors(const char* fname); + SslStatus sslGetStatus(int n); + int sslRecvHandler(char* src, int count); + SslStatus sslDoHandshake(void); + int sslEncrypt(void); + int sslWrite(const void* buf, int count); }; /* class TcpConnection */ diff --git a/src/async/core/AsyncTcpPrioClient.h b/src/async/core/AsyncTcpPrioClient.h index a078a4a29..e719a5157 100644 --- a/src/async/core/AsyncTcpPrioClient.h +++ b/src/async/core/AsyncTcpPrioClient.h @@ -167,20 +167,6 @@ class TcpPrioClient : public ConT, public TcpPrioClientBase TcpPrioClientBase::disconnect(); } - /** - * @brief Mark connection as failed - * - * The application can use this function to mark a connection as failed so - * that when a reconnect is performed, the next server will be tried. If a - * connect is classified as successful, the same host will be tried again - * on reconnect. - */ - //void markAsFailedConnect(void) - //{ - // //std::cout << "### TcpPrioClient::markAsFailedConnect" << std::endl; - // m_successful_connect = false; - //} - protected: using ConT::operator=; using TcpPrioClientBase::operator=; @@ -206,7 +192,6 @@ class TcpPrioClient : public ConT, public TcpPrioClientBase virtual void onDisconnected(TcpConnection::DisconnectReason reason) { //std::cout << "### TcpPrioClient::onDisconnected:" - // //<< " m_successful_connect=" << m_successful_connect // << std::endl; ConT::onDisconnected(reason); TcpPrioClientBase::onDisconnected(reason); @@ -232,11 +217,10 @@ class TcpPrioClient : public ConT, public TcpPrioClientBase } private: - //bool m_successful_connect = false; - TcpPrioClient& operator=(TcpClient&& other) { - //std::cout << "### TcpPrioClient::operator=(TcpClient&&)" << std::endl; + //std::cout << "### TcpPrioClient::operator=(TcpClient&&)" + // << std::endl; *static_cast(this) = std::move(*static_cast(&other)); return *this; diff --git a/src/async/core/AsyncTcpPrioClientBase.cpp b/src/async/core/AsyncTcpPrioClientBase.cpp index 0693c30dd..31f10f046 100644 --- a/src/async/core/AsyncTcpPrioClientBase.cpp +++ b/src/async/core/AsyncTcpPrioClientBase.cpp @@ -158,6 +158,16 @@ class TcpPrioClientBase::Machine m.state().disconnectEvent(); } + void markAsEstablished(void) + { + ctx.marked_as_established = true; + } + + bool markedAsEstablished(void) const + { + return ctx.marked_as_established; + } + bool isIdle(void) const { return m.isActive(); @@ -231,6 +241,7 @@ class TcpPrioClientBase::Machine DnsSRVList rrs; DnsSRVList::iterator next_rr = rrs.end(); BackoffTime connect_retry_wait; + bool marked_as_established = false; Context(TcpPrioClientBase *client) : client(client), bg_con(client->newTcpClient()) {} @@ -333,7 +344,7 @@ class TcpPrioClientBase::Machine static constexpr auto NAME = "Connecting"; void entry(void) noexcept { - ctx().connect_retry_wait.reset(); + //ctx().connect_retry_wait.reset(); } }; /* StateConnecting */ @@ -367,6 +378,7 @@ class TcpPrioClientBase::Machine #endif if (!ctx().rrs.empty()) { + ctx().next_rr = ctx().rrs.end(); setState(); } else @@ -384,12 +396,7 @@ class TcpPrioClientBase::Machine void entry(void) noexcept { - auto& next_rr = ctx().next_rr = ctx().rrs.begin(); -#ifdef ASYNC_STATE_MACHINE_DEBUG - std::cout << "### Connecting to " << (*ctx().next_rr)->target() - << ":" << (*ctx().next_rr)->port() << std::endl; -#endif - ctx().connect((*next_rr)->target(), (*next_rr)->port()); + connectToNext(); } virtual void connectedEvent(void) noexcept override @@ -401,18 +408,33 @@ class TcpPrioClientBase::Machine virtual void disconnectedEvent(void) noexcept override { DEBUG_EVENT; - auto& next_rr = ++ctx().next_rr; - if (next_rr == ctx().rrs.end()) + connectToNext(); + } + + private: + void connectToNext(void) { - setState(); - return; - } + auto& next_rr = ctx().next_rr; + if (next_rr == ctx().rrs.end()) + { + next_rr = ctx().rrs.begin(); + } + else if (!ctx().marked_as_established) + { + next_rr = std::next(next_rr); + } + if (next_rr == ctx().rrs.end()) + { + setState(); + return; + } #ifdef ASYNC_STATE_MACHINE_DEBUG - std::cout << "### Connecting to " << (*ctx().next_rr)->target() - << ":" << (*ctx().next_rr)->port() << std::endl; + std::cout << "### Connecting to " << (*next_rr)->target() + << ":" << (*next_rr)->port() << std::endl; #endif - ctx().connect((*next_rr)->target(), (*next_rr)->port()); - } + ctx().marked_as_established = false; + ctx().connect((*next_rr)->target(), (*next_rr)->port()); + } }; /* StateConnectingTryConnect */ @@ -423,6 +445,10 @@ class TcpPrioClientBase::Machine void entry(void) noexcept { + if (ctx().marked_as_established) + { + ctx().connect_retry_wait.reset(); + } setTimeout(ctx().connect_retry_wait); } @@ -465,7 +491,14 @@ class TcpPrioClientBase::Machine virtual void disconnectedEvent(void) noexcept override { DEBUG_EVENT; - setState(); + if (ctx().marked_as_established) + { + setState(); + } + else + { + setState(); + } } }; /* StateConnected */ @@ -570,6 +603,8 @@ class TcpPrioClientBase::Machine std::cout << "### Connecting to " << (*ctx().next_rr)->target() << ":" << (*ctx().next_rr)->port() << std::endl; #endif + ctx().bg_con->conObj()->setRecvBufLen( + ctx().client->conObj()->recvBufLen()); ctx().bg_con->connect((*ctx().next_rr)->target(), (*ctx().next_rr)->port()); } @@ -587,8 +622,10 @@ class TcpPrioClientBase::Machine ctx().closeConnection(); ctx().emitDisconnected(TcpConnection::DR_SWITCH_PEER); } + auto ssl_ctx = ctx().client->conObj()->sslContext(); *reinterpret_cast(ctx().client) = std::move(*ctx().bg_con); + ctx().client->conObj()->setSslContext(*ssl_ctx, false); Application::app().runTask(sigc::bind( [](Context& ctx) { @@ -715,6 +752,7 @@ const std::string& TcpPrioClientBase::service(void) const void TcpPrioClientBase::connect(void) { + //m_successful_connect = false; m_machine->connect(); } /* TcpPrioClientBase::connect */ @@ -725,6 +763,19 @@ void TcpPrioClientBase::disconnect(void) } /* TcpPrioClientBase::disconnect */ +void TcpPrioClientBase::markAsEstablished(void) +{ + //std::cout << "### TcpPrioClientBase::markAsEstablished" << std::endl; + m_machine->markAsEstablished(); +} /* TcpPrioClientBase::markAsEstablished */ + + +bool TcpPrioClientBase::markedAsEstablished(void) const +{ + return m_machine->markedAsEstablished(); +} /* TcpPrioClientBase::markedAsEstablished */ + + bool TcpPrioClientBase::isIdle(void) const { return m_machine->isIdle(); diff --git a/src/async/core/AsyncTcpPrioClientBase.h b/src/async/core/AsyncTcpPrioClientBase.h index 28e7c472d..3871fc803 100644 --- a/src/async/core/AsyncTcpPrioClientBase.h +++ b/src/async/core/AsyncTcpPrioClientBase.h @@ -259,6 +259,26 @@ class TcpPrioClientBase : public TcpClientBase */ virtual void disconnect(void); + /** + * @brief Mark connection as established + * + * The application must use this function to mark a connection as + * established when the application layer deem the connection as + * successful. It is up to the application to decide this, e.g. after the + * connection has been authenticated. + * If a connection has not been marked as established when a disconnection + * occurs, a new connection will be tried again after the exponential + * backoff timer has expired. + * On the other hand, if the connection has been marked as established, a + * reconnect will be retried after the minimal reconnect delay. + */ + void markAsEstablished(void); + + /** + * @brief Check if a connection has been marked as established + */ + bool markedAsEstablished(void) const; + /** * @brief Check if the connection is idle * @return Returns \em true if the connection is idle @@ -267,8 +287,14 @@ class TcpPrioClientBase : public TcpClientBase */ bool isIdle(void) const; + /** + * @brief Check if connected to the primary server + */ bool isPrimary(void) const; + /** + * @brief Inherit the assignment operator from TcpClientBase + */ using TcpClientBase::operator=; protected: @@ -283,6 +309,19 @@ class TcpPrioClientBase : public TcpClientBase */ void initialize(void); + /** + * @brief Check if the connection has been fully connected + * @return Return \em true if the connection was successful + * + * This function return true when the connection has been fully + * established. It will continue to return true even after disconnection + * and will be reset at the moment when a new connection attempt is made. + */ + //virtual bool successfulConnect(void) override + //{ + // return m_successful_connect && TcpClientBase::successfulConnect(); + //} + /** * @brief Called when the connection has been established to the server * @@ -319,7 +358,8 @@ class TcpPrioClientBase : public TcpClientBase private: class Machine; - Machine* m_machine = nullptr; + Machine* m_machine = nullptr; + //bool m_successful_connect = false; }; /* class TcpPrioClientBase */ diff --git a/src/async/core/AsyncTcpServerBase.cpp b/src/async/core/AsyncTcpServerBase.cpp index 173e84e4a..44245d2c4 100644 --- a/src/async/core/AsyncTcpServerBase.cpp +++ b/src/async/core/AsyncTcpServerBase.cpp @@ -284,6 +284,12 @@ int TcpServerBase::writeExcept(TcpConnection *con, const void *buf, int count) } /* TcpServerBase::writeExcept */ +void TcpServerBase::setSslContext(SslContext& ctx) +{ + m_ssl_ctx = &ctx; +} /* TcpServerBase::setSslContext */ + + /**************************************************************************** * * Protected member functions @@ -292,6 +298,10 @@ int TcpServerBase::writeExcept(TcpConnection *con, const void *buf, int count) void TcpServerBase::addConnection(TcpConnection *con) { + if (m_ssl_ctx != nullptr) + { + con->setSslContext(*m_ssl_ctx, true); + } tcpConnectionList.push_back(con); } /* TcpServerBase::addConnection */ @@ -303,7 +313,7 @@ void TcpServerBase::removeConnection(TcpConnection *con) assert(it != tcpConnectionList.end()); tcpConnectionList.erase(it); Application::app().runTask([=]{ delete con; }); -} /* TcpServerBase::onDisconnected */ +} /* TcpServerBase::removeConnection */ /**************************************************************************** diff --git a/src/async/core/AsyncTcpServerBase.h b/src/async/core/AsyncTcpServerBase.h index 524e3df7d..a7a4bc3b0 100644 --- a/src/async/core/AsyncTcpServerBase.h +++ b/src/async/core/AsyncTcpServerBase.h @@ -46,6 +46,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ****************************************************************************/ #include +#include /**************************************************************************** @@ -164,6 +165,22 @@ class TcpServerBase : public sigc::trackable */ int writeExcept(TcpConnection *con, const void *buf, int count); + /** + * @brief Set the SSL context for all new connections + * @param ctx The SSL context to set + * + * Call this function to set an SSL context that is applied automatically + * for all client connections. If this is set up in the server all that + * need to be done, in a client connection after it is established, is to + * enable SSL by calling enableSsl() on the connection object. + * + * NOTE: The context object is neither copied nor managed by this class so + * it must be persisted by the caller for as long as this class or any + * connections live. It is also the responsibility for the caller to delete + * the context object when it fills no purpose anymore. + */ + void setSslContext(SslContext& ctx); + protected: virtual void createConnection(int sock, const IpAddress& remote_addr, uint16_t remote_port) = 0; @@ -176,6 +193,7 @@ class TcpServerBase : public sigc::trackable int sock; FdWatch *rd_watch; TcpConnectionList tcpConnectionList; + SslContext* m_ssl_ctx = nullptr; void cleanup(void); void onConnection(FdWatch *watch); diff --git a/src/async/core/AsyncUdpSocket.cpp b/src/async/core/AsyncUdpSocket.cpp index 8b37b9f69..f5fcd8273 100644 --- a/src/async/core/AsyncUdpSocket.cpp +++ b/src/async/core/AsyncUdpSocket.cpp @@ -156,8 +156,6 @@ class UdpPacket UdpSocket::UdpSocket(uint16_t local_port, const IpAddress &bind_ip) : sock(-1), rd_watch(0), wr_watch(0), send_buf(0) { - struct sockaddr_in addr; - // Create UDP socket sock = socket(AF_INET, SOCK_DGRAM, 0); if(sock == -1) @@ -178,6 +176,7 @@ UdpSocket::UdpSocket(uint16_t local_port, const IpAddress &bind_ip) // Bind the socket to a local port if one was specified if (local_port > 0) { + struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(local_port); @@ -189,14 +188,15 @@ UdpSocket::UdpSocket(uint16_t local_port, const IpAddress &bind_ip) { addr.sin_addr = bind_ip.ip4Addr(); } - if(::bind(sock, reinterpret_cast(&addr), sizeof(addr)) == -1) + if(::bind(sock, reinterpret_cast(&addr), + sizeof(addr)) == -1) { perror("bind"); cleanup(); return; } } - + // Setup a watch for incoming data rd_watch = new FdWatch(sock, FdWatch::FD_WATCH_RD); assert(rd_watch != 0); @@ -218,6 +218,40 @@ UdpSocket::~UdpSocket(void) } /* UdpSocket::~UdpSocket */ +Async::IpAddress UdpSocket::localAddr(void) const +{ + struct sockaddr_in addr; + socklen_t len = sizeof(addr); + int ret = getsockname(sock, reinterpret_cast(&addr), + &len); + if ((ret != 0) || (len != sizeof(addr))) + { + perror("getsockname"); + return Async::IpAddress(); + } + //std::cout << "### UdpSocket::localAddr: sin_addr=" + // << Async::IpAddress(addr.sin_addr) << std::endl; + return Async::IpAddress(addr.sin_addr); +} /* UdpSocket::localAddr */ + + +uint16_t UdpSocket::localPort(void) const +{ + struct sockaddr_in addr; + socklen_t len = sizeof(addr); + int ret = getsockname(sock, reinterpret_cast(&addr), + &len); + if ((ret != 0) || (len != sizeof(addr))) + { + perror("getsockname"); + return 0; + } + //std::cout << "### UdpSocket::localPort: sin_port=" + // << ntohs(addr.sin_port) << std::endl; + return ntohs(addr.sin_port); +} /* UdpSocket::localPort */ + + bool UdpSocket::write(const IpAddress& remote_ip, int remote_port, const void *buf, int count) { @@ -261,23 +295,11 @@ bool UdpSocket::write(const IpAddress& remote_ip, int remote_port, * ****************************************************************************/ - -/* - *------------------------------------------------------------------------ - * Method: - * Purpose: - * Input: - * Output: - * Author: - * Created: - * Remarks: - * Bugs: - *------------------------------------------------------------------------ - */ - - - - +void UdpSocket::onDataReceived(const IpAddress& ip, uint16_t port, void* buf, + int count) +{ + dataReceived(ip, port, buf, count); +} /* UdpSocket::onDataReceived */ /**************************************************************************** @@ -334,9 +356,8 @@ void UdpSocket::handleInput(FdWatch *watch) perror("recvfrom in UdpSocket::handleInput"); return; } - - dataReceived(IpAddress(addr.sin_addr), ntohs(addr.sin_port), buf, len); - + + onDataReceived(IpAddress(addr.sin_addr), ntohs(addr.sin_port), buf, len); } /* UdpSocket::handleInput */ diff --git a/src/async/core/AsyncUdpSocket.h b/src/async/core/AsyncUdpSocket.h index 0c2bc665f..5cce13a6f 100644 --- a/src/async/core/AsyncUdpSocket.h +++ b/src/async/core/AsyncUdpSocket.h @@ -1,30 +1,30 @@ /** - * @file AsyncUdpSocket.h - * @brief Contains a class for using UDP sockets - * @author Tobias Blomberg - * @date 2003-04-26 - * - * This file contains a class for communication over a UDP sockets. - * - * \verbatim - * Async - A library for programming event driven applications - * Copyright (C) 2003 Tobias Blomberg - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - * \endverbatim - */ +@file AsyncUdpSocket.h +@brief Contains a class for using UDP sockets +@author Tobias Blomberg +@date 2003-04-26 + +This file contains a class for communication over a UDP sockets. + +\verbatim +Async - A library for programming event driven applications +Copyright (C) 2003-2023 Tobias Blomberg + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +\endverbatim +*/ /** @example AsyncUdpSocket_demo.cpp An example of how to use the Async::UdpSocket class @@ -136,8 +136,8 @@ class UdpSocket : public sigc::trackable /** * @brief Destructor */ - ~UdpSocket(void); - + virtual ~UdpSocket(void); + /** * @brief Check if the initialization was ok * @return Returns \em true if everything went fine during initialization @@ -146,7 +146,19 @@ class UdpSocket : public sigc::trackable * This function should always be called after constructing the object to * see if everything went fine. */ - bool initOk(void) const { return (sock != -1); } + virtual bool initOk(void) const { return (sock != -1); } + + /** + * @brief Get the local IP address associated with this connection + * @return Returns an IP address + */ + Async::IpAddress localAddr(void) const; + + /** + * @brief Get the local UDP port associated with this connection + * @return Returns a port number + */ + uint16_t localPort(void) const; /** * @brief Write data to the remote host @@ -156,16 +168,16 @@ class UdpSocket : public sigc::trackable * @param count The number of bytes to write * @return Return \em true on success or \em false on failure */ - bool write(const IpAddress& remote_ip, int remote_port, const void *buf, - int count); + virtual bool write(const IpAddress& remote_ip, int remote_port, + const void *buf, int count); /** * @brief Get the file descriptor for the UDP socket * @return Returns the file descriptor associated with the socket or * -1 on error */ - int fd(void) const { return sock; } - + virtual int fd(void) const { return sock; } + /** * @brief A signal that is emitted when data has been received * @param ip The IP-address the data was received from @@ -183,7 +195,9 @@ class UdpSocket : public sigc::trackable sigc::signal sendBufferFull; protected: - + virtual void onDataReceived(const IpAddress& ip, uint16_t port, void* buf, + int count); + private: int sock; FdWatch * rd_watch; diff --git a/src/async/core/CMakeLists.txt b/src/async/core/CMakeLists.txt index 3ad68fc09..21a49b201 100644 --- a/src/async/core/CMakeLists.txt +++ b/src/async/core/CMakeLists.txt @@ -7,7 +7,10 @@ set(EXPINC AsyncApplication.h AsyncFdWatch.h AsyncTimer.h AsyncIpAddress.h AsyncFramedTcpConnection.h AsyncTcpClientBase.h AsyncTcpServerBase.h AsyncHttpServerConnection.h AsyncFactory.h AsyncDnsResourceRecord.h AsyncTcpPrioClientBase.h AsyncTcpPrioClient.h AsyncStateMachine.h - AsyncPlugin.h) + AsyncPlugin.h AsyncEncryptedUdpSocket.h + AsyncSslContext.h AsyncSslKeypair.h AsyncSslCertSigningReq.h + AsyncSslX509.h AsyncSslX509Extensions.h + AsyncSslX509ExtSubjectAltName.h AsyncDigest.h) set(LIBSRC AsyncApplication.cpp AsyncFdWatch.cpp AsyncTimer.cpp AsyncIpAddress.cpp AsyncDnsLookup.cpp AsyncTcpClientBase.cpp @@ -16,13 +19,16 @@ set(LIBSRC AsyncApplication.cpp AsyncFdWatch.cpp AsyncTimer.cpp AsyncSerialDevice.cpp AsyncFileReader.cpp AsyncAtTimer.cpp AsyncExec.cpp AsyncPty.cpp AsyncPtyStreamBuf.cpp AsyncFramedTcpConnection.cpp AsyncHttpServerConnection.cpp - AsyncTcpPrioClientBase.cpp AsyncPlugin.cpp) + AsyncTcpPrioClientBase.cpp AsyncPlugin.cpp + AsyncEncryptedUdpSocket.cpp) # Copy exported include files to the global include directory foreach(incfile ${EXPINC}) expinc(${incfile}) endforeach(incfile) +set(LIBS ${LIBS} -lcrypto -lssl) + # Find pthreads find_package(Threads) set(LIBS ${LIBS} ${CMAKE_THREAD_LIBS_INIT}) diff --git a/src/async/demo/AsyncDigest_demo.cpp b/src/async/demo/AsyncDigest_demo.cpp new file mode 100644 index 000000000..ff40b3553 --- /dev/null +++ b/src/async/demo/AsyncDigest_demo.cpp @@ -0,0 +1,118 @@ +#include +#include +#include +#include +#include + +#include + +int main() +{ + const std::string msg("The quick brown fox jumps over the lazy dog"); + const std::string md_algorithm("sha256"); + const std::string private_key_pem = + "-----BEGIN PRIVATE KEY-----\n" + "MIIBVgIBADANBgkqhkiG9w0BAQEFAASCAUAwggE8AgEAAkEA9hMmwek/t6lsQ4P1\n" + "mouGSfnKeeJKgQ7V10pF6eLbtgke5bGpvObJpmOC4rhBcvUWRM26fAtN28UB1uTs\n" + "lQoprwIDAQABAkAqeE21I/uiSDRuRqUqAjCwLdN7S8oOEjBoEuKUJlpDRWMTmUIi\n" + "jz5KUF8dKFUESIBr4wm0eFwvEQ3Hc0s+NOxZAiEA+2iLdJjOCIM1Cf/GWS+Zm7VE\n" + "Jigcxb4lmkCp6SieMvUCIQD6katuM9cwxciRyxRw5//eIul75UF3W5YD9+O5uDzr\n" + "kwIhAJAihtlJBc5hktXxuwDExnc7vB94Hc7MzfganI8dB13FAiEAzz85Kdda/443\n" + "jM8JwzFA4rzBnaZLdauc8v9PrccDLF0CIQD4IW5bAfBBCNozBj1937NPMGNF+Jdf\n" + "5bCIctxqutH20w==\n" + "-----END PRIVATE KEY-----\n"; + const std::string public_key_pem = + "-----BEGIN PUBLIC KEY-----\n" + "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAPYTJsHpP7epbEOD9ZqLhkn5ynniSoEO\n" + "1ddKReni27YJHuWxqbzmyaZjguK4QXL1FkTNunwLTdvFAdbk7JUKKa8CAwEAAQ==\n" + "-----END PUBLIC KEY-----\n"; + + Async::SslKeypair private_key; + if (!private_key.privateKeyFromPem(private_key_pem)) + { + std::cout << "*** ERROR: Async::Digest::privateKeyFromPem() failed" + << std::endl; + return 1; + } + std::cout << private_key.privateKeyPem() << std::endl; + + Async::SslKeypair public_key; + if (!public_key.publicKeyFromPem(public_key_pem)) + { + std::cout << "*** ERROR: Async::Digest::publicKeyFromPem() failed" + << std::endl; + return 1; + } + std::cout << public_key.publicKeyPem() << std::endl; + + Async::Digest sdgst; + if (!sdgst.signInit(md_algorithm, private_key)) + { + std::cout << "*** ERROR: Async::Digest::signInit() failed" << std::endl; + return 1; + } + const auto sig = sdgst.sign(msg); + if (sig.empty()) + { + std::cout << "*** ERROR: Async::Digest::sign() failed" << std::endl; + return 1; + } + std::cout << "Signature size: " << sig.size() << std::endl; + size_t cnt = 0; + for (const auto& byte : sig) + { + std::cout << std::setfill('0') << std::setw(2) << std::hex + << unsigned(byte) << " "; + if (++cnt % 16 == 0) + { + std::cout << std::endl; + } + } + std::cout << std::endl; + + Async::Digest vdgst; + if (!vdgst.signVerifyInit(md_algorithm, public_key)) + { + std::cout << "*** ERROR: Async::Digest::signVerifyInit() failed" + << std::endl; + return 1; + } + bool verify_ok = vdgst.signVerify(sig, msg); + std::cout << "Verify: " << (verify_ok ? "OK" : "FAIL") << "\n" << std::endl; + + + Async::Digest dgst; + if (!dgst.mdInit("sha256")) + { + std::cout << "*** ERROR: Async::Digest::init() failed" + << std::endl; + return 1; + } + if (!dgst.mdUpdate(msg)) + { + std::cout << "*** ERROR: Async::Digest::update() failed" + << std::endl; + return 1; + } + const auto sha256sum = dgst.mdFinal(); + if (sha256sum.empty()) + { + std::cout << "*** ERROR: Async::Digest::final() failed" + << std::endl; + return 1; + } + std::cout << "SHA256SUM:" << std::endl; + cnt = 0; + for (const auto& byte : sha256sum) + { + std::cout << std::setfill('0') << std::setw(2) << std::hex + << unsigned(byte) << " "; + if (++cnt % 16 == 0) + { + std::cout << std::endl; + } + } + std::cout << std::endl; + + return 0; +} diff --git a/src/async/demo/AsyncHttpServer_demo.cpp b/src/async/demo/AsyncHttpServer_demo.cpp index 2d817ed38..f3f712f83 100644 --- a/src/async/demo/AsyncHttpServer_demo.cpp +++ b/src/async/demo/AsyncHttpServer_demo.cpp @@ -1,3 +1,11 @@ +// A demo http/https server. +// Run AsyncSslX509_demo first to generate CA, key and certificate +// Use a web browser or curl to access: +// +// curl http://localhost:8080 +// curl --cacert demo_ca.crt https://localhost:8443 +// + #include #include #include @@ -63,6 +71,25 @@ void clientDisconnected(Async::HttpServerConnection *con, } /* clientConnected */ +void sslClientConnected(Async::HttpServerConnection *con) +{ + std::cout << "/// SSL Client connected: " + << con->remoteHost() << ":" << con->remotePort() << std::endl; + con->requestReceived.connect(sigc::ptr_fun(&requestReceived)); + con->enableSsl(true); +} /* sslClientConnected */ + + +void sslClientDisconnected(Async::HttpServerConnection *con, + Async::HttpServerConnection::DisconnectReason reason) +{ + std::cout << "\\\\\\ SSL Client disconnected: " + << con->remoteHost() << ":" << con->remotePort() + << ": " << Async::HttpServerConnection::disconnectReasonStr(reason) + << std::endl; +} /* sslClientConnected */ + + int main(void) { Async::CppApplication app; @@ -70,8 +97,19 @@ int main(void) app.catchUnixSignal(SIGTERM); app.unixSignalCaught.connect( sigc::hide(sigc::mem_fun(app, &Async::CppApplication::quit))); + + // Listen for http connections on TCP port 8080 Async::TcpServer server("8080"); server.clientConnected.connect(sigc::ptr_fun(&clientConnected)); server.clientDisconnected.connect(sigc::ptr_fun(&clientDisconnected)); + + // Listen for https connections on TCP port 8443 + Async::TcpServer ssl_server("8443"); + Async::SslContext ctx; + ctx.setCertificateFiles("demo.key", "demo.crt"); + ssl_server.setSslContext(ctx); + ssl_server.clientConnected.connect(sigc::ptr_fun(&sslClientConnected)); + ssl_server.clientDisconnected.connect(sigc::ptr_fun(&sslClientDisconnected)); + app.exec(); } diff --git a/src/async/demo/AsyncSslTcpClient_demo.cpp b/src/async/demo/AsyncSslTcpClient_demo.cpp new file mode 100644 index 000000000..792951fed --- /dev/null +++ b/src/async/demo/AsyncSslTcpClient_demo.cpp @@ -0,0 +1,89 @@ +/****************************************************************************** +# To generate the client key- and certificate files: +openssl req -newkey rsa:2048 -nodes -keyout client.key \ + -subj "/CN=MYCALL" -sha256 -days 3650 -out client.csr + +# Sign the client certificat using the server key +openssl x509 -req -in client.csr -CA server.crt \ + -CAkey server.key -CAcreateserial -days 3650 -out client.crt +******************************************************************************/ + +#include +#include +#include +#include + +using namespace std; +using namespace Async; + +class MyClass : public sigc::trackable +{ + public: + MyClass(std::string hostname, uint16_t port) : hostname(hostname) + { + con = new TcpClient<>(hostname, port); + if (!m_ssl_ctx.setCertificateFiles("client.key", "client.crt")) + { + std::cout << "*** ERROR: Failed to read key/cert files" << std::endl; + exit(1); + } + if (!m_ssl_ctx.setCaCertificateFile("server.crt")) + { + std::cout << "*** ERROR: Failed to read CA file" << std::endl; + exit(1); + } + con->setSslContext(m_ssl_ctx); + con->connected.connect(mem_fun(*this, &MyClass::onConnected)); + con->disconnected.connect(mem_fun(*this, &MyClass::onDisconnected)); + con->dataReceived.connect(mem_fun(*this, &MyClass::onDataReceived)); + con->connect(); + } + + ~MyClass(void) + { + delete con; + } + + private: + TcpClient<>* con; + std::string hostname; + SslContext m_ssl_ctx; + + void onConnected(void) + { + cout << "Connection established to " << con->remoteHost() << "...\n"; + con->enableSsl(true); + std::ostringstream os; + os << "GET / HTTP/1.0\r\n" + "Connection: Close\r\n" + "Host: " << hostname << "\r\n" + "\r\n"; + con->write(os.str().data(), os.str().size()); + } + + void onDisconnected(TcpConnection *con, TcpClient<>::DisconnectReason reason) + { + cout << "Disconnected from " << con->remoteHost() << "...\n"; + Application::app().quit(); + } + + int onDataReceived(TcpConnection *con, void *buf, int count) + { + cout.write(static_cast(buf), count); + cout << std::endl; + Application::app().quit(); + return count; + } +}; + +int main(int argc, char **argv) +{ + CppApplication app; + if (argc < 3) + { + std::cout << "Usage: " << argv[0] << " " << std::endl; + exit(1); + } + MyClass my_class(argv[1], atoi(argv[2])); + app.exec(); +} diff --git a/src/async/demo/AsyncSslTcpServer_demo.cpp b/src/async/demo/AsyncSslTcpServer_demo.cpp new file mode 100644 index 000000000..35d7ae2f3 --- /dev/null +++ b/src/async/demo/AsyncSslTcpServer_demo.cpp @@ -0,0 +1,183 @@ +/****************************************************************************** +# To generate the server key- and certificate files: +openssl req -x509 -newkey rsa:2048 -nodes -keyout server.key \ + -subj "/CN=localhost" -sha256 -days 3650 -out server.crt +******************************************************************************/ + +#include +#include + +#include + +#include +#include +#include + +class MySslServer +{ + public: + MySslServer(const std::string& service) + : m_server(service), m_ping_timer(1000, Async::Timer::TYPE_PERIODIC) + { + if (!m_ssl_ctx.setCertificateFiles("server.key", "server.crt")) + { + std::cout << "*** ERROR: Failed to read key/cert files" << std::endl; + exit(1); + } + if (!m_ssl_ctx.setCaCertificateFile("server.crt")) + { + std::cout << "*** ERROR: Failed to read CA file" << std::endl; + exit(1); + } + + std::string session_id("AsyncSslTcpServer_demo"); + SSL_CTX_set_session_id_context( + m_ssl_ctx, + reinterpret_cast(session_id.data()), + session_id.size()); + + m_server.setSslContext(m_ssl_ctx); + m_server.clientConnected.connect( + sigc::mem_fun(*this, &MySslServer::onClientConnected)); + + // Start a timer to periofically send some data to all clients + //m_ping_timer.expired.connect( + // [&](Async::Timer*) + // { + // m_server.writeAll("PING\n", 5); + // }); + std::cout << "Connect using: \"openssl s_client -connect localhost:" + << service << "\" " "from another console" << std::endl; + } + + private: + Async::TcpServer m_server; + Async::SslContext m_ssl_ctx; + Async::Timer m_ping_timer; + + void onClientConnected(Async::TcpConnection *con) + { + std::cout << "Client " << con->remoteHost() << ":" + << con->remotePort() << " connected, " + << m_server.numberOfClients() << " clients connected\n"; + con->enableSsl(true); + con->verifyPeer.connect( + [](Async::TcpConnection* con, bool preverify_ok, + X509_STORE_CTX *ctx) + { + X509* err_cert = X509_STORE_CTX_get_current_cert(ctx); + assert(err_cert != nullptr); + int err = X509_STORE_CTX_get_error(ctx); + int depth = X509_STORE_CTX_get_error_depth(ctx); + + /* + * Retrieve the pointer to the SSL of the connection currently treated + * and the application specific data stored into the SSL object. + */ + //SSL* ssl = X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx()); + + char buf[256]; + auto subj = X509_get_subject_name(err_cert); + int lastpos = -1; + for (;;) + { + lastpos = X509_NAME_get_index_by_NID(subj, + NID_commonName, lastpos); + if (lastpos == -1) + { + break; + } + X509_NAME_ENTRY *e = X509_NAME_get_entry(subj, lastpos); + ASN1_STRING *d = X509_NAME_ENTRY_get_data(e); + const unsigned char* str = (ASN1_STRING_get0_data(d)); + std::cout << "### CN=" << str << std::endl; + } + X509_NAME_oneline(subj, buf, sizeof(buf)); + + + /* + * Catch a too long certificate chain. The depth limit set using + * SSL_CTX_set_verify_depth() is by purpose set to "limit+1" so + * that whenever the "depth>verify_depth" condition is met, we + * have violated the limit and want to log this error condition. + * We must do it here, because the CHAIN_TOO_LONG error would not + * be found explicitly; only errors introduced by cutting off the + * additional certificates would be logged. + */ + if (depth > 1) + { + preverify_ok = false; + err = X509_V_ERR_CERT_CHAIN_TOO_LONG; + X509_STORE_CTX_set_error(ctx, err); + } + if (!preverify_ok) + { + std::cout << "*** ERROR: Verify error:num=" << err << ":" + << X509_verify_cert_error_string(err) << ":depth=" + << depth << ":" << buf << std::endl; + } + else if (true) + { + std::cout << "### depth=" << depth << ":" << buf << std::endl; + } + + /* + * At this point, err contains the last verification error. We can use + * it for something special + */ + if (!preverify_ok && (err == X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT)) + { + X509_NAME_oneline(X509_get_issuer_name(err_cert), + buf, sizeof(buf)); + std::cout << "### issuer=" << buf << std::endl; + } + + return preverify_ok; + }); + con->dataReceived.connect( + [](Async::TcpConnection* con, void* buf, int len) + { + std::cout << "SSL data received: len=" << len; + std::cout << " data="; + std::cout.write(reinterpret_cast(buf), len); + if (memcmp(buf, "GET ", 4) == 0) + { + std::string res; + res += "HTTP/1.1 200 OK\r\n"; + res += "Content-Type: text/plain\r\n"; + res += "Content-Length: 15\r\n"; + res += "Connection: Close\r\n"; + res += "\r\n"; + res += "Hello, Client\r\n"; + con->write(res.data(), res.size()); + } + return len; + }); + con->disconnected.connect( + [&](Async::TcpConnection *con, + Async::TcpConnection::DisconnectReason reason) + { + std::cout << "Client " << con->remoteHost().toString() << ":" + << con->remotePort() << " disconnected, " + << m_server.numberOfClients() << " clients connected\n"; + }); + } +}; /* class MySslServer */ + +int main(int argc, char **argv) +{ + Async::CppApplication app; + app.unixSignalCaught.connect([&](int signal) + { + std::cout << "Caught signal. Exiting..." << std::endl; + app.quit(); + }); + app.catchUnixSignal(SIGINT); + app.catchUnixSignal(SIGTERM); + + MySslServer ssl_server("12345"); + + app.exec(); + + return 0; +} /* main */ diff --git a/src/async/demo/AsyncSslX509_demo.cpp b/src/async/demo/AsyncSslX509_demo.cpp new file mode 100644 index 000000000..7c5107e6e --- /dev/null +++ b/src/async/demo/AsyncSslX509_demo.cpp @@ -0,0 +1,121 @@ +#include +#include +#include +#include + +int main(void) +{ + // Create a key pair for the CA + Async::SslKeypair ca_pkey; + if (!ca_pkey.generate(2048)) + { + std::cout << "*** ERROR: Failed to generate CA key" << std::endl; + return 1; + } + if (!ca_pkey.writePrivateKeyFile("demo_ca.key")) + { + std::cout << "*** WARNING: Failed to write CA key file" << std::endl; + } + + // Create a CA certificate and sign it with the key above + Async::SslX509 ca_cert; + ca_cert.setSerialNumber(1); + ca_cert.setVersion(Async::SslX509::VERSION_3); + ca_cert.addIssuerName("CN", "Demo Root CA"); + ca_cert.addIssuerName("L", "My City"); + ca_cert.addIssuerName("C", "XX"); + ca_cert.setSubjectName(ca_cert.issuerName()); + Async::SslX509Extensions ca_exts; + ca_exts.addBasicConstraints("critical, CA:TRUE"); + ca_exts.addKeyUsage("critical, cRLSign, digitalSignature, keyCertSign"); + ca_exts.addSubjectAltNames("email:ca@example.org"); + ca_cert.addExtensions(ca_exts); + time_t t = time(nullptr); + ca_cert.setNotBefore(t); + ca_cert.setNotAfter(t + 24*3600); + ca_cert.setPublicKey(ca_pkey); + ca_cert.sign(ca_pkey); + std::cout << "--------------- CA Certificate ----------------" << std::endl; + ca_cert.print(); + std::cout << "-----------------------------------------------" << std::endl; + if (!ca_cert.writePemFile("demo_ca.crt")) + { + std::cout << "*** WARNING: Failed to write CA certificate file" + << std::endl; + } + + // Create a key pair for the server certificate + Async::SslKeypair cert_pkey; + if (!cert_pkey.generate(2048)) + { + std::cout << "*** ERROR: Failed to generate server certificate key" + << std::endl; + return 1; + } + if (!cert_pkey.writePrivateKeyFile("demo.key")) + { + std::cout << "*** WARNING: Failed to write CA key file" << std::endl; + } + + // Create a Certificate Signing Request + Async::SslCertSigningReq csr; + csr.setVersion(Async::SslCertSigningReq::VERSION_1); + csr.addSubjectName("CN", "hostname.example.org"); + csr.addSubjectName("L", "My City"); + csr.addSubjectName("C", "XX"); + Async::SslX509Extensions csr_exts; + csr_exts.addSubjectAltNames( + "DNS:hostname.example.org" + ", DNS:alias.example.org" + ", DNS:localhost" + ", IP:127.0.0.1" + ", email:admin@example.org" + ", URI:https://www.example.org" + ", otherName:msUPN;UTF8:sb@sb.local"); + csr.addExtensions(csr_exts); + csr.setPublicKey(cert_pkey); + csr.sign(cert_pkey); + std::cout << "--------- Certificate Signing Request ---------" << std::endl; + csr.print(); + std::cout << "-----------------------------------------------" << std::endl; + if (!csr.writePemFile("demo.csr")) + { + std::cout << "*** WARNING: Failed to write CSR file" << std::endl; + } + std::cout << "The CSR verification " + << (csr.verify(cert_pkey) ? "PASSED" : "FAILED") + << std::endl; + + // Create the certificate using the CSR then sign it using the CA cert + Async::SslX509 cert; + cert.setSerialNumber(2); + cert.setVersion(Async::SslX509::VERSION_3); + cert.setIssuerName(ca_cert.subjectName()); + cert.setSubjectName(csr.subjectName()); + cert.setNotBefore(t); + cert.setNotAfter(t + 3600); + const Async::SslX509Extensions exts(csr.extensions()); + Async::SslX509Extensions cert_exts; + cert_exts.addBasicConstraints("critical, CA:FALSE"); + cert_exts.addKeyUsage("critical, nonRepudiation, digitalSignature, keyEncipherment, keyAgreement"); + cert_exts.addExtKeyUsage("serverAuth"); + Async::SslX509ExtSubjectAltName san(exts.subjectAltName()); + cert_exts.addExtension(san); + cert.addExtensions(cert_exts); + Async::SslKeypair csr_pkey(csr.publicKey()); + cert.setPublicKey(csr_pkey); + cert.sign(ca_pkey); + std::cout << "------------- Server Certificate --------------" << std::endl; + cert.print(); + std::cout << "-----------------------------------------------" << std::endl; + if (!cert.writePemFile("demo.crt")) + { + std::cout << "*** WARNING: Failed to write certificate file" + << std::endl; + } + std::cout << "The certificate verification " + << (cert.verify(ca_pkey) ? "PASSED" : "FAILED") + << std::endl; + + return 0; +} diff --git a/src/async/demo/AsyncTcpClient_demo.cpp b/src/async/demo/AsyncTcpClient_demo.cpp index 8485761db..d3e559225 100644 --- a/src/async/demo/AsyncTcpClient_demo.cpp +++ b/src/async/demo/AsyncTcpClient_demo.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -7,12 +8,12 @@ using namespace Async; class MyClass : public sigc::trackable { public: - MyClass(void) + MyClass(std::string hostname, uint16_t port) { con.connected.connect(mem_fun(*this, &MyClass::onConnected)); con.disconnected.connect(mem_fun(*this, &MyClass::onDisconnected)); con.dataReceived.connect(mem_fun(*this, &MyClass::onDataReceived)); - con.connect("www.svxlink.org", 80); + con.connect(hostname, port); } private: @@ -24,6 +25,7 @@ class MyClass : public sigc::trackable << std::endl; std::string req( "GET / HTTP/1.0\r\n" + "Connection: Close\r\n" "Host: " + con.remoteHostName() + "\r\n" "\r\n"); std::cout << "--- Sending request:\n" << req << std::endl; @@ -41,9 +43,8 @@ class MyClass : public sigc::trackable int onDataReceived(TcpConnection *, void *buf, int count) { std::cout << "--- Data received:" << std::endl; - const char *str = static_cast(buf); - std::string html(str, str+count); - std::cout << html; + std::cout.write(static_cast(buf), count); + std::cout << std::endl; return count; } }; @@ -51,6 +52,6 @@ class MyClass : public sigc::trackable int main(int argc, char **argv) { CppApplication app; - MyClass my_class; + MyClass my_class("checkip.amazonaws.com", 80); app.exec(); } diff --git a/src/async/demo/CMakeLists.txt b/src/async/demo/CMakeLists.txt index 2394aad1d..ed87e5034 100644 --- a/src/async/demo/CMakeLists.txt +++ b/src/async/demo/CMakeLists.txt @@ -7,6 +7,8 @@ set(CPPPROGS AsyncAudioIO_demo AsyncDnsLookup_demo AsyncFdWatch_demo AsyncAudioFsf_demo AsyncHttpServer_demo AsyncFactory_demo AsyncAudioContainer_demo AsyncTcpPrioClient_demo AsyncStateMachine_demo AsyncPlugin_demo + AsyncSslTcpServer_demo AsyncSslTcpClient_demo + AsyncSslX509_demo AsyncDigest_demo ) set(QTPROGS AsyncQtApplication_demo) diff --git a/src/config.h.in b/src/config.h.in index 53b3073f8..e3baab548 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -7,6 +7,7 @@ #define SVX_SHARE_INSTALL_DIR "@SVX_SHARE_INSTALL_DIR@" #define SVX_MODULE_INSTALL_DIR "@SVX_MODULE_INSTALL_DIR@" #define SVX_LOGIC_CORE_INSTALL_DIR "@SVX_LOGIC_CORE_INSTALL_DIR@" +#define SVX_LOCAL_STATE_DIR "@SVX_LOCAL_STATE_DIR@" #define PROJECT_VERSION "@PROJECT_VERSION@" #endif /* CONFIG_H_INCLUDED */ diff --git a/src/doc/man/svxlink.conf.5 b/src/doc/man/svxlink.conf.5 index 484ac4cc6..70e68209e 100644 --- a/src/doc/man/svxlink.conf.5 +++ b/src/doc/man/svxlink.conf.5 @@ -1,4 +1,4 @@ -.TH SVXLINK.CONF 5 "JANUARY 2024" Linux "File Formats" +.TH SVXLINK.CONF 5 "JULY 2024" Linux "File Formats" . .SH NAME . @@ -624,10 +624,19 @@ configurable so that a talk group can be automatically selected when it's active. After a configurable timeout the node will return to monitor all configured talk groups if there is no activity on the currently selected talk group. Which talk group to select for outgoing traffic for a node can be set to -a default and/or selected using DTMF commands. The DTMF commands are entered -through the logic linking definition associated with the reflector. So if a -command prefix of 9 have been specified for a link, selecting talk group 888 -will be achieved by entering the command 91888#. +a default, selected by the CTCSS tone used and/or selected using DTMF +commands. The DTMF commands are entered through the logic linking definition +associated with the reflector. So if a command prefix of 9 have been +specified for a link, selecting talk group 888 will be achieved by entering +the command 91888#. +.P +The reflector connection will be authenticated using X.509 certificates. Both +the server and the client requires authentication. The AUTH_KEY authentication +method that were previously used is deprecated. The certificate generation +process is mostly automated. The contents of the certificate can be customized +using the configuration variables with name prefix CERT_. Default values are +good in most cases. CERT_EMAIL may be good to specify so that the SvxLink node +owner can be contacted. Ask the reflector sysop what the convention is. .TP .B TYPE The type for a reflector logic core is always @@ -787,6 +796,88 @@ It is also possible to set audio codec parameters using the same configuration variables as documented for networked receivers and transmitters. For example, to lighten the encoder CPU load for the Opus encoder, set OPUS_ENC_COMPLEXITY to something lower than 9. +.TP +.B CERT_PKI_DIR +The path to the directory where PKI (Public Key Infrastructure) files will be +stored. + +Example: CERT_PKI_DIR=/var/lib/svxlink/pki +.TP +.B CERT_KEYFILE +The path to the private key for the X.509 client certificate. This file will +be auto generated if it does not exist. It must be kept secure since this is +what authenticates your node. It must NOT be sent in the clear over the +internet for example. Also, if you loose this file you will have to ask the +reflector sysop to issue a new certificate for the new key. + +Example: CERT_KEYFILE=/var/lib/svxlink/pki/MYCALL.key +.TP +.B CERT_CSRFILE +The path to the certificate signing request for the X.509 client certificate. +This file will be auto generated if it does not exist or if the configuration +information has been changed. When a new CSR is generated, the reflector sysop +need to sign it before it will become active. + +Example: CERT_CSRFILE=/var/lib/svxlink/pki/MYCALL.csr +.TP +.B CERT_CRTFILE +The path to the X.509 client certificate. This file is a public file that is +used to identify your node. It is sent to the reflector during the +authentication procedure. It will be created after the CSR has been signed by +the reflector sysop. The reflector will send this file to the client during +the authentication procedure if it's missing on the node or if the certificate +has been updated. + +Example: CERT_CRTFILE=/var/lib/svxlink/pki/MYCALL.crt +.TP +.B CERT_CAFILE +The path to the X.509 certificate bundle. This file contains one or more +certificates that is used to verify the authenticity of the reflector server. + +Example: CERT_CAFILE=/var/lib/svxlink/pki/ca-bundle.pem +.TP +.B CERT_SUBJ_GN, CERT_SUBJ_givenName +The name of the person, with which s/he is normally called, owning the +SvxLink node. + +Example: CERT_SUBJ_GN=John +.TP +.B CERT_SUBJ_SN, CERT_SUBJ_surname +The family name of the person owning the SvxLink node. + +Example: CERT_SUBJ_SN=Doe +.TP +.B CERT_SUBJ_OU, CERT_SUBJ_organizationalUnitName +The name of the organizational unit to which the SvxLink node belongs. That +may be a subdivision of a ham radio organization for example. + +Example: CERT_SUBJ_OU="VHF/UHF Unit" +.TP +.B CERT_SUBJ_O, CERT_SUBJ_organizationName +The name of the organization to which the SvxLink node belongs. That may be a +ham radio club for example. + +Example: CERT_SUBJ_O=SSA +.TP +.B CERT_SUBJ_L, CERT_SUBJ_localityName +The name of the city where the SvxLink node is located. + +Example: CERT_SUBJ_L=Stockholm +.TP +.B CERT_SUBJ_ST, CERT_SUBJ_stateOrProvinceName +The name of the state or province where the SvxLink node is located. + +Example: CERT_SUBJ_ST=Södermanland +.TP +.B CERT_SUBJ_C, CERT_SUBJ_countryName +The ISO 3166 country code for the country where the SvxLink node is located. + +Example: CERT_SUBJ_C=SE +.TP +.B CERT_EMAIL +The email address where the SvxLink node owner can be reached. + +Example: CERT_EMAIL=mycall@example.com . .SS QSO Recorder Section . diff --git a/src/doc/man/svxreflector.conf.5 b/src/doc/man/svxreflector.conf.5 index 3bc852605..47fe886e6 100644 --- a/src/doc/man/svxreflector.conf.5 +++ b/src/doc/man/svxreflector.conf.5 @@ -1,4 +1,4 @@ -.TH SVXREFLECTOR.CONF 5 "APRIL 2021" Linux "File Formats" +.TH SVXREFLECTOR.CONF 5 "JULY 2024" Linux "File Formats" . .SH NAME . @@ -150,6 +150,103 @@ device like this: echo "CFG section varname value" > /dev/shm/reflector_ctrl e.g. echo "CFG GLOBAL SQL_TIMEOUT_BLOCKTIME 60" > /dev/shm/reflector_ctrl + +To sign a client certificate: + + echo "CA SIGN callsign" > /dev/shm/reflector_ctrl +.TP +.B CALLSIGN_MATCH +A regular expression for verifying that the common name in client certificates +really contain a valid callsign. The default is to roughly match standard ham +radio callsigns. +.TP +.B CERT_PKI_DIR +The path to the directory containing PKI (Public Key Infrastructure) files. If +a relative path is given, the value of the build time variable +SVX_LOCAL_STATE_DIR (e.g. /var/lib/svxlink/) will be prepended. + +Default: CERT_PKI_DIR=pki/ +.TP +.B CERT_CA_KEYS_DIR +The path to the directory containing private key files. If +a relative path is given, the value of the CERT_PKI_DIR variable will be +prepended. + +Default: CERT_CA_KEYS_DIR=private/ +.TP +.B CERT_CA_PENDING_CSRS_DIR +The path to the directory containing unsigned CSR files. If a relative path is +given, the value of the CERT_PKI_DIR variable will be prepended. + +Default: CERT_CA_PENDING_CSRS_DIR=pending_csrs/ +.TP +.B CERT_CA_CSRS_DIR +The path to the directory containing signed CSR files. If a relative path is +given, the value of the CERT_PKI_DIR variable will be prepended. + +Default: CERT_CA_CSRS_DIR=csrs/ +.TP +.B CERT_CA_CERTS_DIR +The path to the directory containing signed certificate files. If a relative +path is given, the value of the CERT_PKI_DIR variable will be prepended. + +Default: CERT_CA_CERTS_DIR=certs/ +. +.SS ROOT_CA, ISSUING_CA and SERVER_CERT sections +. +These configuration sections is used to customize the contents of the different +certificates that is generated by the reflector server. +.TP +.B KEYFILE +The path to the certificate private key file. If a relative path is given, the +value of the CERT_CA_KEYS_DIR variable will be prepended. +.TP +.B CSRFILE +The path to the certificate signing request file. If a relative path is given, +the value of the CERT_CA_CSRS_DIR variable will be prepended. +.TP +.B CRTFILE +The path to the certificate file. If a relative path is given, the value of the +CERT_CA_CERTS_DIR variable will be prepended. +.TP +.B COMMON_NAME +The common name (CN) used in the subject of the certificate. +.TP +.B ORG_UNIT +The organizational unit (OU) used in the subject of the certificate. + +Example: ORG_UNIT="VHF/UHF-sektionen" +.TP +.B ORG +The organization name (O) used in the subject of the certificate. + +Example: ORG=SSA +.TP +.B LOCALITY +The locality name (L) used in the subject of the certificate. + +Example: LOCALITY=Boden +.TP +.B STATE +The name of the state or province (ST) used in the subject of the certificate. + +Example: STATE=Norrbotten +.TP +.B COUNTRY +The ISO country code (C) used in the subject of the certificate. + +Example: COUNTRY=SE +.TP +.B SUBJECT_ALT_NAME +A comma separated list of subject alternative names. + +Example: SUBJECT_ALT_NAME=DNS:public-hostname.example.org,IP:172.17.1.42 +.TP +.B EMAIL_ADDRESS=sysop@svxlink.example.org +The email address that can be used to get in contact with the owner of the +certificate. + +Example: EMAIL_ADDRESS=sysop@svxlink.example.org . .SS USERS and PASSWORDS sections . diff --git a/src/echolib/EchoLinkDirectory.cpp b/src/echolib/EchoLinkDirectory.cpp index e579a93b7..226a8db11 100644 --- a/src/echolib/EchoLinkDirectory.cpp +++ b/src/echolib/EchoLinkDirectory.cpp @@ -847,11 +847,7 @@ void Directory::ctrlSockDisconnected(void) error(string("Directory server communications error: ") + strerror(errno)); break; - - case Async::TcpClient<>::DR_RECV_BUFFER_OVERFLOW: - error("Directory server receiver buffer overflow!\n"); - break; - + case Async::TcpClient<>::DR_ORDERED_DISCONNECT: break; } diff --git a/src/svxlink/ChangeLog b/src/svxlink/ChangeLog index b63f24588..7d77bc0d3 100644 --- a/src/svxlink/ChangeLog +++ b/src/svxlink/ChangeLog @@ -5,6 +5,28 @@ Contribution from DL1HRC/Adi, following an idea from DJ1JAY/Jens to change some SvxReflector parameters at runtime. +* The reflector connection is now authenticated using X.509 certificates. Both + the server and the client requires authentication. The AUTH_KEY + authentication method that were previously used is deprecated. The + certificate generation process is mostly automated. The contents of the + certificate can be customized using the configuration variables with name + prefix CERT_. Default values are good in most cases. CERT_EMAIL may be good + to specify so that the SvxLink node owner can be contacted. Ask the + reflector sysop what the convention is. + Both the TCP and UDP connections are encrypted. The default is to use + the AES128 cipher in GCM mode. + The reflector server need to be updated to use this version of SvxLink on a + reflector client. If the server is older and only support the version 2 + protocol, you must use TYPE=ReflectorV2 in the ReflectorLogic config. + All PKI (Public Key Infrastructure) files will be stored in the directory + given by the CERT_PKI_DIR configuration variable. The path defaults to + something like /var/lib/svxlink/pki but the exact default path depend on + build configuration. In any case, that path need to be created and made + writable for SvxLink. That should be done automatically by "make install". + Building SvxLink also require a new dependency on OpenSSL so the development + package for that library need to be installed (e.g. install package + libssl-dev if on a Debian based distro). + 1.8.0 -- 25 Feb 2024 diff --git a/src/svxlink/modules/frn/QsoFrn.cpp b/src/svxlink/modules/frn/QsoFrn.cpp index f1168ce8f..4a65d6dd1 100644 --- a/src/svxlink/modules/frn/QsoFrn.cpp +++ b/src/svxlink/modules/frn/QsoFrn.cpp @@ -249,8 +249,8 @@ QsoFrn::QsoFrn(ModuleFrn *module) mem_fun(*this, &QsoFrn::onDisconnected)); tcp_client->dataReceived.connect( mem_fun(*this, &QsoFrn::onDataReceived)); - tcp_client->sendBufferFull.connect( - mem_fun(*this, &QsoFrn::onSendBufferFull)); + //tcp_client->sendBufferFull.connect( + // mem_fun(*this, &QsoFrn::onSendBufferFull)); this->rxVoiceStarted.connect( mem_fun(*this, &QsoFrn::onRxVoiceStarted)); @@ -844,10 +844,10 @@ void QsoFrn::onDisconnected(TcpConnection *conn, needs_reconnect = true; break; - case TcpConnection::DR_RECV_BUFFER_OVERFLOW: - cout << "DR_RECV_BUFFER_OVERFLOW" << endl; - setState(STATE_ERROR); - break; + //case TcpConnection::DR_RECV_BUFFER_OVERFLOW: + // cout << "DR_RECV_BUFFER_OVERFLOW" << endl; + // setState(STATE_ERROR); + // break; case TcpConnection::DR_ORDERED_DISCONNECT: cout << "DR_ORDERED_DISCONNECT" << endl; diff --git a/src/svxlink/reflector/CMakeLists.txt b/src/svxlink/reflector/CMakeLists.txt index d3a6e84c9..4468ef29f 100644 --- a/src/svxlink/reflector/CMakeLists.txt +++ b/src/svxlink/reflector/CMakeLists.txt @@ -5,10 +5,10 @@ include_directories(${POPT_INCLUDE_DIRS}) add_definitions(${POPT_DEFINITIONS}) # Find the GCrypt library -find_package(GCrypt REQUIRED) -set(LIBS ${LIBS} ${GCRYPT_LIBRARIES}) -include_directories(${GCRYPT_INCLUDE_DIRS}) -add_definitions(${GCRYPT_DEFINITIONS}) +#find_package(GCrypt REQUIRED) +#set(LIBS ${LIBS} ${GCRYPT_LIBRARIES}) +#include_directories(${GCRYPT_INCLUDE_DIRS}) +#add_definitions(${GCRYPT_DEFINITIONS}) # Find jsoncpp library #find_package(jsoncpp REQUIRED) @@ -34,4 +34,5 @@ set_target_properties(svxreflector PROPERTIES # Install targets install(TARGETS svxreflector DESTINATION ${BIN_INSTALL_DIR}) install_if_not_exists(svxreflector.conf ${SVX_SYSCONF_INSTALL_DIR}) -install(PROGRAMS svxreflector-status DESTINATION ${BIN_INSTALL_DIR}) +install(PROGRAMS svxreflector-status svxreflector-ca + DESTINATION ${BIN_INSTALL_DIR}) diff --git a/src/svxlink/reflector/Reflector.cpp b/src/svxlink/reflector/Reflector.cpp index d72e9dd50..faec27468 100644 --- a/src/svxlink/reflector/Reflector.cpp +++ b/src/svxlink/reflector/Reflector.cpp @@ -32,6 +32,11 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include #include +#include +#include +#include +#include +#include /**************************************************************************** @@ -42,10 +47,13 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include #include -#include +#include +#include +#include #include #include #include +#include /**************************************************************************** @@ -76,6 +84,7 @@ using namespace Async; * ****************************************************************************/ +#define RENEW_AFTER 2/3 /**************************************************************************** @@ -88,10 +97,82 @@ using namespace Async; /**************************************************************************** * - * Prototypes + * Local functions * ****************************************************************************/ +namespace { + //void splitFilename(const std::string& filename, std::string& dirname, + // std::string& basename) + //{ + // std::string ext; + // basename = filename; + + // size_t basenamepos = filename.find_last_of('/'); + // if (basenamepos != string::npos) + // { + // if (basenamepos + 1 < filename.size()) + // { + // basename = filename.substr(basenamepos + 1); + // } + // dirname = filename.substr(0, basenamepos + 1); + // } + + // size_t extpos = basename.find_last_of('.'); + // if (extpos != string::npos) + // { + // if (extpos+1 < basename.size()) + // ext = basename.substr(extpos+1); + // basename.erase(extpos); + // } + //} + + bool ensureDirectoryExist(const std::string& path) + { + std::vector parts; + SvxLink::splitStr(parts, path, "/"); + std::string dirname; + if (path[0] == '/') + { + dirname = "/"; + } + else if (path[0] != '.') + { + dirname = "./"; + } + if (path.back() != '/') + { + parts.erase(std::prev(parts.end())); + } + for (const auto& part : parts) + { + dirname += part + "/"; + if (access(dirname.c_str(), F_OK) != 0) + { + std::cout << "Create directory '" << dirname << "'" << std::endl; + if (mkdir(dirname.c_str(), 0755) != 0) + { + std::cerr << "*** ERROR: Could not create directory '" + << dirname << "'" << std::endl; + return false; + } + } + } + return true; + } /* ensureDirectoryExist */ + + + void startCertRenewTimer(const Async::SslX509& cert, Async::AtTimer& timer) + { + int days=0, seconds=0; + cert.timeSpan(days, seconds); + time_t renew_time = cert.notBefore() + + (static_cast(days)*24*3600 + seconds)*RENEW_AFTER; + timer.setTimeout(renew_time); + timer.setExpireOffset(10000); + timer.start(); + } /* startCertRenewTimer */ +}; /**************************************************************************** @@ -111,8 +192,10 @@ using namespace Async; namespace { ReflectorClient::ProtoVerRangeFilter v1_client_filter( ProtoVer(1, 0), ProtoVer(1, 999)); - ReflectorClient::ProtoVerRangeFilter v2_client_filter( - ProtoVer(2, 0), ProtoVer(2, 999)); + //ReflectorClient::ProtoVerRangeFilter v2_client_filter( + // ProtoVer(2, 0), ProtoVer(2, 999)); + ReflectorClient::ProtoVerLargerOrEqualFilter ge_v2_client_filter( + ProtoVer(2, 0)); }; @@ -124,12 +207,32 @@ namespace { Reflector::Reflector(void) : m_srv(0), m_udp_sock(0), m_tg_for_v1_clients(1), m_random_qsy_lo(0), - m_random_qsy_hi(0), m_random_qsy_tg(0), m_http_server(0), m_cmd_pty(0) + m_random_qsy_hi(0), m_random_qsy_tg(0), m_http_server(0), m_cmd_pty(0), + m_keys_dir("private/"), m_pending_csrs_dir("pending_csrs/"), + m_csrs_dir("csrs/"), m_certs_dir("certs/"), m_pki_dir("pki/") { TGHandler::instance()->talkerUpdated.connect( mem_fun(*this, &Reflector::onTalkerUpdated)); TGHandler::instance()->requestAutoQsy.connect( mem_fun(*this, &Reflector::onRequestAutoQsy)); + m_renew_cert_timer.expired.connect( + [&](Async::AtTimer*) + { + if (!loadServerCertificateFiles()) + { + std::cerr << "*** WARNING: Failed to renew server certificate" + << std::endl; + } + }); + m_renew_issue_ca_cert_timer.expired.connect( + [&](Async::AtTimer*) + { + if (!loadSigningCAFiles()) + { + std::cerr << "*** WARNING: Failed to renew issuing CA certificate" + << std::endl; + } + }); } /* Reflector::Reflector */ @@ -154,28 +257,6 @@ bool Reflector::initialize(Async::Config &cfg) m_cfg = &cfg; TGHandler::instance()->setConfig(m_cfg); - // Initialize the GCrypt library if not already initialized - if (!gcry_control(GCRYCTL_INITIALIZATION_FINISHED_P)) - { - gcry_check_version(NULL); - gcry_error_t err; - err = gcry_control(GCRYCTL_DISABLE_SECMEM, 0); - if (err != GPG_ERR_NO_ERROR) - { - cerr << "*** ERROR: Failed to initialize the Libgcrypt library: " - << gcry_strsource(err) << "/" << gcry_strerror(err) << endl; - return false; - } - // Tell Libgcrypt that initialization has completed - err = gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0); - if (err != GPG_ERR_NO_ERROR) - { - cerr << "*** ERROR: Failed to initialize the Libgcrypt library: " - << gcry_strsource(err) << "/" << gcry_strerror(err) << endl; - return false; - } - } - std::string listen_port("5300"); cfg.getValue("GLOBAL", "LISTEN_PORT", listen_port); m_srv = new TcpServer(listen_port); @@ -184,14 +265,29 @@ bool Reflector::initialize(Async::Config &cfg) m_srv->clientDisconnected.connect( mem_fun(*this, &Reflector::clientDisconnected)); + if (!loadCertificateFiles()) + { + return false; + } + + m_srv->setSslContext(m_ssl_ctx); + uint16_t udp_listen_port = 5300; cfg.getValue("GLOBAL", "LISTEN_PORT", udp_listen_port); - m_udp_sock = new UdpSocket(udp_listen_port); - if ((m_udp_sock == 0) || !m_udp_sock->initOk()) + m_udp_sock = new Async::EncryptedUdpSocket(udp_listen_port); + const char* err = "unknown reason"; + if ((err="bad allocation", (m_udp_sock == 0)) || + (err="initialization failure", !m_udp_sock->initOk()) || + (err="unsupported cipher", !m_udp_sock->setCipher(UdpCipher::NAME))) { - cerr << "*** ERROR: Could not initialize UDP socket" << endl; + std::cerr << "*** ERROR: Could not initialize UDP socket due to " + << err << std::endl; return false; } + m_udp_sock->setCipherAADLength(UdpCipher::AADLEN); + m_udp_sock->setTagLength(UdpCipher::TAGLEN); + m_udp_sock->cipherDataReceived.connect( + mem_fun(*this, &Reflector::udpCipherDataReceived)); m_udp_sock->dataReceived.connect( mem_fun(*this, &Reflector::udpDatagramReceived)); @@ -282,11 +378,40 @@ void Reflector::broadcastMsg(const ReflectorMsg& msg, } /* Reflector::broadcastMsg */ -bool Reflector::sendUdpDatagram(ReflectorClient *client, const void *buf, - size_t count) +bool Reflector::sendUdpDatagram(ReflectorClient *client, + const ReflectorUdpMsg& msg) { - return m_udp_sock->write(client->remoteHost(), client->remoteUdpPort(), buf, - count); + if (client->protoVer() >= ProtoVer(3, 0)) + { + ReflectorUdpMsg header(msg.type()); + ostringstream ss; + assert(header.pack(ss) && msg.pack(ss)); + + m_udp_sock->setCipherIV(client->udpCipherIV()); + m_udp_sock->setCipherKey(client->udpCipherKey()); + UdpCipher::AAD aad{client->udpCipherIVCntrNext()}; + std::stringstream aadss; + if (!aad.pack(aadss)) + { + std::cout << "*** WARNING: Packing associated data failed for UDP " + "datagram to " << client->remoteHost() << ":" + << client->remotePort() << std::endl; + return false; + } + return m_udp_sock->write(client->remoteHost(), client->remoteUdpPort(), + aadss.str().data(), aadss.str().size(), + ss.str().data(), ss.str().size()); + } + else + { + ReflectorUdpMsgV2 header(msg.type(), client->clientId(), + client->udpCipherIVCntrNext() & 0xffff); + ostringstream ss; + assert(header.pack(ss) && msg.pack(ss)); + return m_udp_sock->UdpSocket::write( + client->remoteHost(), client->remoteUdpPort(), + ss.str().data(), ss.str().size()); + } } /* Reflector::sendUdpDatagram */ @@ -326,11 +451,152 @@ void Reflector::requestQsy(ReflectorClient *client, uint32_t tg) broadcastMsg(MsgRequestQsy(tg), ReflectorClient::mkAndFilter( - v2_client_filter, + ge_v2_client_filter, ReflectorClient::TgFilter(current_tg))); } /* Reflector::requestQsy */ +Async::SslCertSigningReq +Reflector::loadClientPendingCsr(const std::string& callsign) +{ + Async::SslCertSigningReq csr; + (void)csr.readPemFile(m_pending_csrs_dir + "/" + callsign + ".csr"); + return csr; +} /* Reflector::loadClientPendingCsr */ + + +Async::SslCertSigningReq +Reflector::loadClientCsr(const std::string& callsign) +{ + Async::SslCertSigningReq csr; + (void)csr.readPemFile(m_csrs_dir + "/" + callsign + ".csr"); + return csr; +} /* Reflector::loadClientPendingCsr */ + + +bool Reflector::signClientCert(Async::SslX509& cert) +{ + //std::cout << "### Reflector::signClientCert" << std::endl; + + cert.setSerialNumber(); + cert.setIssuerName(m_issue_ca_cert.subjectName()); + time_t tnow = time(NULL); + cert.setNotBefore(tnow); + cert.setNotAfter(tnow + CERT_VALIDITY_TIME); + auto cn = cert.commonName(); + if (!cert.sign(m_issue_ca_pkey)) + { + std::cerr << "*** ERROR: Certificate signing failed for client " + << cn << std::endl; + return false; + } + auto crtfile = m_certs_dir + "/" + cn + ".crt"; + if (!cert.writePemFile(crtfile) || !m_issue_ca_cert.appendPemFile(crtfile)) + { + std::cerr << "*** WARNING: Failed to write client certificate file '" + << crtfile << "'" << std::endl; + } + return true; +} /* Reflector::signClientCert */ + + +Async::SslX509 Reflector::signClientCsr(const std::string& cn) +{ + //std::cout << "### Reflector::signClientCsr" << std::endl; + + Async::SslX509 cert(nullptr); + + auto req = loadClientPendingCsr(cn); + if (req.isNull()) + { + std::cerr << "*** ERROR: Cannot find CSR to sign '" << req.filePath() + << "'" << std::endl; + return cert; + } + + cert.clear(); + cert.setVersion(Async::SslX509::VERSION_3); + cert.setSubjectName(req.subjectName()); + const Async::SslX509Extensions exts(req.extensions()); + Async::SslX509Extensions cert_exts; + cert_exts.addBasicConstraints("critical, CA:FALSE"); + cert_exts.addKeyUsage( + "critical, digitalSignature, keyEncipherment, keyAgreement"); + cert_exts.addExtKeyUsage("clientAuth"); + Async::SslX509ExtSubjectAltName san(exts.subjectAltName()); + cert_exts.addExtension(san); + cert.addExtensions(cert_exts); + Async::SslKeypair csr_pkey(req.publicKey()); + cert.setPublicKey(csr_pkey); + + if (!signClientCert(cert)) + { + cert.set(nullptr); + } + + std::string csr_path = m_csrs_dir + "/" + cn + ".csr"; + if (rename(req.filePath().c_str(), csr_path.c_str()) != 0) + { + char errstr[256]; + (void)strerror_r(errno, errstr, sizeof(errstr)); + std::cerr << "*** WARNING: Failed to move signed CSR from '" + << req.filePath() << "' to '" << csr_path << "': " + << errstr << std::endl; + } + + auto client = ReflectorClient::lookup(cn); + if ((client != nullptr) && !cert.isNull()) + { + client->certificateUpdated(cert); + } + + return cert; +} /* Reflector::signClientCsr */ + + +Async::SslX509 Reflector::loadClientCertificate(const std::string& callsign) +{ + Async::SslX509 cert; + if (!cert.readPemFile(m_certs_dir + "/" + callsign + ".crt") || + cert.isNull() || + !cert.verify(m_issue_ca_pkey) || + !cert.timeIsWithinRange()) + { + return nullptr; + } + return cert; +} /* Reflector::loadClientCertificate */ + + +std::string Reflector::clientCertPem(const std::string& callsign) const +{ + std::string crtfile(m_certs_dir + "/" + callsign + ".crt"); + std::ifstream ifs(crtfile); + if (!ifs.good()) + { + return std::string(); + } + return std::string(std::istreambuf_iterator{ifs}, {}); +} /* Reflector::clientCertPem */ + + +std::string Reflector::caBundlePem(void) const +{ + std::ifstream ifs(m_ca_bundle_file); + if (ifs.good()) + { + return std::string(std::istreambuf_iterator{ifs}, {}); + } + return std::string(); +} /* Reflector::caBundlePem */ + + +std::string Reflector::issuingCertPem(void) const +{ + return m_issue_ca_cert.pem(); +} /* Reflector::issuingCertPem */ + + /**************************************************************************** * * Protected member functions @@ -347,9 +613,13 @@ void Reflector::requestQsy(ReflectorClient *client, uint32_t tg) void Reflector::clientConnected(Async::FramedTcpConnection *con) { - cout << "Client " << con->remoteHost() << ":" << con->remotePort() - << " connected" << endl; - m_client_con_map[con] = new ReflectorClient(this, con, m_cfg); + std::cout << con->remoteHost() << ":" << con->remotePort() + << ": Client connected" << endl; + ReflectorClient *client = new ReflectorClient(this, con, m_cfg); + con->verifyPeer.connect(sigc::mem_fun(*this, &Reflector::onVerifyPeer)); + client->csrReceived.connect( + sigc::mem_fun(*this, &Reflector::onCsrReceived)); + m_client_con_map[con] = client; } /* Reflector::clientConnected */ @@ -368,9 +638,9 @@ void Reflector::clientDisconnected(Async::FramedTcpConnection *con, } else { - cout << "Client " << con->remoteHost() << ":" << con->remotePort() << " "; + std::cout << con->remoteHost() << ":" << con->remotePort() << ": "; } - cout << "disconnected: " << TcpConnection::disconnectReasonStr(reason) + std::cout << "Client disconnected: " << TcpConnection::disconnectReasonStr(reason) << endl; m_client_con_map.erase(it); @@ -384,11 +654,96 @@ void Reflector::clientDisconnected(Async::FramedTcpConnection *con, } /* Reflector::clientDisconnected */ +bool Reflector::udpCipherDataReceived(const IpAddress& addr, uint16_t port, + void *buf, int count) +{ + if ((count <= 0) || (static_cast(count) < UdpCipher::AADLEN)) + { + std::cout << "### : Ignoring too short UDP datagram (" << count + << " bytes)" << std::endl; + return true; + } + + stringstream ss; + ss.write(reinterpret_cast(buf), UdpCipher::AADLEN); + assert(m_aad.unpack(ss)); + + ReflectorClient* client = nullptr; + if (m_aad.iv_cntr == 0) + { + UdpCipher::InitialAAD iaad; + //std::cout << "### Reflector::udpCipherDataReceived: m_aad.iv_cntr=" + // << m_aad.iv_cntr << std::endl; + if (static_cast(count) < iaad.packedSize()) + { + std::cout << "### Reflector::udpCipherDataReceived: " + "Ignoring malformed UDP registration datagram" << std::endl; + return true; + } + ss.clear(); + ss.write(reinterpret_cast(buf)+UdpCipher::AADLEN, + sizeof(UdpCipher::ClientId)); + + Async::MsgPacker::unpack(ss, iaad.client_id); + //std::cout << "### Reflector::udpCipherDataReceived: client_id=" + // << iaad.client_id << std::endl; + auto client = ReflectorClient::lookup(iaad.client_id); + if (client == nullptr) + { + std::cout << "### Could not find client id (" << iaad.client_id + << ") specified in initial AAD datagram" << std::endl; + return true; + } + m_udp_sock->setCipherIV(UdpCipher::IV{client->udpCipherIVRand(), + client->clientId(), 0}); + m_udp_sock->setCipherKey(client->udpCipherKey()); + m_udp_sock->setCipherAADLength(iaad.packedSize()); + } + else if ((client=ReflectorClient::lookup(std::make_pair(addr, port)))) + { + //if (static_cast(count) < UdpCipher::AADLEN) + //{ + // std::cout << "### Reflector::udpCipherDataReceived: Datagram too short " + // "to hold associated data" << std::endl; + // return true; + //} + + //if (!aad_unpack_ok) + //{ + // std::cout << "*** WARNING: Unpacking associated data failed for UDP " + // "datagram from " << addr << ":" << port << std::endl; + // return true; + //} + //std::cout << "### Reflector::udpCipherDataReceived: m_aad.iv_cntr=" + // << m_aad.iv_cntr << std::endl; + m_udp_sock->setCipherIV(UdpCipher::IV{client->udpCipherIVRand(), + client->clientId(), m_aad.iv_cntr}); + m_udp_sock->setCipherKey(client->udpCipherKey()); + m_udp_sock->setCipherAADLength(UdpCipher::AADLEN); + } + else + { + udpDatagramReceived(addr, port, nullptr, buf, count); + return true; + } + + return false; +} /* Reflector::udpCipherDataReceived */ + + void Reflector::udpDatagramReceived(const IpAddress& addr, uint16_t port, - void *buf, int count) + void* aadptr, void *buf, int count) { + //std::cout << "### Reflector::udpDatagramReceived:" + // << " addr=" << addr + // << " port=" << port + // << " count=" << count + // << std::endl; + + assert(m_udp_sock->cipherAADLength() >= UdpCipher::AADLEN); + stringstream ss; - ss.write(reinterpret_cast(buf), count); + ss.write(reinterpret_cast(buf), static_cast(count)); ReflectorUdpMsg header; if (!header.unpack(ss)) @@ -397,14 +752,96 @@ void Reflector::udpDatagramReceived(const IpAddress& addr, uint16_t port, "from " << addr << ":" << port << endl; return; } + ReflectorUdpMsgV2 header_v2; - ReflectorClient *client = ReflectorClient::lookup(header.clientId()); - if (client == nullptr) + ReflectorClient* client = nullptr; + UdpCipher::AAD aad; + if (aadptr != nullptr) { - cerr << "*** WARNING: Incoming UDP datagram from " << addr << ":" << port - << " has invalid client id " << header.clientId() << endl; - return; + //std::cout << "### Reflector::udpDatagramReceived: m_aad.iv_cntr=" + // << m_aad.iv_cntr << std::endl; + + stringstream aadss; + aadss.write(reinterpret_cast(aadptr), + m_udp_sock->cipherAADLength()); + + if (!aad.unpack(aadss)) + { + return; + } + if (aad.iv_cntr == 0) // Client UDP registration + { + UdpCipher::InitialAAD iaad; + assert(aadss.seekg(0)); + if (!iaad.unpack(aadss)) + { + std::cout << "### Reflector::udpDatagramReceived: " + "Could not unpack iaad" << std::endl; + return; + } + assert(iaad.iv_cntr == 0); + //std::cout << "### Reflector::udpDatagramReceived: iaad.client_id=" + // << iaad.client_id << std::endl; + client = ReflectorClient::lookup(iaad.client_id); + if (client == nullptr) + { + std::cout << "### Reflector::udpDatagramReceived: Could not find " + "client id " << iaad.client_id << std::endl; + return; + } + else if (client->remoteUdpPort() == 0) + { + //client->setRemoteUdpPort(port); + } + else + { + std::cout << "### Reflector::udpDatagramReceived: Client " + << iaad.client_id << " already registered." << std::endl; + } + client->setUdpRxSeq(0); + //client->sendUdpMsg(MsgUdpHeartbeat()); + } + else + { + client = ReflectorClient::lookup(std::make_pair(addr, port)); + if (client == nullptr) + { + std::cout << "### Unknown client " << addr << ":" << port << std::endl; + return; + } + } + } + else + { + ss.seekg(0); + if (!header_v2.unpack(ss)) + { + std::cout << "*** WARNING: Unpacking V2 message header failed for UDP " + "datagram from " << addr << ":" << port << std::endl; + return; + } + client = ReflectorClient::lookup(header_v2.clientId()); + if (client == nullptr) + { + std::cerr << "*** WARNING: Incoming V2 UDP datagram from " << addr << ":" + << port << " has invalid client id " << header_v2.clientId() + << std::endl; + return; + } } + + //auto client = ReflectorClient::lookup(std::make_pair(addr, port)); + //if (client == nullptr) + //{ + // client = ReflectorClient::lookup(header.clientId()); + // if (client == nullptr) + // { + // cerr << "*** WARNING: Incoming UDP datagram from " << addr << ":" << port + // << " has invalid client id " << header.clientId() << endl; + // return; + // } + //} + if (addr != client->remoteHost()) { cerr << "*** WARNING[" << client->callsign() @@ -417,7 +854,7 @@ void Reflector::udpDatagramReceived(const IpAddress& addr, uint16_t port, client->setRemoteUdpPort(port); client->sendUdpMsg(MsgUdpHeartbeat()); } - else if (port != client->remoteUdpPort()) + if (port != client->remoteUdpPort()) { cerr << "*** WARNING[" << client->callsign() << "]: Incoming UDP packet has the wrong source UDP " @@ -427,24 +864,50 @@ void Reflector::udpDatagramReceived(const IpAddress& addr, uint16_t port, } // Check sequence number - uint16_t udp_rx_seq_diff = header.sequenceNum() - client->nextUdpRxSeq(); - if (udp_rx_seq_diff > 0x7fff) // Frame out of sequence (ignore) + if (client->protoVer() >= ProtoVer(3, 0)) { - cout << client->callsign() - << ": Dropping out of sequence frame with seq=" - << header.sequenceNum() << ". Expected seq=" - << client->nextUdpRxSeq() << endl; - return; + if (aad.iv_cntr < client->nextUdpRxSeq()) // Frame out of sequence (ignore) + { + std::cout << client->callsign() + << ": Dropping out of sequence UDP frame with seq=" + << aad.iv_cntr << std::endl; + return; + } + else if (aad.iv_cntr > client->nextUdpRxSeq()) // Frame lost + { + std::cout << client->callsign() << ": UDP frame(s) lost. Expected seq=" + << client->nextUdpRxSeq() + << " but received " << aad.iv_cntr + << ". Resetting next expected sequence number to " + << (aad.iv_cntr + 1) << std::endl; + } + client->setUdpRxSeq(aad.iv_cntr + 1); } - else if (udp_rx_seq_diff > 0) // Frame(s) lost + else { - cout << client->callsign() - << ": UDP frame(s) lost. Expected seq=" << client->nextUdpRxSeq() - << ". Received seq=" << header.sequenceNum() << endl; + uint16_t next_udp_rx_seq = client->nextUdpRxSeq() & 0xffff; + uint16_t udp_rx_seq_diff = header_v2.sequenceNum() - next_udp_rx_seq; + if (udp_rx_seq_diff > 0x7fff) // Frame out of sequence (ignore) + { + std::cout << client->callsign() + << ": Dropping out of sequence frame with seq=" + << header_v2.sequenceNum() << ". Expected seq=" + << next_udp_rx_seq << std::endl; + return; + } + else if (udp_rx_seq_diff > 0) // Frame(s) lost + { + cout << client->callsign() + << ": UDP frame(s) lost. Expected seq=" << next_udp_rx_seq + << ". Received seq=" << header_v2.sequenceNum() << endl; + } + client->setUdpRxSeq(header_v2.sequenceNum() + 1); } client->udpMsgReceived(header); + //std::cout << "### Reflector::udpDatagramReceived: type=" + // << header.type() << std::endl; switch (header.type()) { case MsgUdpHeartbeat::TYPE: @@ -596,7 +1059,7 @@ void Reflector::onTalkerUpdated(uint32_t tg, ReflectorClient* old_talker, cout << old_talker->callsign() << ": Talker stop on TG #" << tg << endl; broadcastMsg(MsgTalkerStop(tg, old_talker->callsign()), ReflectorClient::mkAndFilter( - v2_client_filter, + ge_v2_client_filter, ReflectorClient::mkOrFilter( ReflectorClient::TgFilter(tg), ReflectorClient::TgMonitorFilter(tg)))); @@ -614,7 +1077,7 @@ void Reflector::onTalkerUpdated(uint32_t tg, ReflectorClient* old_talker, cout << new_talker->callsign() << ": Talker start on TG #" << tg << endl; broadcastMsg(MsgTalkerStart(tg, new_talker->callsign()), ReflectorClient::mkAndFilter( - v2_client_filter, + ge_v2_client_filter, ReflectorClient::mkOrFilter( ReflectorClient::TgFilter(tg), ReflectorClient::TgMonitorFilter(tg)))); @@ -655,6 +1118,11 @@ void Reflector::httpRequestReceived(Async::HttpServerConnection *con, for (const auto& item : m_client_con_map) { ReflectorClient* client = item.second; + if (client->conState() != ReflectorClient::STATE_CONNECTED) + { + continue; + } + Json::Value node(client->nodeInfo()); //node["addr"] = client->remoteHost().toString(); node["protoVer"]["majorVer"] = client->protoVer().majorVer(); @@ -774,7 +1242,7 @@ void Reflector::onRequestAutoQsy(uint32_t from_tg) broadcastMsg(MsgRequestQsy(tg), ReflectorClient::mkAndFilter( - v2_client_filter, + ge_v2_client_filter, ReflectorClient::TgFilter(from_tg))); } /* Reflector::onRequestAutoQsy */ @@ -820,18 +1288,87 @@ void Reflector::ctrlPtyDataReceived(const void *buf, size_t count) errss << "Invalid PTY command '" << cmdline << "'"; goto write_status; } + std::transform(cmd.begin(), cmd.end(), cmd.begin(), ::toupper); if (cmd == "CFG") { std::string section, tag, value; if (!(ss >> section >> tag >> value) || !ss.eof()) { - errss << "Invalid PTY command '" << cmdline << "'. " + errss << "Invalid CFG PTY command '" << cmdline << "'. " "Usage: CFG
"; goto write_status; } m_cfg->setValue(section, tag, value); } + else if (cmd == "CA") + { + std::string subcmd; + if (!(ss >> subcmd)) + { + errss << "Invalid CA PTY command '" << cmdline << "'. " + "Usage: CA PENDING|SIGN |LS|RM "; + goto write_status; + } + std::transform(subcmd.begin(), subcmd.end(), subcmd.begin(), ::toupper); + if (subcmd == "SIGN") + { + std::string cn; + if (!(ss >> cn)) + { + errss << "Invalid CA SIGN PTY command '" << cmdline << "'. " + "Usage: CA SIGN "; + goto write_status; + } + auto cert = signClientCsr(cn); + if (!cert.isNull()) + { + std::cout << "------------- Client Certificate --------------" + << std::endl; + cert.print(" "); + std::cout << "-----------------------------------------------" + << std::endl; + } + else + { + errss << "Certificate signing failed"; + } + } + else if (subcmd == "RM") + { + std::string cn; + if (!(ss >> cn)) + { + errss << "Invalid CA RM PTY command '" << cmdline << "'. " + "Usage: CA RM "; + goto write_status; + } + if (removeClientCert(cn)) + + { + std::cout << cn << ": Removed client certificate and CSR" + << std::endl; + } + else + { + errss << "Failed to remove certificate and CSR for '" << cn << "'"; + } + } + else if (subcmd == "LS") + { + errss << "Not yet implemented"; + } + else if (subcmd == "PENDING") + { + errss << "Not yet implemented"; + } + else + { + errss << "Invalid CA PTY command '" << cmdline << "'. " + "Usage: CA PENDING|SIGN |LS|RM "; + goto write_status; + } + } else { errss << "Unknown PTY command '" << cmdline @@ -889,6 +1426,686 @@ void Reflector::cfgUpdated(const std::string& section, const std::string& tag) } /* Reflector::cfgUpdated */ +bool Reflector::loadCertificateFiles(void) +{ + if (!buildPath("GLOBAL", "CERT_PKI_DIR", SVX_LOCAL_STATE_DIR, m_pki_dir) || + !buildPath("GLOBAL", "CERT_CA_KEYS_DIR", m_pki_dir, m_keys_dir) || + !buildPath("GLOBAL", "CERT_CA_PENDING_CSRS_DIR", m_pki_dir, + m_pending_csrs_dir) || + !buildPath("GLOBAL", "CERT_CA_CSRS_DIR", m_pki_dir, m_csrs_dir) || + !buildPath("GLOBAL", "CERT_CA_CERTS_DIR", m_pki_dir, m_certs_dir)) + { + return false; + } + + if (!loadRootCAFiles() || !loadSigningCAFiles() || + !loadServerCertificateFiles()) + { + return false; + } + + if (!m_cfg->getValue("GLOBAL", "CERT_CA_BUNDLE", m_ca_bundle_file)) + { + m_ca_bundle_file = m_pki_dir + "/ca-bundle.crt"; + } + if (access(m_ca_bundle_file.c_str(), F_OK) != 0) + { + if (!ensureDirectoryExist(m_ca_bundle_file) || + !m_ca_cert.writePemFile(m_ca_bundle_file)) + { + std::cout << "*** ERROR: Failed to write CA bundle file '" + << m_ca_bundle_file << "'" << std::endl; + return false; + } + } + if (!m_ssl_ctx.setCaCertificateFile(m_ca_bundle_file)) + { + std::cout << "*** ERROR: Failed to read CA certificate bundle '" + << m_ca_bundle_file << "'" << std::endl; + return false; + } + + struct stat st; + if (stat(m_ca_bundle_file.c_str(), &st) != 0) + { + char errstr[256]; + (void)strerror_r(errno, errstr, sizeof(errstr)); + std::cerr << "*** ERROR: Failed to read CA file from '" + << m_ca_bundle_file << "': " << errstr << std::endl; + return false; + } + auto bundle = caBundlePem(); + m_ca_size = bundle.size(); + Async::Digest ca_dgst; + if (!ca_dgst.md(m_ca_md, MsgCABundle::MD_ALG, bundle)) + { + std::cerr << "*** ERROR: CA bundle checksumming failed" + << std::endl; + return false; + } + ca_dgst.signInit(MsgCABundle::MD_ALG, m_issue_ca_pkey); + m_ca_sig = ca_dgst.sign(bundle); + m_ca_url = ""; + m_cfg->getValue("GLOBAL", "CERT_CA_URL", m_ca_url); + + return true; +} /* Reflector::loadCertificateFiles */ + + +bool Reflector::loadServerCertificateFiles(void) +{ + std::string cert_cn; + if (!m_cfg->getValue("SERVER_CERT", "COMMON_NAME", cert_cn) || + cert_cn.empty()) + { + std::cerr << "*** ERROR: The 'SERVER_CERT/COMMON_NAME' variable is " + "unset which is needed for certificate signing request " + "generation." << std::endl; + return false; + } + + std::string keyfile; + if (!m_cfg->getValue("SERVER_CERT", "KEYFILE", keyfile)) + { + keyfile = m_keys_dir + "/" + cert_cn + ".key"; + } + Async::SslKeypair pkey; + if (access(keyfile.c_str(), F_OK) != 0) + { + std::cout << "Server private key file not found. Generating '" + << keyfile << "'" << std::endl; + if (!generateKeyFile(pkey, keyfile)) + { + return false; + } + } + else if (!pkey.readPrivateKeyFile(keyfile)) + { + std::cerr << "*** ERROR: Failed to read private key file from '" + << keyfile << "'" << std::endl; + return false; + } + + if (!m_cfg->getValue("SERVER_CERT", "CRTFILE", m_crtfile)) + { + m_crtfile = m_certs_dir + "/" + cert_cn + ".crt"; + } + Async::SslX509 cert; + bool generate_cert = (access(m_crtfile.c_str(), F_OK) != 0); + if (!generate_cert) + { + generate_cert = !cert.readPemFile(m_crtfile) || + !cert.verify(m_issue_ca_pkey); + if (generate_cert) + { + std::cerr << "*** WARNING: Failed to read server certificate " + "from '" << m_crtfile << "' or the cert is invalid. " + "Generating new certificate." << std::endl; + cert.clear(); + } + else + { + int days=0, seconds=0; + cert.timeSpan(days, seconds); + //std::cout << "### days=" << days << " seconds=" << seconds + // << std::endl; + time_t tnow = time(NULL); + time_t renew_time = tnow + (days*24*3600 + seconds)*RENEW_AFTER; + if (!cert.timeIsWithinRange(tnow, renew_time)) + { + std::cerr << "Time to renew the server certificate '" << m_crtfile + << "'. It's valid until " + << cert.notAfterLocaltimeString() << "." << std::endl; + cert.clear(); + generate_cert = true; + } + } + } + if (generate_cert) + { + //if (!pkey_fresh && !generateKeyFile(pkey, keyfile)) + //{ + // return false; + //} + + std::string csrfile; + if (!m_cfg->getValue("SERVER_CERT", "CSRFILE", csrfile)) + { + csrfile = m_csrs_dir + "/" + cert_cn + ".csr"; + } + Async::SslCertSigningReq req; + std::cout << "Generating server certificate signing request file '" + << csrfile << "'" << std::endl; + req.setVersion(Async::SslCertSigningReq::VERSION_1); + req.addSubjectName("CN", cert_cn); + Async::SslX509Extensions req_exts; + req_exts.addBasicConstraints("critical, CA:FALSE"); + req_exts.addKeyUsage( + "critical, digitalSignature, keyEncipherment, keyAgreement"); + req_exts.addExtKeyUsage("serverAuth"); + std::stringstream csr_san_ss; + csr_san_ss << "DNS:" << cert_cn; + std::string cert_san_str; + if (m_cfg->getValue("SERVER_CERT", "SUBJECT_ALT_NAME", cert_san_str) && + !cert_san_str.empty()) + { + csr_san_ss << "," << cert_san_str; + } + std::string email_address; + if (m_cfg->getValue("SERVER_CERT", "EMAIL_ADDRESS", email_address) && + !email_address.empty()) + { + csr_san_ss << ",email:" << email_address; + } + req_exts.addSubjectAltNames(csr_san_ss.str()); + req.addExtensions(req_exts); + req.setPublicKey(pkey); + req.sign(pkey); + if (!req.writePemFile(csrfile)) + { + // FIXME: Read SSL error stack + + std::cerr << "*** WARNING: Failed to write server certificate " + "signing request file to '" << csrfile << "'" + << std::endl; + //return false; + } + std::cout << "-------- Certificate Signing Request -------" << std::endl; + req.print(); + std::cout << "--------------------------------------------" << std::endl; + + std::cout << "Generating server certificate file '" << m_crtfile << "'" + << std::endl; + cert.setSerialNumber(); + cert.setVersion(Async::SslX509::VERSION_3); + cert.setIssuerName(m_issue_ca_cert.subjectName()); + cert.setSubjectName(req.subjectName()); + time_t tnow = time(NULL); + cert.setNotBefore(tnow); + cert.setNotAfter(tnow + CERT_VALIDITY_TIME); + cert.addExtensions(req.extensions()); + cert.setPublicKey(pkey); + cert.sign(m_issue_ca_pkey); + assert(cert.verify(m_issue_ca_pkey)); + if (!ensureDirectoryExist(m_crtfile) || !cert.writePemFile(m_crtfile) || + !m_issue_ca_cert.appendPemFile(m_crtfile)) + { + std::cout << "*** ERROR: Failed to write server certificate file '" + << m_crtfile << "'" << std::endl; + return false; + } + } + std::cout << "------------ Server Certificate ------------" << std::endl; + cert.print(); + std::cout << "--------------------------------------------" << std::endl; + + if (!m_ssl_ctx.setCertificateFiles(keyfile, m_crtfile)) + { + std::cout << "*** ERROR: Failed to read and verify key ('" + << keyfile << "') and certificate ('" + << m_crtfile << "') files. " + << "If key- and cert-file does not match, the certificate " + "is invalid for any other reason, you need " + "to remove the cert file in order to trigger the " + "generation of a new certificate signing request." + "Then the CSR need to be signed by the CA which creates a " + "valid certificate." + << std::endl; + return false; + } + + startCertRenewTimer(cert, m_renew_cert_timer); + + return true; +} /* Reflector::loadServerCertificateFiles */ + + +bool Reflector::generateKeyFile(Async::SslKeypair& pkey, + const std::string& keyfile) +{ + pkey.generate(2048); + if (!ensureDirectoryExist(keyfile) || !pkey.writePrivateKeyFile(keyfile)) + { + std::cerr << "*** ERROR: Failed to write private key file to '" + << keyfile << "'" << std::endl; + return false; + } + return true; +} /* Reflector::generateKeyFile */ + + +bool Reflector::loadRootCAFiles(void) +{ + // Read root CA private key or generate a new one if it does not exist + std::string ca_keyfile; + if (!m_cfg->getValue("ROOT_CA", "KEYFILE", ca_keyfile)) + { + ca_keyfile = m_keys_dir + "/svxreflector_root_ca.key"; + } + if (access(ca_keyfile.c_str(), F_OK) != 0) + { + std::cout << "Root CA private key file not found. Generating '" + << ca_keyfile << "'" << std::endl; + if (!m_ca_pkey.generate(4096)) + { + std::cout << "*** ERROR: Failed to generate root CA key" << std::endl; + return false; + } + if (!ensureDirectoryExist(ca_keyfile) || + !m_ca_pkey.writePrivateKeyFile(ca_keyfile)) + { + std::cerr << "*** ERROR: Failed to write root CA private key file to '" + << ca_keyfile << "'" << std::endl; + return false; + } + } + else if (!m_ca_pkey.readPrivateKeyFile(ca_keyfile)) + { + std::cerr << "*** ERROR: Failed to read root CA private key file from '" + << ca_keyfile << "'" << std::endl; + return false; + } + + // Read the root CA certificate or generate a new one if it does not exist + std::string ca_crtfile; + if (!m_cfg->getValue("ROOT_CA", "CRTFILE", ca_crtfile)) + { + ca_crtfile = m_certs_dir + "/svxreflector_root_ca.crt"; + } + bool generate_ca_cert = (access(ca_crtfile.c_str(), F_OK) != 0); + if (!generate_ca_cert) + { + if (!m_ca_cert.readPemFile(ca_crtfile) || + !m_ca_cert.verify(m_ca_pkey) || + !m_ca_cert.timeIsWithinRange()) + { + std::cerr << "*** ERROR: Failed to read root CA certificate file " + "from '" << ca_crtfile << "' or the cert is invalid." + << std::endl; + return false; + } + } + if (generate_ca_cert) + { + std::cout << "Generating root CA certificate file '" << ca_crtfile << "'" + << std::endl; + m_ca_cert.setSerialNumber(); + m_ca_cert.setVersion(Async::SslX509::VERSION_3); + + std::string value; + value = "SvxReflector Root CA"; + (void)m_cfg->getValue("ROOT_CA", "COMMON_NAME", value); + if (value.empty()) + { + std::cerr << "*** ERROR: The 'ROOT_CA/COMMON_NAME' variable is " + "unset which is needed for root CA certificate generation." + << std::endl; + return false; + } + m_ca_cert.addIssuerName("CN", value); + if (m_cfg->getValue("ROOT_CA", "ORG_UNIT", value) && + !value.empty()) + { + m_ca_cert.addIssuerName("OU", value); + } + if (m_cfg->getValue("ROOT_CA", "ORG", value) && !value.empty()) + { + m_ca_cert.addIssuerName("O", value); + } + if (m_cfg->getValue("ROOT_CA", "LOCALITY", value) && + !value.empty()) + { + m_ca_cert.addIssuerName("L", value); + } + if (m_cfg->getValue("ROOT_CA", "STATE", value) && !value.empty()) + { + m_ca_cert.addIssuerName("ST", value); + } + if (m_cfg->getValue("ROOT_CA", "COUNTRY", value) && !value.empty()) + { + m_ca_cert.addIssuerName("C", value); + } + m_ca_cert.setSubjectName(m_ca_cert.issuerName()); + Async::SslX509Extensions ca_exts; + ca_exts.addBasicConstraints("critical, CA:TRUE"); + ca_exts.addKeyUsage("critical, cRLSign, digitalSignature, keyCertSign"); + if (m_cfg->getValue("ROOT_CA", "EMAIL_ADDRESS", value) && + !value.empty()) + { + ca_exts.addSubjectAltNames("email:" + value); + } + m_ca_cert.addExtensions(ca_exts); + time_t tnow = time(NULL); + m_ca_cert.setNotBefore(tnow); + m_ca_cert.setNotAfter(tnow + 25*365*24*3600); + m_ca_cert.setPublicKey(m_ca_pkey); + m_ca_cert.sign(m_ca_pkey); + if (!m_ca_cert.writePemFile(ca_crtfile)) + { + std::cout << "*** ERROR: Failed to write root CA certificate file '" + << ca_crtfile << "'" << std::endl; + return false; + } + } + std::cout << "----------- Root CA Certificate ------------" << std::endl; + m_ca_cert.print(); + std::cout << "--------------------------------------------" << std::endl; + + return true; +} /* Reflector::loadRootCAFiles */ + + +bool Reflector::loadSigningCAFiles(void) +{ + // Read issuing CA private key or generate a new one if it does not exist + std::string ca_keyfile; + if (!m_cfg->getValue("ISSUING_CA", "KEYFILE", ca_keyfile)) + { + ca_keyfile = m_keys_dir + "/svxreflector_issuing_ca.key"; + } + if (access(ca_keyfile.c_str(), F_OK) != 0) + { + std::cout << "Issuing CA private key file not found. Generating '" + << ca_keyfile << "'" << std::endl; + if (!m_issue_ca_pkey.generate(2048)) + { + std::cout << "*** ERROR: Failed to generate CA key" << std::endl; + return false; + } + if (!ensureDirectoryExist(ca_keyfile) || + !m_issue_ca_pkey.writePrivateKeyFile(ca_keyfile)) + { + std::cerr << "*** ERROR: Failed to write issuing CA private key file " + "to '" << ca_keyfile << "'" << std::endl; + return false; + } + } + else if (!m_issue_ca_pkey.readPrivateKeyFile(ca_keyfile)) + { + std::cerr << "*** ERROR: Failed to read issuing CA private key file " + "from '" << ca_keyfile << "'" << std::endl; + return false; + } + + // Read the CA certificate or generate a new one if it does not exist + std::string ca_crtfile; + if (!m_cfg->getValue("ISSUING_CA", "CRTFILE", ca_crtfile)) + { + ca_crtfile = m_certs_dir + "/svxreflector_issuing_ca.crt"; + } + bool generate_ca_cert = (access(ca_crtfile.c_str(), F_OK) != 0); + if (!generate_ca_cert) + { + generate_ca_cert = !m_issue_ca_cert.readPemFile(ca_crtfile) || + !m_issue_ca_cert.verify(m_ca_pkey) || + !m_issue_ca_cert.timeIsWithinRange(); + if (generate_ca_cert) + { + std::cerr << "*** WARNING: Failed to read issuing CA certificate " + "from '" << ca_crtfile << "' or the cert is invalid. " + "Generating new certificate." << std::endl; + m_issue_ca_cert.clear(); + } + else + { + int days=0, seconds=0; + m_issue_ca_cert.timeSpan(days, seconds); + time_t tnow = time(NULL); + time_t renew_time = tnow + (days*24*3600 + seconds)*RENEW_AFTER; + if (!m_issue_ca_cert.timeIsWithinRange(tnow, renew_time)) + { + std::cerr << "Time to renew the issuing CA certificate '" + << ca_crtfile << "'. It's valid until " + << m_issue_ca_cert.notAfterLocaltimeString() << "." + << std::endl; + m_issue_ca_cert.clear(); + generate_ca_cert = true; + } + } + } + + if (generate_ca_cert) + { + std::string ca_csrfile; + if (!m_cfg->getValue("ISSUING_CA", "CSRFILE", ca_csrfile)) + { + ca_csrfile = m_csrs_dir + "/svxreflector_issuing_ca.csr"; + } + std::cout << "Generating issuing CA CSR file '" << ca_csrfile + << "'" << std::endl; + Async::SslCertSigningReq csr; + csr.setVersion(Async::SslCertSigningReq::VERSION_1); + std::string value; + value = "SvxReflector Issuing CA"; + (void)m_cfg->getValue("ISSUING_CA", "COMMON_NAME", value); + if (value.empty()) + { + std::cerr << "*** ERROR: The 'ISSUING_CA/COMMON_NAME' variable is " + "unset which is needed for issuing CA certificate " + "generation." << std::endl; + return false; + } + csr.addSubjectName("CN", value); + if (m_cfg->getValue("ISSUING_CA", "ORG_UNIT", value) && + !value.empty()) + { + csr.addSubjectName("OU", value); + } + if (m_cfg->getValue("ISSUING_CA", "ORG", value) && !value.empty()) + { + csr.addSubjectName("O", value); + } + if (m_cfg->getValue("ISSUING_CA", "LOCALITY", value) && !value.empty()) + { + csr.addSubjectName("L", value); + } + if (m_cfg->getValue("ISSUING_CA", "STATE", value) && !value.empty()) + { + csr.addSubjectName("ST", value); + } + if (m_cfg->getValue("ISSUING_CA", "COUNTRY", value) && !value.empty()) + { + csr.addSubjectName("C", value); + } + Async::SslX509Extensions exts; + exts.addBasicConstraints("critical, CA:TRUE"); + exts.addKeyUsage("critical, cRLSign, digitalSignature, keyCertSign"); + if (m_cfg->getValue("ISSUING_CA", "EMAIL_ADDRESS", value) && + !value.empty()) + { + exts.addSubjectAltNames("email:" + value); + } + csr.addExtensions(exts); + csr.setPublicKey(m_issue_ca_pkey); + csr.sign(m_issue_ca_pkey); + //csr.print(); + if (!csr.writePemFile(ca_csrfile)) + { + std::cout << "*** ERROR: Failed to write issuing CA CSR file '" + << ca_csrfile << "'" << std::endl; + return false; + } + + std::cout << "Generating issuing CA certificate file '" << ca_crtfile + << "'" << std::endl; + m_issue_ca_cert.setSerialNumber(); + m_issue_ca_cert.setVersion(Async::SslX509::VERSION_3); + m_issue_ca_cert.setSubjectName(csr.subjectName()); + m_issue_ca_cert.addExtensions(csr.extensions()); + time_t tnow = time(NULL); + m_issue_ca_cert.setNotBefore(tnow); + m_issue_ca_cert.setNotAfter(tnow + 4*CERT_VALIDITY_TIME); + m_issue_ca_cert.setPublicKey(m_issue_ca_pkey); + m_issue_ca_cert.setIssuerName(m_ca_cert.subjectName()); + m_issue_ca_cert.sign(m_ca_pkey); + if (!m_issue_ca_cert.writePemFile(ca_crtfile)) + { + std::cout << "*** ERROR: Failed to write issuing CA certificate file '" + << ca_crtfile << "'" << std::endl; + return false; + } + } + std::cout << "---------- Issuing CA Certificate ----------" << std::endl; + m_issue_ca_cert.print(); + std::cout << "--------------------------------------------" << std::endl; + + startCertRenewTimer(m_issue_ca_cert, m_renew_issue_ca_cert_timer); + + return true; +} /* Reflector::loadSigningCAFiles */ + + +bool Reflector::onVerifyPeer(TcpConnection *con, bool preverify_ok, + X509_STORE_CTX *x509_store_ctx) +{ + //std::cout << "### Reflector::onVerifyPeer: preverify_ok=" + // << (preverify_ok ? "yes" : "no") << std::endl; + + Async::SslX509 cert(*x509_store_ctx); + preverify_ok = preverify_ok && !cert.isNull(); + preverify_ok = preverify_ok && !cert.commonName().empty(); + if (!preverify_ok) + { + std::cout << "*** ERROR: Certificate verification failed for client" + << std::endl; + std::cout << "------------- Peer Certificate --------------" << std::endl; + cert.print(); + std::cout << "---------------------------------------------" << std::endl; + } + + return preverify_ok; +} /* Reflector::onVerifyPeer */ + + +Async::SslX509 Reflector::onCsrReceived(Async::SslCertSigningReq& req) +{ + if (req.isNull()) + { + return nullptr; + } + + std::string callsign(req.commonName()); + if (callsign.empty()) + { + std::cout << "*** WARNING: The callsign (CN) in the CSR is empty. " + "Ignoring this CSR." << std::endl; + return nullptr; + } + // FIXME: Move code to initialize() and make a public function + // verifyCallsign() so that callsigns can be verified from + // ReflectorClient. + std::string csrestr; + if (!m_cfg->getValue("GLOBAL", "CALLSIGN_MATCH", csrestr) || + csrestr.empty()) + { + csrestr = "[A-Z0-9][A-Z]{0,2}\\d[A-Z0-9]{1,3}[A-Z](?:-[A-Z0-9]{1,3})?"; + } + const std::regex csre(csrestr); + if (!std::regex_match(callsign, csre)) + { + std::cout << "*** WARNING: The callsign (CN) in the received CSR, '" + << callsign << "', is malformed." << std::endl; + return nullptr; + } + + std::string csr_path(m_csrs_dir + "/" + callsign + ".csr"); + Async::SslCertSigningReq csr; + if (!csr.readPemFile(csr_path)) + { + csr.set(nullptr); + } + + if (!csr.isNull() && (req.publicKey() != csr.publicKey())) + { + std::cerr << "*** WARNING: The received CSR with callsign '" + << callsign << "' has a different public key " + "than the current CSR. That may be a sign of someone " + "trying to highjack a callsign or the owner of the " + "callsign has generated a new private/public key pair." + << std::endl; + return nullptr; + } + + std::string crtfile(m_certs_dir + "/" + callsign + ".crt"); + Async::SslX509 cert; + if (!cert.readPemFile(crtfile) || !cert.verify(m_issue_ca_pkey) || + !cert.timeIsWithinRange() || (cert.publicKey() != req.publicKey())) + { + cert.set(nullptr); + } + + std::string pending_csr_path(m_pending_csrs_dir + "/" + callsign + ".csr"); + Async::SslCertSigningReq pending_csr; + if (!pending_csr.readPemFile(pending_csr_path)) + { + pending_csr.set(nullptr); + } + + if (( + csr.isNull() || + (req.digest() != csr.digest()) || + cert.isNull() + ) && ( + !pending_csr.readPemFile(pending_csr_path) || + (req.digest() != pending_csr.digest()) + )) + { + std::cout << callsign << ": Add pending CSR '" << pending_csr_path + << "' to CA" << std::endl; + if (!req.writePemFile(pending_csr_path)) + { + std::cerr << "*** WARNING: Could not write CSR file '" + << pending_csr_path << "'" << std::endl; + } + } + else + { + std::cout << callsign << ": The new CSR is the same as the already " + << "existing CSR, so ignoring the new one" << std::endl; + } + + return cert; +} /* Reflector::onCsrReceived */ + + +bool Reflector::buildPath(const std::string& sec, const std::string& tag, + const std::string& defdir, std::string& defpath) +{ + bool isdir = (defpath.back() == '/'); + std::string path(defpath); + if (!m_cfg->getValue(sec, tag, path) || path.empty()) + { + path = defpath; + } + //std::cout << "### sec=" << sec << " tag=" << tag << " defdir=" << defdir << " defpath=" << defpath << " path=" << path << std::endl; + if ((path.front() != '/') && (path.front() != '.')) + { + path = defdir + "/" + defpath; + } + if (!ensureDirectoryExist(path)) + { + return false; + } + if (isdir && (path.back() == '/')) + { + defpath = path.substr(0, path.size()-1); + } + else + { + defpath = std::move(path); + } + //std::cout << "### defpath=" << defpath << std::endl; + return true; +} /* Reflector::buildPath */ + + +bool Reflector::removeClientCert(const std::string& cn) +{ + std::cout << "### Reflector::removeClientCert: cn=" << cn << std::endl; + return true; +} /* Reflector::removeClientCert */ + + /* * This file has not been truncated */ diff --git a/src/svxlink/reflector/Reflector.h b/src/svxlink/reflector/Reflector.h index 20f0c2215..fead95eec 100644 --- a/src/svxlink/reflector/Reflector.h +++ b/src/svxlink/reflector/Reflector.h @@ -49,6 +49,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include #include #include +#include #include @@ -70,7 +71,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA namespace Async { - class UdpSocket; + class EncryptedUdpSocket; class Config; class Pty; }; @@ -165,7 +166,7 @@ class Reflector : public sigc::trackable * @param count The number of bytes in the payload * @return Returns \em true on success or else \em false */ - bool sendUdpDatagram(ReflectorClient *client, const void *buf, size_t count); + bool sendUdpDatagram(ReflectorClient *client, const ReflectorUdpMsg& msg); void broadcastUdpMsg(const ReflectorUdpMsg& msg, const ReflectorClient::Filter& filter=ReflectorClient::NoFilter()); @@ -182,32 +183,74 @@ class Reflector : public sigc::trackable */ void requestQsy(ReflectorClient *client, uint32_t tg); + Async::EncryptedUdpSocket* udpSocket(void) const { return m_udp_sock; } + uint32_t randomQsyLo(void) const { return m_random_qsy_lo; } uint32_t randomQsyHi(void) const { return m_random_qsy_hi; } + Async::SslCertSigningReq loadClientPendingCsr(const std::string& callsign); + Async::SslCertSigningReq loadClientCsr(const std::string& callsign); + bool signClientCert(Async::SslX509& cert); + Async::SslX509 signClientCsr(const std::string& cn); + Async::SslX509 loadClientCertificate(const std::string& callsign); + + size_t caSize(void) const { return m_ca_size; } + const std::vector& caDigest(void) const { return m_ca_md; } + const std::vector& caSignature(void) const { return m_ca_sig; } + const std::string& caUrl(void) const { return m_ca_url; } + std::string clientCertPem(const std::string& callsign) const; + std::string caBundlePem(void) const; + std::string issuingCertPem(void) const; + + protected: + private: typedef std::map ReflectorClientConMap; typedef Async::TcpServer FramedTcpServer; - - FramedTcpServer* m_srv; - Async::UdpSocket* m_udp_sock; - ReflectorClientConMap m_client_con_map; - Async::Config* m_cfg; - uint32_t m_tg_for_v1_clients; - uint32_t m_random_qsy_lo; - uint32_t m_random_qsy_hi; - uint32_t m_random_qsy_tg; - Async::TcpServer* m_http_server; - Async::Pty* m_cmd_pty; + using HttpServer = Async::TcpServer; + + static constexpr time_t CERT_VALIDITY_TIME = 3*3600; + + FramedTcpServer* m_srv; + Async::EncryptedUdpSocket* m_udp_sock; + ReflectorClientConMap m_client_con_map; + Async::Config* m_cfg; + uint32_t m_tg_for_v1_clients; + uint32_t m_random_qsy_lo; + uint32_t m_random_qsy_hi; + uint32_t m_random_qsy_tg; + HttpServer* m_http_server; + Async::Pty* m_cmd_pty; + Async::SslContext m_ssl_ctx; + std::string m_keys_dir; + std::string m_pending_csrs_dir; + std::string m_csrs_dir; + std::string m_certs_dir; + UdpCipher::AAD m_aad; + Async::SslKeypair m_ca_pkey; + Async::SslX509 m_ca_cert; + Async::SslKeypair m_issue_ca_pkey; + Async::SslX509 m_issue_ca_cert; + std::string m_pki_dir; + std::string m_ca_bundle_file; + std::string m_crtfile; + Async::AtTimer m_renew_cert_timer; + Async::AtTimer m_renew_issue_ca_cert_timer; + size_t m_ca_size = 0; + std::vector m_ca_md; + std::vector m_ca_sig; + std::string m_ca_url; Reflector(const Reflector&); Reflector& operator=(const Reflector&); void clientConnected(Async::FramedTcpConnection *con); void clientDisconnected(Async::FramedTcpConnection *con, Async::FramedTcpConnection::DisconnectReason reason); + bool udpCipherDataReceived(const Async::IpAddress& addr, uint16_t port, + void *buf, int count); void udpDatagramReceived(const Async::IpAddress& addr, uint16_t port, - void *buf, int count); + void* aad, void *buf, int count); void onTalkerUpdated(uint32_t tg, ReflectorClient* old_talker, ReflectorClient *new_talker); void httpRequestReceived(Async::HttpServerConnection *con, @@ -219,6 +262,17 @@ class Reflector : public sigc::trackable uint32_t nextRandomQsyTg(void); void ctrlPtyDataReceived(const void *buf, size_t count); void cfgUpdated(const std::string& section, const std::string& tag); + bool loadCertificateFiles(void); + bool loadServerCertificateFiles(void); + bool generateKeyFile(Async::SslKeypair& pkey, const std::string& keyfile); + bool loadRootCAFiles(void); + bool loadSigningCAFiles(void); + bool onVerifyPeer(Async::TcpConnection *con, bool preverify_ok, + X509_STORE_CTX *x509_store_ctx); + Async::SslX509 onCsrReceived(Async::SslCertSigningReq& req); + bool buildPath(const std::string& sec, const std::string& tag, + const std::string& defdir, std::string& defpath); + bool removeClientCert(const std::string& cn); }; /* class Reflector */ diff --git a/src/svxlink/reflector/ReflectorClient.cpp b/src/svxlink/reflector/ReflectorClient.cpp index dd6d2ac5f..f1a343f24 100644 --- a/src/svxlink/reflector/ReflectorClient.cpp +++ b/src/svxlink/reflector/ReflectorClient.cpp @@ -31,6 +31,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ****************************************************************************/ #include +#include #include #include #include @@ -47,6 +48,8 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include #include #include +#include +#include #include @@ -78,6 +81,7 @@ using namespace Async; * ****************************************************************************/ +#define RENEW_AFTER 2/3 /**************************************************************************** @@ -112,8 +116,11 @@ using namespace Async; ****************************************************************************/ ReflectorClient::ClientMap ReflectorClient::client_map; +ReflectorClient::ClientSrcMap ReflectorClient::client_src_map; +ReflectorClient::ClientCallsignMap ReflectorClient::client_callsign_map; std::mt19937 ReflectorClient::id_gen(std::random_device{}()); -ReflectorClient::ClientIdRandomDist ReflectorClient::id_dist(0, CLIENT_ID_MAX); +ReflectorClient::ClientIdRandomDist ReflectorClient::id_dist( + CLIENT_ID_MIN, CLIENT_ID_MAX); /**************************************************************************** @@ -122,7 +129,7 @@ ReflectorClient::ClientIdRandomDist ReflectorClient::id_dist(0, CLIENT_ID_MAX); * ****************************************************************************/ -ReflectorClient* ReflectorClient::lookup(ClientId id) +ReflectorClient* ReflectorClient::lookup(const ClientId& id) { auto it = client_map.find(id); if (it == client_map.end()) @@ -133,6 +140,28 @@ ReflectorClient* ReflectorClient::lookup(ClientId id) } /* ReflectorClient::lookup */ +ReflectorClient* ReflectorClient::lookup(const ClientSrc& src) +{ + auto it = client_src_map.find(src); + if (it == client_src_map.end()) + { + return nullptr; + } + return it->second; +} /* ReflectorClient::lookup */ + + +ReflectorClient* ReflectorClient::lookup(const std::string& cs) +{ + auto it = client_callsign_map.find(cs); + if (it == client_callsign_map.end()) + { + return nullptr; + } + return it->second; +} /* ReflectorClient::lookup */ + + void ReflectorClient::cleanup(void) { auto client_map_copy = client_map; @@ -156,23 +185,27 @@ ReflectorClient::ReflectorClient(Reflector *ref, Async::FramedTcpConnection *con Async::Config *cfg) : m_con(con), m_con_state(STATE_EXPECT_PROTO_VER), m_disc_timer(10000, Timer::TYPE_ONESHOT, false), - m_client_id(newClient(this)), m_remote_udp_port(0), m_cfg(cfg), - m_next_udp_tx_seq(0), m_next_udp_rx_seq(0), + m_client_id(newClientId(this)), m_remote_udp_port(0), m_cfg(cfg), + /*m_next_udp_tx_seq(0),*/ m_next_udp_rx_seq(0), m_heartbeat_timer(1000, Timer::TYPE_PERIODIC), m_heartbeat_tx_cnt(HEARTBEAT_TX_CNT_RESET), m_heartbeat_rx_cnt(HEARTBEAT_RX_CNT_RESET), m_udp_heartbeat_tx_cnt(UDP_HEARTBEAT_TX_CNT_RESET), m_udp_heartbeat_rx_cnt(UDP_HEARTBEAT_RX_CNT_RESET), m_reflector(ref), m_blocktime(0), m_remaining_blocktime(0), - m_current_tg(0) + m_current_tg(0), m_udp_cipher_iv_cntr(0) { m_con->setMaxFrameSize(ReflectorMsg::MAX_PREAUTH_FRAME_SIZE); + m_con->sslConnectionReady.connect( + sigc::mem_fun(*this, &ReflectorClient::onSslConnectionReady)); m_con->frameReceived.connect( - mem_fun(*this, &ReflectorClient::onFrameReceived)); + sigc::mem_fun(*this, &ReflectorClient::onFrameReceived)); m_disc_timer.expired.connect( - mem_fun(*this, &ReflectorClient::onDiscTimeout)); + sigc::mem_fun(*this, &ReflectorClient::onDiscTimeout)); m_heartbeat_timer.expired.connect( - mem_fun(*this, &ReflectorClient::handleHeartbeat)); + sigc::mem_fun(*this, &ReflectorClient::handleHeartbeat)); + m_renew_cert_timer.expired.connect(sigc::hide( + sigc::mem_fun(*this, &ReflectorClient::renewClientCertificate))); string codecs; if (m_cfg->getValue("GLOBAL", "CODECS", codecs)) @@ -210,10 +243,26 @@ ReflectorClient::~ReflectorClient(void) auto client_it = client_map.find(m_client_id); assert(client_it != client_map.end()); client_map.erase(client_it); + client_src_map.erase(m_client_src); + if (!m_callsign.empty()) + { + client_callsign_map.erase(m_callsign); + } TGHandler::instance()->removeClient(this); } /* ReflectorClient::~ReflectorClient */ +void ReflectorClient::setRemoteUdpPort(uint16_t port) +{ + assert(m_remote_udp_port == 0); + m_remote_udp_port = port; + if (m_client_proto_ver >= ProtoVer(3, 0)) + { + m_client_src = newClientSrc(this); + } +} /* ReflectorClient::setRemoteUdpPort */ + + int ReflectorClient::sendMsg(const ReflectorMsg& msg) { if (((m_con_state != STATE_CONNECTED) && (msg.type() >= 100)) || @@ -239,8 +288,6 @@ int ReflectorClient::sendMsg(const ReflectorMsg& msg) void ReflectorClient::udpMsgReceived(const ReflectorUdpMsg &header) { - m_next_udp_rx_seq = header.sequenceNum() + 1; - m_udp_heartbeat_rx_cnt = UDP_HEARTBEAT_RX_CNT_RESET; if ((m_blocktime > 0) && (header.type() == MsgUdpAudio::TYPE)) @@ -259,10 +306,7 @@ void ReflectorClient::sendUdpMsg(const ReflectorUdpMsg &msg) m_udp_heartbeat_tx_cnt = UDP_HEARTBEAT_TX_CNT_RESET; - ReflectorUdpMsg header(msg.type(), clientId(), nextUdpTxSeq()); - ostringstream ss; - assert(header.pack(ss) && msg.pack(ss)); - (void)m_reflector->sendUdpDatagram(this, ss.str().data(), ss.str().size()); + (void)m_reflector->sendUdpDatagram(this, msg); } /* ReflectorClient::sendUdpMsg */ @@ -273,6 +317,22 @@ void ReflectorClient::setBlock(unsigned blocktime) } /* ReflectorClient::setBlock */ +std::vector ReflectorClient::udpCipherIV(void) const +{ + return UdpCipher::IV{udpCipherIVRand(), 0, m_udp_cipher_iv_cntr}; +} /* ReflectorClient::udpCipherIV */ + + +void ReflectorClient::certificateUpdated(Async::SslX509& cert) +{ + if (m_con_state == STATE_CONNECTED) + { + //sendMsg(MsgClientCert(cert.pem())); + sendClientCert(cert); + } +} /* ReflectorClient::certificateUpdated */ + + /**************************************************************************** * * Protected member functions @@ -287,17 +347,94 @@ void ReflectorClient::setBlock(unsigned blocktime) * ****************************************************************************/ -ReflectorClient::ClientId ReflectorClient::newClient(ReflectorClient* client) +ReflectorClient::ClientId ReflectorClient::newClientId(ReflectorClient* client) { - assert(!(client_map.size() > CLIENT_ID_MAX)); + assert(client_map.size() < + (static_cast(CLIENT_ID_MAX)-CLIENT_ID_MIN+1)); ClientId id = id_dist(id_gen); while (client_map.count(id) > 0) { - id = (id < CLIENT_ID_MAX) ? id+1 : 0; + id = (id < CLIENT_ID_MAX) ? id+1 : CLIENT_ID_MIN; } client_map[id] = client; return id; -} /* ReflectorClient::newClient */ +} /* ReflectorClient::newClientId */ + + +ReflectorClient::ClientSrc ReflectorClient::newClientSrc(ReflectorClient* client) +{ + ClientSrc src{std::make_pair(client->m_con->remoteHost(), + client->m_remote_udp_port)}; + client_src_map[src] = client; + return src; +} /* ReflectorClient::newClientSrc */ + + +void ReflectorClient::onSslConnectionReady(TcpConnection *con) +{ + //std::cout << "### ReflectorClient::onSslConnectionReady" << std::endl; + + if (m_con_state != STATE_EXPECT_SSL_CON_READY) + { + std::cout << "*** ERROR[" << m_con->remoteHost() << ":" + << m_con->remotePort() + << "]: SSL connection ready event unexpected" << std::endl; + disconnect(); + return; + } + + m_con->setMaxFrameSize(ReflectorMsg::MAX_POST_SSL_SETUP_SIZE); + + Async::SslX509 peer_cert(con->sslPeerCertificate()); + if (peer_cert.isNull()) + { + std::cout << m_con->remoteHost() << ":" << m_con->remotePort() + << ": No peer certificate. Requesting Certificate " + "Signing Request from peer." << std::endl; + sendMsg(MsgClientCsrRequest()); + m_con_state = STATE_EXPECT_CSR; + return; + //MsgAuthChallenge challenge_msg; + //if (challenge_msg.challenge() == nullptr) + //{ + // disconnect(); + // return; + //} + //memcpy(m_auth_challenge, challenge_msg.challenge(), + // MsgAuthChallenge::LENGTH); + //sendMsg(challenge_msg); + //m_con_state = STATE_EXPECT_AUTH_RESPONSE; + //return; + } + + std::cout << "-------------- Peer Certificate ---------------" << std::endl; + peer_cert.print(); + std::cout << "-----------------------------------------------" << std::endl; + + std::string callsign = peer_cert.commonName(); + //if (callsign.empty()) + //{ + // std::cout << "*** ERROR[" << m_con->remoteHost() << ":" + // << m_con->remotePort() + // << "]: peer certificate has empty common name" << std::endl; + // disconnect(); + // return; + //} + + int days=0, seconds=0; + peer_cert.timeSpan(days, seconds); + time_t renew_time = peer_cert.notBefore() + + (static_cast(days)*24*3600 + seconds)*RENEW_AFTER; + //std::cout << "### Client cert days=" << days << " seconds=" + // << seconds << " renew_in=" << (renew_time - time(NULL)) + // << std::endl; + m_renew_cert_timer.setTimeout(renew_time); + m_renew_cert_timer.setExpireOffset(10000); + m_renew_cert_timer.start(); + + std::cout << callsign << ": " << peer_cert.subjectNameString() << std::endl; + connectionAuthenticated(callsign); +} /* ReflectorClient::onSslConnectionReady */ void ReflectorClient::onFrameReceived(FramedTcpConnection *con, @@ -343,9 +480,18 @@ void ReflectorClient::onFrameReceived(FramedTcpConnection *con, case MsgProtoVer::TYPE: handleMsgProtoVer(ss); break; + case MsgCABundleRequest::TYPE: + handleMsgCABundleRequest(ss); + break; + case MsgStartEncryptionRequest::TYPE: + handleMsgStartEncryptionRequest(ss); + break; case MsgAuthResponse::TYPE: handleMsgAuthResponse(ss); break; + case MsgClientCsr::TYPE: + handleMsgClientCsr(ss); + break; case MsgSelectTG::TYPE: handleSelectTG(ss); break; @@ -397,8 +543,9 @@ void ReflectorClient::handleMsgProtoVer(std::istream& is) MsgProtoVer msg; if (!msg.unpack(is)) { - std::cout << "Client " << m_con->remoteHost() << ":" << m_con->remotePort() - << " ERROR: Could not unpack MsgProtoVer\n"; + std::cout << "*** ERROR[" << m_con->remoteHost() << ":" + << m_con->remotePort() << "]: Could not unpack MsgProtoVer" + << std::endl; sendError("Illegal MsgProtoVer protocol message received"); return; } @@ -406,19 +553,33 @@ void ReflectorClient::handleMsgProtoVer(std::istream& is) ProtoVer max_proto_ver(MsgProtoVer::MAJOR, MsgProtoVer::MINOR); if (m_client_proto_ver > max_proto_ver) { - std::cout << "Client " << m_con->remoteHost() << ":" << m_con->remotePort() - << " use protocol version " + std::cout << m_con->remoteHost() << ":" << m_con->remotePort() + << ": Using protocol version " << msg.majorVer() << "." << msg.minorVer() - << " which is newer than we can handle. Asking for downgrade to " - << MsgProtoVer::MAJOR << "." << MsgProtoVer::MINOR << "." - << std::endl; - sendMsg(MsgProtoVerDowngrade()); + << " which is newer than we can handle."; + if (m_con_state == STATE_EXPECT_PROTO_VER) + { + std::cout << " Asking for downgrade to " + << MsgProtoVer::MAJOR << "." << MsgProtoVer::MINOR << "."; + sendMsg(MsgProtoVerDowngrade()); + } + else + { + std::cout << " Downgrade failed."; + std::ostringstream ss; + ss << "Unsupported protocol version " << msg.majorVer() << "." + << msg.minorVer() << ". May be at most " + << MsgProtoVer::MAJOR << "." << MsgProtoVer::MINOR << "."; + sendError(ss.str()); + } + std::cout << std::endl; return; } else if (m_client_proto_ver < ProtoVer(MIN_MAJOR_VER, MIN_MINOR_VER)) { - std::cout << "Client " << m_con->remoteHost() << ":" << m_con->remotePort() - << " is using protocol version " + std::cout << "*** ERROR[" << m_con->remoteHost() << ":" + << m_con->remotePort() + << "]: Client is using protocol version " << msg.majorVer() << "." << msg.minorVer() << " which is too old. Must at least be version " << MIN_MAJOR_VER << "." << MIN_MINOR_VER << "." << std::endl; @@ -430,20 +591,81 @@ void ReflectorClient::handleMsgProtoVer(std::istream& is) return; } - MsgAuthChallenge challenge_msg; - memcpy(m_auth_challenge, challenge_msg.challenge(), - MsgAuthChallenge::CHALLENGE_LEN); - sendMsg(challenge_msg); - m_con_state = STATE_EXPECT_AUTH_RESPONSE; + if (m_client_proto_ver.majorVer() >= 3) + { + //std::cout << "### ReflectorClient::handMsgProtoVer: Send CAInfo" + // << std::endl; + m_con->setMaxFrameSize(ReflectorMsg::MAX_PRE_SSL_SETUP_SIZE); + sendMsg(MsgCAInfo(m_reflector->caSize(), m_reflector->caDigest())); + m_con_state = STATE_EXPECT_START_ENCRYPTION; + } + else + { + sendAuthChallenge(); + } } /* ReflectorClient::handleMsgProtoVer */ +void ReflectorClient::handleMsgCABundleRequest(std::istream& is) +{ + //std::cout << "### ReflectorClient::handleMsgCABundleRequest" << std::endl; + + if (m_con_state != STATE_EXPECT_START_ENCRYPTION) + { + std::cout << "*** ERROR[" << m_con->remoteHost() << ":" + << m_con->remotePort() << "]: Unexpected MsgCABundleRequest" + << std::endl; + disconnect(); + return; + } + + std::cout << "### Sending CA Bundle" << std::endl; + m_con->setMaxFrameSize(ReflectorMsg::MAX_PRE_SSL_SETUP_SIZE); + sendMsg(MsgCABundle(m_reflector->caBundlePem(), m_reflector->caSignature(), + m_reflector->issuingCertPem())); +} /* ReflectorClient::handleMsgCABundleRequest */ + + +void ReflectorClient::handleMsgStartEncryptionRequest(std::istream& is) +{ + //std::cout << "### ReflectorClient::handleMsgStartEncryptionRequest" + // << std::endl; + + if (m_con_state != STATE_EXPECT_START_ENCRYPTION) + { + std::cout << "*** ERROR[" << m_con->remoteHost() << ":" + << m_con->remotePort() + << "]: Unexpected MsgStartEncryptionRequest" << std::endl; + disconnect(); + return; + } + + MsgStartEncryptionRequest msg; + if (!msg.unpack(is)) + { + std::cerr << "*** ERROR[" << m_con->remoteHost() << ":" + << m_con->remotePort() + << "]: Could not unpack MsgStartEncryptionRequest" << std::endl; + disconnect(); + return; + } + + std::cout << m_con->remoteHost() << ":" << m_con->remotePort() + << ": Starting encryption" << std::endl; + + sendMsg(MsgStartEncryption()); + m_con->enableSsl(true); + m_con_state = STATE_EXPECT_SSL_CON_READY; +} /* ReflectorClient::handleMsgStartEncryptionRequest */ + + void ReflectorClient::handleMsgAuthResponse(std::istream& is) { if (m_con_state != STATE_EXPECT_AUTH_RESPONSE) { - cout << "Client " << m_con->remoteHost() << ":" << m_con->remotePort() - << " Authentication response unexpected" << endl; + std::cerr << "*** ERROR[" << m_con->remoteHost() << ":" + << m_con->remotePort() + << "]: Authentication response unexpected" << std::endl; sendError("Authentication response unexpected"); return; } @@ -451,70 +673,119 @@ void ReflectorClient::handleMsgAuthResponse(std::istream& is) MsgAuthResponse msg; if (!msg.unpack(is)) { - cout << "Client " << m_con->remoteHost() << ":" << m_con->remotePort() - << " ERROR: Could not unpack MsgAuthResponse" << endl; + std::cerr << "*** ERROR[" << m_con->remoteHost() << ":" + << m_con->remotePort() + << "]: Could not unpack MsgAuthResponse" << std::endl; + sendError("Illegal MsgAuthResponse protocol message received"); + return; + } + if (msg.callsign().empty()) + { + std::cerr << "*** ERROR[" << m_con->remoteHost() << ":" + << m_con->remotePort() + << "]: Empty callsign in MsgAuthResponse" << std::endl; sendError("Illegal MsgAuthResponse protocol message received"); return; } + //const auto peer_cert = m_reflector->loadClientCertificate(msg.callsign()); + //if (!peer_cert.isNull()) + //{ + // std::cout << "Client " << m_con->remoteHost() << ":" + // << m_con->remotePort() << " (" << msg.callsign() << "?)" + // << ": Sending client certificate to peer" << std::endl; + // //sendMsg(MsgClientCert(peer_cert.pem())); + // sendClientCert(peer_cert); + // //m_con_state = STATE_EXPECT_CSR; + // return; + //} + //else //if (m_reflector->loadClientPendingCsr(msg.callsign()).isNull()) + //{ + // std::cout << "Client " << m_con->remoteHost() << ":" + // << m_con->remotePort() << " (" << msg.callsign() << "?)" + // << ": Sending CSR request to peer" << std::endl; + // sendMsg(MsgClientCsrRequest()); + //} + string auth_key = lookupUserKey(msg.callsign()); if (!auth_key.empty() && msg.verify(auth_key, m_auth_challenge)) { - vector connected_nodes; - m_reflector->nodeList(connected_nodes); - if (find(connected_nodes.begin(), connected_nodes.end(), - msg.callsign()) == connected_nodes.end()) - { - m_con->setMaxFrameSize(ReflectorMsg::MAX_POSTAUTH_FRAME_SIZE); - m_callsign = msg.callsign(); - sendMsg(MsgAuthOk()); - cout << m_callsign << ": Login OK from " - << m_con->remoteHost() << ":" << m_con->remotePort() - << " with protocol version " << m_client_proto_ver.majorVer() - << "." << m_client_proto_ver.minorVer() - << endl; - m_con_state = STATE_CONNECTED; - MsgServerInfo msg_srv_info(m_client_id, m_supported_codecs); - m_reflector->nodeList(msg_srv_info.nodes()); - sendMsg(msg_srv_info); - if (m_client_proto_ver < ProtoVer(0, 7)) - { - MsgNodeList msg_node_list(msg_srv_info.nodes()); - sendMsg(msg_node_list); - } - if (m_client_proto_ver < ProtoVer(2, 0)) - { - if (TGHandler::instance()->switchTo(this, m_reflector->tgForV1Clients())) - { - std::cout << m_callsign << ": Select TG #" - << m_reflector->tgForV1Clients() << std::endl; - m_current_tg = m_reflector->tgForV1Clients(); - } - else - { - std::cout << m_callsign - << ": V1 client not allowed to use default TG #" - << m_reflector->tgForV1Clients() << std::endl; - } - } - m_reflector->broadcastMsg(MsgNodeJoined(m_callsign), ExceptFilter(this)); - } - else - { - cout << msg.callsign() << ": Already connected" << endl; - sendError("Access denied"); - } + std::cout << msg.callsign() << ": Received valid auth key" << std::endl; + connectionAuthenticated(msg.callsign()); } else { - cout << "Client " << m_con->remoteHost() << ":" << m_con->remotePort() - << " Authentication failed for user \"" << msg.callsign() - << "\"" << endl; + std::cerr << "*** ERROR[" << m_con->remoteHost() << ":" + << m_con->remotePort() << "]: Authentication failed for user '" + << msg.callsign() << "'" << std::endl; sendError("Access denied"); } } /* ReflectorClient::handleMsgAuthResponse */ +void ReflectorClient::handleMsgClientCsr(std::istream& is) +{ + std::ostringstream idss; + if (m_con_state == STATE_CONNECTED) + { + idss << m_callsign; + } + else + { + idss << m_con->remoteHost() << ":" << m_con->remotePort(); + } + + if ((m_con_state != STATE_CONNECTED) && (m_con_state != STATE_EXPECT_CSR)) + { + std::cerr << "*** ERROR[" << idss.str() + << "]: Certificate Signing Request unexpected" << std::endl; + sendError("Certificate Signing Request unexpected"); + return; + } + + MsgClientCsr msg; + if (!msg.unpack(is)) + { + std::cout << "*** ERROR[" << idss.str() + << "]: Could not unpack MsgClientCsr" << std::endl; + sendError("Illegal MsgClientCsr protocol message received"); + return; + } + + std::cerr << idss.str() << ": Received CSR" << std::endl; + + Async::SslCertSigningReq req; + if (!req.readPem(msg.csrPem()) || req.isNull()) + { + std::cerr << "*** ERROR[" << idss.str() << "]: Invalid CSR received:\n" + << msg.csrPem() << std::endl; + sendError("Invalid CSR received"); + return; + } + req.print(idss.str() + ": "); + + auto cert = csrReceived(req); + auto current_req = m_reflector->loadClientCsr(req.commonName()); + if (( + (m_con_state == STATE_EXPECT_CSR) || + (!current_req.isNull() && (req.digest() == current_req.digest())) + ) && + sendClientCert(cert)) + { + //std::cout << "### Sent certificate to peer:" << std::endl; + //cert.print(); + m_con_state = STATE_EXPECT_DISCONNECT; + } + else if (m_con_state == STATE_EXPECT_CSR) + { + std::cout << idss.str() << ": No valid certificate found matching CSR. " + "Sending authentication challenge." << std::endl; + sendAuthChallenge(); + m_con_state = STATE_EXPECT_AUTH_RESPONSE; + } +} /* ReflectorClient::handleMsgClientCsr */ + + void ReflectorClient::handleSelectTG(std::istream& is) { MsgSelectTG msg; @@ -590,18 +861,42 @@ void ReflectorClient::handleTgMonitor(std::istream& is) void ReflectorClient::handleNodeInfo(std::istream& is) { - MsgNodeInfo msg; - if (!msg.unpack(is)) + std::string jsonstr; + if (m_client_proto_ver >= ProtoVer(3, 0)) { - cout << "Client " << m_con->remoteHost() << ":" << m_con->remotePort() - << " ERROR: Could not unpack MsgNodeInfo" << endl; - sendError("Illegal MsgNodeInfo protocol message received"); - return; + MsgNodeInfo msg; + if (!msg.unpack(is)) + { + cout << "Client " << m_con->remoteHost() << ":" << m_con->remotePort() + << " ERROR: Could not unpack MsgNodeInfo" << endl; + sendError("Illegal MsgNodeInfo protocol message received"); + return; + } + //std::cout << "### handleNodeInfo: udpSrcPort()=" << msg.udpSrcPort() + // << " JSON=" << msg.json() << std::endl; + //setRemoteUdpPort(msg.udpSrcPort()); + setUdpCipherIVRand(msg.ivRand()); + setUdpCipherKey(msg.udpCipherKey()); + jsonstr = msg.json(); + + sendMsg(MsgStartUdpEncryption()); + } + else + { + MsgNodeInfoV2 msg; + if (!msg.unpack(is)) + { + std::cout << "Client " << m_con->remoteHost() << ":" + << m_con->remotePort() + << " ERROR: Could not unpack MsgNodeInfoV2" << std::endl; + sendError("Illegal MsgNodeInfo protocol message received"); + return; + } + jsonstr = msg.json(); } - //std::cout << "### handleNodeInfo: " << msg.json() << std::endl; try { - std::istringstream is(msg.json()); + std::istringstream is(jsonstr); is >> m_node_info; } catch (const Json::Exception& e) @@ -903,6 +1198,120 @@ std::string ReflectorClient::lookupUserKey(const std::string& callsign) } /* ReflectorClient::lookupUserKey */ +void ReflectorClient::connectionAuthenticated(const std::string& callsign) +{ + vector connected_nodes; + m_reflector->nodeList(connected_nodes); + if (find(connected_nodes.begin(), connected_nodes.end(), + callsign) == connected_nodes.end()) + { + m_con->setMaxFrameSize(ReflectorMsg::MAX_POSTAUTH_FRAME_SIZE); + m_callsign = callsign; + sendMsg(MsgAuthOk()); + cout << m_callsign << ": Login OK from " + << m_con->remoteHost() << ":" << m_con->remotePort() + << " with protocol version " << m_client_proto_ver.majorVer() + << "." << m_client_proto_ver.minorVer() + << endl; + m_con_state = STATE_CONNECTED; + + assert(client_callsign_map.find(m_callsign) == client_callsign_map.end()); + client_callsign_map[m_callsign] = this; + + //const auto cert = m_reflector->loadClientCertificate(m_callsign); + //const auto peer_cert = m_con->sslPeerCertificate(); + //if (!cert.isNull() && !peer_cert.isNull() && + // (cert.publicKey() != peer_cert.publicKey())) + //{ + // std::cout << m_callsign << ": Requesting CSR from peer" << std::endl; + // sendMsg(MsgClientCsrRequest()); + //} + + MsgServerInfo msg_srv_info(m_client_id, m_supported_codecs); + m_reflector->nodeList(msg_srv_info.nodes()); + sendMsg(msg_srv_info); + + if (m_client_proto_ver < ProtoVer(0, 7)) + { + MsgNodeList msg_node_list(msg_srv_info.nodes()); + sendMsg(msg_node_list); + } + + if (m_client_proto_ver < ProtoVer(2, 0)) + { + if (TGHandler::instance()->switchTo(this, m_reflector->tgForV1Clients())) + { + std::cout << m_callsign << ": Select TG #" + << m_reflector->tgForV1Clients() << std::endl; + m_current_tg = m_reflector->tgForV1Clients(); + } + else + { + std::cout << m_callsign + << ": V1 client not allowed to use default TG #" + << m_reflector->tgForV1Clients() << std::endl; + } + } + m_reflector->broadcastMsg(MsgNodeJoined(m_callsign), ExceptFilter(this)); + } + else + { + cout << callsign << ": Already connected" << endl; + sendError("Access denied"); + } +} /* ReflectorClient::connectionAuthenticated */ + + +bool ReflectorClient::sendClientCert(const Async::SslX509& cert) +{ + if (cert.isNull()) + { + return false; + } + + const auto callsign = cert.commonName(); + const auto pending_csr = m_reflector->loadClientPendingCsr(callsign); + if (!pending_csr.isNull() && (cert.publicKey() != pending_csr.publicKey())) + { + //std::cout << "### ReflectorClient::sendClientCert: Cert public key " + // "differs compared to pending CSR public key" << std::endl; + //sendMsg(MsgClientCert()); + return false; + } + return sendMsg(MsgClientCert(m_reflector->clientCertPem(callsign))); +} /* ReflectorClient::sendClientCert */ + + +void ReflectorClient::sendAuthChallenge(void) +{ + MsgAuthChallenge challenge_msg; + if (challenge_msg.challenge() == nullptr) + { + disconnect(); + return; + } + memcpy(m_auth_challenge, challenge_msg.challenge(), + MsgAuthChallenge::LENGTH); + sendMsg(challenge_msg); + m_con_state = STATE_EXPECT_AUTH_RESPONSE; +} + + +void ReflectorClient::renewClientCertificate(void) +{ + std::cout << m_callsign << ": Renew client certificate" << std::endl; + auto cert = m_con->sslPeerCertificate(); + if (cert.isNull() || !m_reflector->signClientCert(cert)) + { + std::cerr << "*** WARNING: Certificate resigning for '" + << m_callsign << "' failed" << std::endl; + return; + } + sendClientCert(cert); + m_con_state = STATE_EXPECT_DISCONNECT; +} /* ReflectorClient::renewClientCertificate */ + + /* * This file has not been truncated */ diff --git a/src/svxlink/reflector/ReflectorClient.h b/src/svxlink/reflector/ReflectorClient.h index 443c45c12..12932d6f0 100644 --- a/src/svxlink/reflector/ReflectorClient.h +++ b/src/svxlink/reflector/ReflectorClient.h @@ -36,6 +36,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include #include +#include #include @@ -47,7 +48,10 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include #include +#include #include +#include +#include /**************************************************************************** @@ -112,11 +116,18 @@ class ReflectorClient { public: using ClientId = ReflectorUdpMsg::ClientId; + using ClientSrc = std::pair; typedef enum { - STATE_DISCONNECTED, STATE_EXPECT_PROTO_VER, STATE_EXPECT_AUTH_RESPONSE, - STATE_CONNECTED, STATE_EXPECT_DISCONNECT + STATE_EXPECT_DISCONNECT, + STATE_DISCONNECTED, + STATE_EXPECT_PROTO_VER, + STATE_EXPECT_START_ENCRYPTION, + STATE_EXPECT_SSL_CON_READY, + STATE_EXPECT_CSR, + STATE_EXPECT_AUTH_RESPONSE, + STATE_CONNECTED } ConState; struct Rx @@ -177,6 +188,18 @@ class ReflectorClient ProtoVerRange m_pv_range; }; + class ProtoVerLargerOrEqualFilter : public Filter + { + public: + ProtoVerLargerOrEqualFilter(const ProtoVer& min) : m_pv(min) {} + virtual bool operator ()(ReflectorClient *client) const + { + return (client->protoVer() >= m_pv); + } + private: + ProtoVer m_pv; + }; + class TgFilter : public Filter { public: @@ -243,7 +266,21 @@ class ReflectorClient * @param id The id of the client object to find * @return Return the client object associated with the given id */ - static ReflectorClient* lookup(ClientId id); + static ReflectorClient* lookup(const ClientId& id); + + /** + * @brief Get the client object associated with the given source addr + * @param src The source address of the client object to find + * @return Return the client object associated with the given source addr + */ + static ReflectorClient* lookup(const ClientSrc& src); + + /** + * @brief Get the client object associated with the given callsign + * @param cs The callsign of the client object to find + * @return Return the client object associated with the given callsign + */ + static ReflectorClient* lookup(const std::string& cs); /** * @brief Remove all client objects @@ -274,6 +311,24 @@ class ReflectorClient */ ClientId clientId(void) const { return m_client_id; } + /** + * @brief Get the local IP address associated with this connection + * @return Returns an IP address + */ + Async::IpAddress localHost(void) const + { + return m_con->localHost(); + } + + /** + * @brief Get the local TCP port associated with this connection + * @return Returns a port number + */ + uint16_t localPort(void) const + { + return m_con->localPort(); + } + /** * @brief Return the remote IP address * @return Returns the IP address of the client @@ -283,6 +338,11 @@ class ReflectorClient return m_con->remoteHost(); } + uint16_t remotePort(void) const + { + return m_con->remotePort(); + } + /** * @brief Return the remote port number * @return Returns the local port number used by the client @@ -297,7 +357,7 @@ class ReflectorClient * client so that UDP packets can be send to the client and check that * incoming packets originate from the correct port. */ - void setRemoteUdpPort(uint16_t port) { m_remote_udp_port = port; } + void setRemoteUdpPort(uint16_t port); /** * @brief Get the callsign for this connection @@ -315,7 +375,12 @@ class ReflectorClient * used by the receiver to find out if a packet is out of order or if a * packet has been lost in transit. */ - uint16_t nextUdpTxSeq(void) { return m_next_udp_tx_seq++; } + //uint16_t nextUdpTxSeq(void) { return m_next_udp_tx_seq++; } + + /** + * @brief Set the UDP RX sequence number + */ + void setUdpRxSeq(UdpCipher::IVCntr seq) { m_next_udp_rx_seq = seq; } /** * @brief Get the next expected UDP packet sequence number @@ -324,7 +389,7 @@ class ReflectorClient * This function will return the next expected UDP sequence number, which * is simply the previously received sequence number plus one. */ - uint16_t nextUdpRxSeq(void) { return m_next_udp_rx_seq; } + UdpCipher::IVCntr nextUdpRxSeq(void) { return m_next_udp_rx_seq; } /** * @brief Send a TCP message to the remote end @@ -426,9 +491,33 @@ class ReflectorClient const Json::Value& nodeInfo(void) const { return m_node_info; } + uint32_t udpCipherIVCntrNext() { return m_udp_cipher_iv_cntr++; } + std::vector udpCipherIV(void) const; + + void setUdpCipherIVRand(const std::vector& iv_rand) + { + m_udp_cipher_iv_rand = iv_rand; + } + std::vector udpCipherIVRand(void) const + { + return m_udp_cipher_iv_rand; + } + + void setUdpCipherKey(const std::vector& key) + { + m_udp_cipher_key = key; + } + std::vector udpCipherKey(void) const { return m_udp_cipher_key; } + + void certificateUpdated(Async::SslX509& cert); + + sigc::signal csrReceived; + private: using ClientIdRandomDist = std::uniform_int_distribution; using ClientMap = std::map; + using ClientSrcMap = std::map; + using ClientCallsignMap = std::map; static const uint16_t MIN_MAJOR_VER = 0; static const uint16_t MIN_MINOR_VER = 6; @@ -439,21 +528,25 @@ class ReflectorClient static const unsigned UDP_HEARTBEAT_RX_CNT_RESET = 120; static const ClientId CLIENT_ID_MAX = std::numeric_limits::max(); + static const ClientId CLIENT_ID_MIN = 1; static ClientMap client_map; + static ClientSrcMap client_src_map; + static ClientCallsignMap client_callsign_map; static std::mt19937 id_gen; static ClientIdRandomDist id_dist; Async::FramedTcpConnection* m_con; - unsigned char m_auth_challenge[MsgAuthChallenge::CHALLENGE_LEN]; + unsigned char m_auth_challenge[MsgAuthChallenge::LENGTH]; ConState m_con_state; Async::Timer m_disc_timer; std::string m_callsign; ClientId m_client_id; + ClientSrc m_client_src; uint16_t m_remote_udp_port; Async::Config* m_cfg; - uint16_t m_next_udp_tx_seq; - uint16_t m_next_udp_rx_seq; + //uint16_t m_next_udp_tx_seq; + UdpCipher::IVCntr m_next_udp_rx_seq; Async::Timer m_heartbeat_timer; unsigned m_heartbeat_tx_cnt; unsigned m_heartbeat_rx_cnt; @@ -469,15 +562,24 @@ class ReflectorClient RxMap m_rx_map; TxMap m_tx_map; Json::Value m_node_info; + std::vector m_udp_cipher_iv_rand; + std::vector m_udp_cipher_key; + UdpCipher::IVCntr m_udp_cipher_iv_cntr; + Async::AtTimer m_renew_cert_timer; - static ClientId newClient(ReflectorClient* client); + static ClientId newClientId(ReflectorClient* client); + static ClientSrc newClientSrc(ReflectorClient* client); ReflectorClient(const ReflectorClient&); ReflectorClient& operator=(const ReflectorClient&); + void onSslConnectionReady(Async::TcpConnection *con); void onFrameReceived(Async::FramedTcpConnection *con, std::vector& data); void handleMsgProtoVer(std::istream& is); + void handleMsgCABundleRequest(std::istream& is); + void handleMsgStartEncryptionRequest(std::istream& is); void handleMsgAuthResponse(std::istream& is); + void handleMsgClientCsr(std::istream& is); void handleSelectTG(std::istream& is); void handleTgMonitor(std::istream& is); void handleNodeInfo(std::istream& is); @@ -491,6 +593,10 @@ class ReflectorClient void disconnect(void); void handleHeartbeat(Async::Timer *t); std::string lookupUserKey(const std::string& callsign); + void connectionAuthenticated(const std::string& callsign); + bool sendClientCert(const Async::SslX509& cert); + void sendAuthChallenge(void); + void renewClientCertificate(void); }; /* class ReflectorClient */ diff --git a/src/svxlink/reflector/ReflectorMsg.h b/src/svxlink/reflector/ReflectorMsg.h index 2f1442c34..a66773f93 100644 --- a/src/svxlink/reflector/ReflectorMsg.h +++ b/src/svxlink/reflector/ReflectorMsg.h @@ -6,7 +6,7 @@ \verbatim SvxReflector - An audio reflector for connecting SvxLink Servers -Copyright (C) 2003-2019 Tobias Blomberg / SM0SVX +Copyright (C) 2003-2024 Tobias Blomberg / SM0SVX This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -34,8 +34,9 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * ****************************************************************************/ -#include -#include +#include +#include +#include /**************************************************************************** @@ -44,6 +45,9 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * ****************************************************************************/ +#include +#include +#include /**************************************************************************** @@ -104,7 +108,9 @@ class ReflectorMsg : public Async::Msg { public: static const uint32_t MAX_PREAUTH_FRAME_SIZE = 64; - static const uint32_t MAX_POSTAUTH_FRAME_SIZE = 16384; + static const uint32_t MAX_PRE_SSL_SETUP_SIZE = 4096; + static const uint32_t MAX_POST_SSL_SETUP_SIZE = 16384; + static const uint32_t MAX_POSTAUTH_FRAME_SIZE = 32768; /** * @brief Constuctor @@ -154,7 +160,7 @@ class ReflectorMsgBase : public ReflectorMsg /** * @brief The base class for UDP network messages * @author Tobias Blomberg / SM0SVX - * @date 2017-02-12 + * @date 2023-08-08 This is the top most base class for UDP messages. It is typically used as the argument type for functions that take a UDP message as argument. @@ -164,13 +170,18 @@ class ReflectorUdpMsg : public Async::Msg public: using ClientId = uint16_t; + /** + * @brief Default constuctor + */ + ReflectorUdpMsg(void) : m_type(0) {} + /** * @brief Constuctor * @param type The message type * @param client_id The client ID + * @param seq Message sequence number */ - ReflectorUdpMsg(uint16_t type=0, ClientId client_id=0, uint16_t seq=0) - : m_type(type), m_client_id(client_id), m_seq(seq) {} + ReflectorUdpMsg(uint16_t type) : m_type(type) {} /** * @brief Destructor @@ -183,6 +194,50 @@ class ReflectorUdpMsg : public Async::Msg */ uint16_t type(void) const { return m_type; } + ASYNC_MSG_MEMBERS(m_type) + + private: + uint16_t m_type; +}; + + +/** + * @brief The header class for UDP network messages in protocol V2 + * @author Tobias Blomberg / SM0SVX + * @date 2017-02-12 + +This is the header class for UDP messages in protocol version < 3. + */ +class ReflectorUdpMsgV2 : public Async::Msg +{ + public: + using ClientId = ReflectorUdpMsg::ClientId; + + /** + * @brief Default constuctor + */ + ReflectorUdpMsgV2(void) : m_type(0), m_client_id(0), m_seq(0) {} + + /** + * @brief Constuctor + * @param type The message type + * @param client_id The client ID + * @param seq Message sequence number + */ + ReflectorUdpMsgV2(uint16_t type, ClientId client_id, uint16_t seq) + : m_type(type), m_client_id(client_id), m_seq(seq) {} + + /** + * @brief Destructor + */ + virtual ~ReflectorUdpMsgV2(void) {} + + /** + * @brief Get the message type + * @return Returns the message type + */ + uint16_t type(void) const { return m_type; } + /** * @brief Get the clientId * @return Returns the client ID @@ -257,7 +312,7 @@ protocol. class MsgProtoVer : public ReflectorMsgBase<5> { public: - static const uint16_t MAJOR = 2; + static const uint16_t MAJOR = 3; static const uint16_t MINOR = 0; MsgProtoVer(void) : m_major(MAJOR), m_minor(MINOR) {} MsgProtoVer(uint16_t major, uint16_t minor) @@ -312,17 +367,25 @@ random number. When received by the client, a MsgAuthResponse message is sent. class MsgAuthChallenge : public ReflectorMsgBase<10> { public: - static const size_t CHALLENGE_LEN = 20; - MsgAuthChallenge(void) : m_challenge(CHALLENGE_LEN) + static const size_t LENGTH = 20; + MsgAuthChallenge(void) : m_challenge(LENGTH) { - gcry_create_nonce(&m_challenge.front(), CHALLENGE_LEN); + int rc = RAND_bytes(&m_challenge.front(), LENGTH); + if (rc != 1) + { + unsigned long err = ERR_get_error(); + std::cerr << "*** WARNING: Failed to generate challenge. " + "RAND_bytes failed with error code " << err + << std::endl; + m_challenge.clear(); + } } const uint8_t *challenge(void) const { - if (m_challenge.size() != CHALLENGE_LEN) + if (m_challenge.size() != LENGTH) { - return 0; + return nullptr; } return &m_challenge[0]; } @@ -350,8 +413,7 @@ the network. class MsgAuthResponse : public ReflectorMsgBase<11> { public: - static const int ALGO = GCRY_MD_SHA1; - static const size_t DIGEST_LEN = 20; + static const size_t DIGEST_LEN = 20; MsgAuthResponse(void) {} /** @@ -362,11 +424,13 @@ class MsgAuthResponse : public ReflectorMsgBase<11> */ MsgAuthResponse(const std::string& callsign, const std::string &key, const unsigned char *challenge) - : m_digest(DIGEST_LEN), m_callsign(callsign) + : m_callsign(callsign) { - if (!calcDigest(&m_digest.front(), key.c_str(), key.size(), challenge)) + if (!calcHMAC(m_digest, key, challenge)) { - exit(1); + std::cerr << "*** ERROR: Digest calculation failed in MsgAuthResponse" + << std::endl; + abort(); } } @@ -392,10 +456,10 @@ class MsgAuthResponse : public ReflectorMsgBase<11> */ bool verify(const std::string &key, const unsigned char *challenge) const { - unsigned char digest[DIGEST_LEN]; - bool ok = calcDigest(digest, key.c_str(), key.size(), challenge); - return ok && (m_digest.size() == DIGEST_LEN) && - (memcmp(&m_digest.front(), digest, DIGEST_LEN) == 0); + Async::Digest::Signature digest; + bool ok = calcHMAC(digest, key, challenge); + return ok && (m_digest.size() == digest.size()) && + Async::Digest::sigEqual(m_digest, digest); } ASYNC_MSG_MEMBERS(m_callsign, m_digest); @@ -404,27 +468,21 @@ class MsgAuthResponse : public ReflectorMsgBase<11> std::vector m_digest; std::string m_callsign; - bool calcDigest(unsigned char *digest, const char *key, - int keylen, const unsigned char *challenge) const + bool calcHMAC(Async::Digest::Signature& hmac, const std::string& key, + const unsigned char *challenge) const { - unsigned char *digest_ptr = 0; - gcry_md_hd_t hd = { 0 }; - gcry_error_t err = gcry_md_open(&hd, ALGO, GCRY_MD_FLAG_HMAC); - if (err) goto error; - err = gcry_md_setkey(hd, key, keylen); - if (err) goto error; - gcry_md_write(hd, challenge, MsgAuthChallenge::CHALLENGE_LEN); - digest_ptr = gcry_md_read(hd, 0); - memcpy(digest, digest_ptr, DIGEST_LEN); - gcry_md_close(hd); - return true; - - error: - gcry_md_close(hd); - std::cerr << "*** ERROR: gcrypt error: " - << gcry_strsource(err) << "/" << gcry_strerror(err) - << std::endl; - return false; + // Create the key object + Async::SslKeypair pkey; + bool ok = pkey.newRawPrivateKey(EVP_PKEY_HMAC, key); + + // Initialize the digest signing with the hash algorithm and the key + Async::Digest dgst; + ok = ok && dgst.signInit("sha1", pkey); + + // Process the challenge to produce the HMAC + ok = ok && dgst.sign(hmac, challenge, MsgAuthChallenge::LENGTH); + + return ok; } }; /* MsgAuthResponse */ @@ -466,10 +524,186 @@ class MsgError : public ReflectorMsgBase<13> }; /* MsgError */ +/** +@brief Request that the server start encryption +@author Tobias Blomberg / SM0SVX +@date 2024-05-11 + +This message is sent by the client to request the server to start enrypting +the communications channel with SSL/TLS. Mutual authentication is required so +that both server and client know that they are talking to the right peer. +If the client does not send a certificate a fallback to password based +authentication is done. +*/ +class MsgStartEncryptionRequest : public ReflectorMsgBase<14> +{ + public: + ASYNC_MSG_NO_MEMBERS +}; /* MsgStartEncryptionRequest */ + + +/** +@brief Command the client to start encryption +@author Tobias Blomberg / SM0SVX +@date 2020-08-01 + +This message is sent by the server to command the client to start enrypting +the communications channel with SSL/TLS. Mutual authentication is required so +that both server and client know that they are talking to the right peer. +If the client does not send a certificate a fallback to password based +authentication is done. +*/ +class MsgStartEncryption : public ReflectorMsgBase<15> +{ + public: + ASYNC_MSG_NO_MEMBERS +}; /* MsgStartEncryption */ + + +/** + * @brief Command the client to send a CSR + * @author Tobias Blomberg / SM0SVX + * @date 2024-05-11 + * + * This message is sent by the server to command the client to send a + * Certificate Signing Request so that the server can provide a signed client + * certificate. The client is expected to send a MsgClientCsr message. + */ +class MsgClientCsrRequest : public ReflectorMsgBase<16> +{ + public: + ASYNC_MSG_NO_MEMBERS +}; /* MsgClientCsrRequest */ + + +/** + * @brief Send a CSR to the server + * @author Tobias Blomberg / SM0SVX + * @date 2024-05-11 + * + * This message is used by the client to send a Certificate Signing Request to + * the server. It must be sent when the client receives a MsgClientCsrRequest. + * The client may also send a CSR, after the connection has been + * authenticated, whenever it find the need for it. + */ +class MsgClientCsr : public ReflectorMsgBase<17> +{ + public: + MsgClientCsr(const std::string& pem="") : m_pem(pem) {} + const std::string& csrPem(void) const { return m_pem; } + + ASYNC_MSG_MEMBERS(m_pem) + + private: + std::string m_pem; +}; /* MsgClientCsr */ + + +/** + * @brief Send a signed client certificate to the client + * @author Tobias Blomberg / SM0SVX + * @date 2024-05-11 + * + * This message is used by the server to send a signed client certificate to a + * client. This is done whenever the server has a valid client certificate + * stored for the client and the server determine that the client probably + * does not have the correct version of the certificate. + */ +class MsgClientCert : public ReflectorMsgBase<18> +{ + public: + MsgClientCert(const std::string& pem="") : m_pem(pem) {} + const std::string& certPem(void) const { return m_pem; } + + ASYNC_MSG_MEMBERS(m_pem) + + private: + std::string m_pem; +}; /* MsgClientCert */ + + +/** + * @brief Send information about the CA bundle + * @author Tobias Blomberg / SM0SVX + * @date 2024-05-11 + * + * This message is used by the server to send information about the CA bundle + * currently in use. The client can use this information to determine if a new + * CA bundle need to be downloaded. + */ +class MsgCAInfo : public ReflectorMsgBase<19> +{ + public: + MsgCAInfo(size_t size=0, const std::vector& md={}) + : m_size(size), m_md(md) {} + size_t pemSize(void) const { return m_size; } + const std::vector& md(void) const { return m_md; } + + ASYNC_MSG_MEMBERS(m_size, m_md) + + private: + uint16_t m_size; + std::vector m_md; +}; /* MsgCAInfo */ + + +/** + * @brief Request that the server send the current CA bundle + * @author Tobias Blomberg / SM0SVX + * @date 2024-05-11 + * + * This message is used by a client to request that the server send the CA + * bundle currently in use. It may only be sent as a reply to the MsgCAInfo + * message. + */ +class MsgCABundleRequest : public ReflectorMsgBase<20> +{ + public: + ASYNC_MSG_NO_MEMBERS +}; /* MsgCABundleRequest */ + + +/** + * @brief Send the CA bundle currently in use to the client + * @author Tobias Blomberg / SM0SVX + * @date 2024-05-11 + * + * This message is used by the server to send the CA bundle currently in use + * to the client. In order for the client to have a way to determine if the CA + * bundle is valid, this message also contain a signature and the certificate + * chain used to sign the bundle. If the signing certificate can be verified + * against the old CA bundle, the client can be sure that the CA bundle is + * from the correct authority. + */ +class MsgCABundle : public ReflectorMsgBase<21> +{ + public: + static constexpr const char* MD_ALG = "sha256"; + MsgCABundle(const std::string& ca_pem="", + const Async::Digest::Signature& sig={}, + const std::string& cert_pem="") + : m_ca_pem(ca_pem), m_sig(sig), m_cert_pem(cert_pem) {} + const std::string& caPem(void) const { return m_ca_pem; } + const Async::Digest::Signature& signature(void) const { return m_sig; } + const std::string& certPem(void) const { return m_cert_pem; } + + ASYNC_MSG_MEMBERS(m_ca_pem, m_sig, m_cert_pem) + + private: + std::string m_ca_pem; + Async::Digest::Signature m_sig; + std::string m_cert_pem; +}; /* MsgCABundle */ + + + + + + /** @brief Server information TCP network message @author Tobias Blomberg / SM0SVX -@date 2017-02-12 +@date 2023-07-24 This message is sent by the server to the client to inform about server and connection properties. @@ -479,12 +713,12 @@ class MsgServerInfo : public ReflectorMsgBase<100> public: using ClientId = ReflectorUdpMsg::ClientId; - MsgServerInfo(ClientId client_id=0, - std::vector codecs=std::vector()) + MsgServerInfo(void) {} + MsgServerInfo(ClientId client_id, std::vector codecs) : m_client_id(client_id), m_codecs(codecs) {} ClientId clientId(void) const { return m_client_id; } std::vector& nodes(void) { return m_nodes; } - std::vector& codecs(void) { return m_codecs; } + const std::vector& codecs(void) { return m_codecs; } ASYNC_MSG_MEMBERS(m_reserved, m_client_id, m_nodes, m_codecs) @@ -965,7 +1199,7 @@ class MsgStateEvent : public ReflectorMsgBase<110> /** @brief Client information @author Tobias Blomberg / SM0SVX -@date 2019-10-06 +@date 2023-07-24 This message is sent by a client to inform the reflector server about various facts about the client. JSON is used so that information can be added without @@ -974,7 +1208,45 @@ redefining the message type. class MsgNodeInfo : public ReflectorMsgBase<111> { public: - MsgNodeInfo(const std::string& json="") + MsgNodeInfo(void) {} + MsgNodeInfo(const std::vector& udp_cipher_iv_rand, + const std::vector& udp_cipher_key, + const std::string& json) + : m_udp_cipher_iv_rand(udp_cipher_iv_rand), + m_udp_cipher_key(udp_cipher_key), m_json(json) {} + + const std::vector& ivRand(void) const + { + return m_udp_cipher_iv_rand; + } + const std::vector& udpCipherKey(void) const + { + return m_udp_cipher_key; + } + const std::string& json(void) const { return m_json; } + + ASYNC_MSG_MEMBERS(m_udp_cipher_iv_rand, m_udp_cipher_key, m_json); + + private: + std::vector m_udp_cipher_iv_rand; + std::vector m_udp_cipher_key; + std::string m_json; +}; /* MsgNodeInfo */ + + +/** +@brief Client information +@author Tobias Blomberg / SM0SVX +@date 2019-10-06 + +This message is sent by a client to inform the reflector server about various +facts about the client. JSON is used so that information can be added without +redefining the message type. +*/ +class MsgNodeInfoV2 : public ReflectorMsgBase<111> +{ + public: + MsgNodeInfoV2(const std::string& json="") : m_json(json) {} const std::string& json(void) const { return m_json; } @@ -983,7 +1255,7 @@ class MsgNodeInfo : public ReflectorMsgBase<111> private: std::string m_json; -}; /* MsgNodeInfo */ +}; /* MsgNodeInfoV2 */ /** @@ -1111,6 +1383,21 @@ class MsgTxStatus : public ReflectorMsgBase<113> }; /* class MsgTxStatus */ +/** +@brief Start UDP Encryption +@author Tobias Blomberg / SM0SVX +@date 2023-08-07 + +This message is sent by the server to inform the client that it's ok to start +UDP encryption. +*/ +class MsgStartUdpEncryption : public ReflectorMsgBase<114> +{ + public: + ASYNC_MSG_NO_MEMBERS +}; /* MsgStartUdpEncryption */ + + /***************************** UDP Messages *****************************/ /** @@ -1204,6 +1491,101 @@ struct MsgUdpSignalStrengthValues }; /* MsgUdpSignalStrengthValues */ +/** +@brief A namespace for holding UDP ciphering information +@author Tobias Blomberg / SM0SVX +@date 2023-08-03 + +This namespace hold some constants, types and classes that are used when +forming ciphered UDP datagrams. +*/ +namespace UdpCipher +{ + using IVCntr = uint32_t; + using ClientId = ReflectorUdpMsg::ClientId; + + static constexpr const char* NAME = "AES-128-GCM"; + static constexpr const size_t AADLEN = 4; + static constexpr const size_t TAGLEN = 8; + static constexpr const size_t IVLEN = 12; + static constexpr const size_t IVRANDLEN = IVLEN - sizeof(IVCntr) - + sizeof(ClientId); + + struct AAD : public Async::Msg + { + AAD(void) {} + AAD(IVCntr cntr) : iv_cntr(cntr) {} + IVCntr iv_cntr = 0; + ASYNC_MSG_MEMBERS(iv_cntr) + }; + + struct InitialAAD : public AAD + { + InitialAAD(void) {} + InitialAAD(ClientId id) : AAD(0), client_id(id) {} + ClientId client_id = 0; + ASYNC_MSG_DERIVED_FROM(AAD) + ASYNC_MSG_MEMBERS(client_id) + }; + + class IV : public Async::Msg + { + public: + IV(void) {} + IV(const std::vector& rand, ClientId client_id, IVCntr cntr) + : m_client_id(client_id), m_cntr(cntr) + { + for (size_t i=0; i(void) const + { + std::vector iv; + iv.reserve(IVLEN); + push_ostreambuf posbuf(iv); + std::ostream pos(&posbuf); + pack(pos); + return iv; + } + + ASYNC_MSG_MEMBERS(m_rand, m_client_id, m_cntr) + + private: + template + struct push_ostreambuf : public std::streambuf + { + push_ostreambuf(Container& ctr) : m_ctr(ctr) {} + + protected: + std::streamsize xsputn(const char_type* s, std::streamsize n) override + { + for (std::streamsize i=0; i "${CA_DB_PATH}/ca.db.serial" + #echo "unique_subject = no" > "${CA_DB_PATH}/ca.db.index.attr" + local openssl_args=(-newkey rsa:2048 -keyout "${CA_PATH}/ca.key") + if ${self_signed}; then + openssl_args+=(-x509 -days 7300 -out "${CA_CRT_PATH}") + else + openssl_args+=(-out "${CA_CSR_PATH}") + fi + openssl req -config <(ca_conf) "${openssl_args[@]}" &&: + if [[ $? -ne 0 ]]; then + echo "*** ERROR: Failed to create CA key and certificate" + return 1 + fi + if ${self_signed}; then + [[ -e "${CA_BUNDLE_PATH}" ]] || cp "${CA_PATH}/ca.crt" "${CA_BUNDLE_PATH}" + else + cat <<-_EOF_ + + --------------------------------------------------------------------- + Send the generated Certificate Signing Request (CSR) located in file + + ${CA_CSR_PATH} + + to the Certificate Authority (CA) for signing. When the signed + certificate is returned by the CA, place it at these two locations: + + ${CA_CRT_PATH} + ${CA_BUNDLE_PATH} + + --------------------------------------------------------------------- + + _EOF_ + fi +} + +ca_cmd_config() +{ + echo "--- CA OpenSSL configuration file" + ca_conf +} + +ca_cmd_pending() +{ + echo "--- Pending Certificate Signing Requests" + shopt -s nullglob + for csrfile in "${PENDING_CSRS_PATH}"/*.csr; do + #echo -n "${csrfile##*/}: " + openssl req -in "${csrfile}" -noout -subject | sed 's/^subject=//' + done +} + +ca_cmd_rmpending() +{ + local callsign="${1:-}" + if ! shift; then + echo "*** ERROR: Missing argument for 'rmpending' command." + echo + print_help + return 1 + fi + echo "--- Remove Pending Certificate Signing Request: ${callsign}" + + local csrfile="${PENDING_CSRS_PATH}/${callsign}.csr" + if [[ ! -e "${csrfile}" ]]; then + echo "*** ERROR: Could not find CSR file: ${csrfile}" + echo + ca_cmd_pending + return 1 + fi + + rm -f "${csrfile}" +} + +ca_cmd_sign() +{ + local callsign="${1:-}" + if ! shift; then + echo "*** ERROR: Missing argument for 'sign' command." + echo + print_help + return 1 + fi + + echo "--- Sign Certificate Signing Request for ${callsign}" + + local csrfile="${PENDING_CSRS_PATH}/${callsign}.csr" + if [[ ! -e "${csrfile}" ]]; then + echo "*** ERROR: Could not find CSR file: ${csrfile}" + return 1 + fi + + openssl ca \ + -config <(ca_conf) \ + -out "${CERTS_PATH}/${callsign}.crt" \ + -infiles "${csrfile}" &&: + if [[ $? -ne 0 ]]; then + echo "*** ERROR: Failed to sign ${csrfile}" + return 1 + fi + + mv "${csrfile}" "${CSRS_PATH}/${csrfile##*/}" &&: + if [[ $? -ne 0 ]]; then + echo "*** ERROR: Failed to move CSR from pending to signed" + return 1 + fi +} + +ca_cmd_revoke() +{ + local callsign="${1:-}" + if ! shift; then + echo "*** ERROR: Missing argument for 'revoke' command." + echo + print_help + return 1 + fi + + echo "--- Revoke Certificate for ${callsign}" + + local crtfile="${CERTS_PATH}/${callsign}.crt" + if [[ ! -e "${crtfile}" ]]; then + echo "*** ERROR: Could not find certificate file: ${crtfile}" + return 1 + fi + + openssl ca \ + -config <(ca_conf) \ + -revoke "${crtfile}" \ + -crl_reason superseded &&: + if [[ $? -ne 0 ]]; then + echo "*** ERROR: Failed to revoke ${crtfile}" + return 1 + fi + + openssl ca \ + -config <(ca_conf) \ + -gencrl \ + -out "${CRL_PATH}" +} + +run_command() +{ + local cmd_name="$1"; shift + local cmd_func="ca_cmd_${cmd_name}" + if ! declare -F "${cmd_func}" &>/dev/null; then + echo "*** ERROR: No such command: '${cmd_name}'" + return 1 + fi + "${cmd_func}" "$@" +} + +print_help() +{ + cat <<-_EOF_ + Usage: ${0##*/} [command args] + + Commands: + + config -- Print OpenSSL configuration file + pending -- List pending certificate signing requests + rmpending -- Remove a pending certificate signing request + sign -- Sign a pending certificate signing request + revoke -- Revoke a certificate + + _EOF_ +} + +main() +{ + if [[ $# -lt 1 ]]; then + print_help + exit 1 + fi + + local cmd="$1"; shift + + initialize_directories + initialize_ca_db + run_command "${cmd}" "$@" +} + +main "$@" + +# vim: set filetype=sh: diff --git a/src/svxlink/reflector/svxreflector.conf b/src/svxlink/reflector/svxreflector.conf index 753d52afb..74fdf3356 100644 --- a/src/svxlink/reflector/svxreflector.conf +++ b/src/svxlink/reflector/svxreflector.conf @@ -15,6 +15,48 @@ TG_FOR_V1_CLIENTS=999 #RANDOM_QSY_RANGE=12399:100 #HTTP_SRV_PORT=8080 COMMAND_PTY=/dev/shm/reflector_ctrl +#CALLSIGN_MATCH="[A-Z0-9][A-Z]{0,2}\\d[A-Z0-9]{1,3}[A-Z](?:-[A-Z0-9]{1,3})?" +#CERT_PKI_DIR=pki/ +#CERT_CA_KEYS_DIR=private/ +#CERT_CA_PENDING_CSRS_DIR=pending_csrs/ +#CERT_CA_CSRS_DIR=csrs/ +#CERT_CA_CERTS_DIR=certs/ + +[ROOT_CA] +#KEYFILE=svxreflector_root_ca.key +#CRTFILE=svxreflector_root_ca.crt +#COMMON_NAME=SvxReflector Root CA +#ORG_UNIT=SvxLink +#ORG=MyOrg +#LOCALITY=MyTown +#STATE=StateOrProvince +#COUNTRY=XX +#EMAIL_ADDRESS=sysop@svxlink.example.org + +[ISSUING_CA] +#KEYFILE=svxreflector_issuing_ca.key +#CSRFILE=svxreflector_issuing_ca.csr +#CRTFILE=svxreflector_issuing_ca.crt +#COMMON_NAME=SvxReflector Issuing CA +#ORG_UNIT=SvxLink +#ORG=MyOrg +#LOCALITY=MyTown +#STATE=StateOrProvince +#COUNTRY=XX +#EMAIL_ADDRESS=sysop@svxlink.example.org + +[SERVER_CERT] +#KEYFILE=svxreflector.key +#CSRFILE=svxreflector.csr +#CRTFILE=svxreflector.crt +#COMMON_NAME=svxreflector.example.org +#ORG_UNIT=SvxLink +#ORG=MyOrg +#LOCALITY=MyTown +#STATE=StateOrProvince +#COUNTRY=XX +#SUBJECT_ALT_NAME=DNS:public-hostname.example.org,IP:172.17.1.42 +#EMAIL_ADDRESS=sysop@svxlink.example.org [USERS] #SM0ABC-1=MyNodes diff --git a/src/svxlink/reflector/svxreflector.cpp b/src/svxlink/reflector/svxreflector.cpp index 68133af32..80d580809 100644 --- a/src/svxlink/reflector/svxreflector.cpp +++ b/src/svxlink/reflector/svxreflector.cpp @@ -360,20 +360,19 @@ int main(int argc, const char *argv[]) } string main_cfg_filename(cfg_filename); + std::string main_cfg_dir = "."; + auto slash_pos = main_cfg_filename.rfind('/'); + if (slash_pos != std::string::npos) + { + main_cfg_dir = main_cfg_filename.substr(0, slash_pos); + } + string cfg_dir; if (cfg.getValue("GLOBAL", "CFG_DIR", cfg_dir)) { if (cfg_dir[0] != '/') { - int slash_pos = main_cfg_filename.rfind('/'); - if (slash_pos != -1) - { - cfg_dir = main_cfg_filename.substr(0, slash_pos+1) + cfg_dir; - } - else - { - cfg_dir = string("./") + cfg_dir; - } + cfg_dir = main_cfg_dir + "/" + cfg_dir; } DIR *dir = opendir(cfg_dir.c_str()); @@ -411,6 +410,13 @@ int main(int argc, const char *argv[]) cfg.getValue("GLOBAL", "TIMESTAMP_FORMAT", tstamp_format); + //std::string pki_dir; + //if (!cfg.getValue("GLOBAL", "CERT_PKI_DIR", pki_dir)) + //{ + // pki_dir = main_cfg_dir + "/pki"; + // cfg.setValue("GLOBAL", "CERT_PKI_DIR", pki_dir); + //} + cout << PROGRAM_NAME " v" SVXREFLECTOR_VERSION " Copyright (C) 2003-2024 Tobias Blomberg / SM0SVX\n\n"; cout << PROGRAM_NAME " comes with ABSOLUTELY NO WARRANTY. " diff --git a/src/svxlink/svxlink/CMakeLists.txt b/src/svxlink/svxlink/CMakeLists.txt index 5e6af1167..cfe517be0 100644 --- a/src/svxlink/svxlink/CMakeLists.txt +++ b/src/svxlink/svxlink/CMakeLists.txt @@ -12,7 +12,7 @@ set(SVXLINK_TCL_EVENT_FILES # Logic core plugins to build set(SVXLINK_LOGIC_CORES - Dummy Simplex Repeater Reflector + Dummy Simplex Repeater ReflectorV2 Reflector ) # Find the popt library @@ -42,10 +42,10 @@ set(LIBS ${LIBS} ${TCL_LIBRARY}) include_directories(${TCL_INCLUDE_PATH}) # Find the GCrypt library -find_package(GCrypt REQUIRED) -set(LIBS ${LIBS} ${GCRYPT_LIBRARIES}) -include_directories(${GCRYPT_INCLUDE_DIRS}) -add_definitions(${GCRYPT_DEFINITIONS}) +#find_package(GCrypt REQUIRED) +#set(LIBS ${LIBS} ${GCRYPT_LIBRARIES}) +#include_directories(${GCRYPT_INCLUDE_DIRS}) +#add_definitions(${GCRYPT_DEFINITIONS}) # Find the dl library - only for Linux, not required for FreeBSD if (${CMAKE_SYSTEM_NAME} MATCHES "Linux") @@ -113,6 +113,7 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/svxlink_gpio_down.in install(TARGETS svxlink DESTINATION ${BIN_INSTALL_DIR}) install_mkdir(${SVX_SPOOL_INSTALL_DIR}/qso_recorder ${SVXLINK_USER}:${SVXLINK_GROUP}) install_mkdir(${SVX_SHARE_INSTALL_DIR}/sounds) +install_mkdir(${SVX_LOCAL_STATE_DIR}/pki ${SVXLINK_USER}:${SVXLINK_GROUP}) install_if_not_exists(${CMAKE_CURRENT_BINARY_DIR}/svxlink.conf ${SVX_SYSCONF_INSTALL_DIR} ) diff --git a/src/svxlink/svxlink/ReflectorLogic.cpp b/src/svxlink/svxlink/ReflectorLogic.cpp index 38b6bb794..725179f3c 100644 --- a/src/svxlink/svxlink/ReflectorLogic.cpp +++ b/src/svxlink/svxlink/ReflectorLogic.cpp @@ -6,7 +6,7 @@ \verbatim SvxLink - A Multi Purpose Voice Services System for Ham Radio Use -Copyright (C) 2003-2023 Tobias Blomberg / SM0SVX +Copyright (C) 2003-2024 Tobias Blomberg / SM0SVX This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -30,13 +30,19 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * ****************************************************************************/ +#include +//#include +//#include + #include #include #include #include #include #include +#include #include +#include /**************************************************************************** @@ -45,7 +51,13 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * ****************************************************************************/ -#include +#include +#include +#include +#include +#include +#include +#include #include #include #include @@ -59,7 +71,6 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ****************************************************************************/ #include "ReflectorLogic.h" -#include "../reflector/ReflectorMsg.h" #include "EventHandler.h" @@ -96,6 +107,53 @@ using namespace Async; * ****************************************************************************/ +namespace { + //void splitFilename(const std::string& filename, std::string& dirname, + // std::string& basename) + //{ + // std::string ext; + // basename = filename; + + // size_t basenamepos = filename.find_last_of('/'); + // if (basenamepos != string::npos) + // { + // if (basenamepos + 1 < filename.size()) + // { + // basename = filename.substr(basenamepos + 1); + // } + // dirname = filename.substr(0, basenamepos + 1); + // } + + // size_t extpos = basename.find_last_of('.'); + // if (extpos != string::npos) + // { + // if (extpos+1 < basename.size()) + // ext = basename.substr(extpos+1); + // basename.erase(extpos); + // } + //} + + template + void hexdump(const T& d) + { + std::ostringstream ss; + std::string sep(48, '-'); + ss << sep << "\n"; + ss << std::setfill('0') << std::hex; + size_t cnt = 0; + for (const auto& byte : d) + { + std::string spacer(" "); + if (++cnt % 16 == 0) + { + spacer = "\n"; + } + ss << std::setw(2) << unsigned(byte) << spacer; + } + std::cout << ss.str() << ((cnt % 16 > 0) ? "\n" : "") + << sep << std::endl; + } +}; /**************************************************************************** @@ -123,6 +181,9 @@ extern "C" { * ****************************************************************************/ +namespace { + MsgProtoVer proto_ver; +}; /**************************************************************************** @@ -135,7 +196,7 @@ ReflectorLogic::ReflectorLogic(void) : m_msg_type(0), m_udp_sock(0), m_logic_con_in(0), m_logic_con_out(0), m_reconnect_timer(60000, Timer::TYPE_ONESHOT, false), - m_next_udp_tx_seq(0), m_next_udp_rx_seq(0), + /*m_next_udp_tx_seq(0),*/ m_next_udp_rx_seq(0), m_heartbeat_timer(1000, Timer::TYPE_PERIODIC, false), m_dec(0), m_flush_timeout_timer(3000, Timer::TYPE_ONESHOT, false), m_udp_heartbeat_tx_cnt_reset(DEFAULT_UDP_HEARTBEAT_TX_CNT_RESET), @@ -177,6 +238,10 @@ ReflectorLogic::ReflectorLogic(void) sigc::mem_fun(*this, &ReflectorLogic::onDisconnected)); m_con.frameReceived.connect( sigc::mem_fun(*this, &ReflectorLogic::onFrameReceived)); + m_con.verifyPeer.connect( + sigc::mem_fun(*this, &ReflectorLogic::onVerifyPeer)); + m_con.sslConnectionReady.connect( + sigc::mem_fun(*this, &ReflectorLogic::onSslConnectionReady)); m_con.setMaxFrameSize(ReflectorMsg::MAX_PREAUTH_FRAME_SIZE); } /* ReflectorLogic::ReflectorLogic */ @@ -250,26 +315,150 @@ bool ReflectorLogic::initialize(Async::Config& cfgobj, const std::string& logic_ } } + if (!cfg().getValue(name(), "CERT_PKI_DIR", m_pki_dir) || m_pki_dir.empty()) + { + m_pki_dir = std::string(SVX_LOCAL_STATE_DIR) + "/pki"; + } + if (!m_pki_dir.empty() && (access(m_pki_dir.c_str(), F_OK) != 0)) + { + std::cout << name() + << ": Create PKI directory \"" << m_pki_dir << "\"" + << std::endl; + if (mkdir(m_pki_dir.c_str(), 0755) != 0) + { + std::cerr << "*** ERROR: Could not create PKI directory \"" + << m_pki_dir << "\" in logic \"" << name() << "\"" + << std::endl; + return false; + } + } + if (!cfg().getValue(name(), "CALLSIGN", m_callsign) || m_callsign.empty()) { std::cerr << "*** ERROR: " << name() - << "/CALLSIGN missing in configuration or is empty" << std::endl; + << "/CALLSIGN missing in configuration or is empty" + << std::endl; return false; } - if (!cfg().getValue(name(), "AUTH_KEY", m_auth_key) || m_auth_key.empty()) + if (!cfg().getValue(name(), "CERT_KEYFILE", m_keyfile)) { - std::cerr << "*** ERROR: " << name() - << "/AUTH_KEY missing in configuration or is empty" << std::endl; - return false; + m_keyfile = m_pki_dir + "/" + m_callsign + ".key"; } - if (m_auth_key == "Change this key now!") + if (access(m_keyfile.c_str(), F_OK) != 0) { - cerr << "*** ERROR: You must change " << name() << "/AUTH_KEY from the " - "default value" << endl; + std::cout << name() + << ": Certificate key file not found. Generating key file \"" + << m_keyfile << "\"" << std::endl; + Async::SslKeypair keypair; + keypair.generate(2048); + if (!keypair.writePrivateKeyFile(m_keyfile)) + { + std::cerr << "*** ERROR: Failed to write private key file to \"" + << m_keyfile << "\" in logic \"" << name() << "\"" + << std::endl; + return false; + } + } + if (!m_ssl_pkey.readPrivateKeyFile(m_keyfile)) + { + std::cerr << "*** ERROR: Failed to read private key file from \"" + << m_keyfile << "\" in logic \"" << name() << "\"" + << std::endl; return false; } + m_ssl_csr.setVersion(Async::SslCertSigningReq::VERSION_1); + m_ssl_csr.addSubjectName("CN", m_callsign); + const std::vector> subject_names{ + {SN_givenName, LN_givenName, "GIVEN_NAME"}, + {SN_surname, LN_surname, "SURNAME"}, + {SN_organizationalUnitName, LN_organizationalUnitName, "ORG_UNIT"}, + {SN_organizationName, LN_organizationName, "ORG"}, + {SN_localityName, LN_localityName, "LOCALITY"}, + {SN_stateOrProvinceName, LN_stateOrProvinceName, "STATE"}, + {SN_countryName, LN_countryName, "COUNTRY"}, + }; + std::string value; + const std::string prefix = "CERT_SUBJ_"; + for (const auto& snames : subject_names) + { + if (std::accumulate(snames.begin(), snames.end(), false, + [&](bool found, const std::string& cfgsname) + { + return found || cfg().getValue(name(), prefix + cfgsname, value); + }) && + !value.empty()) + { + if (!m_ssl_csr.addSubjectName(snames[0], value)) + { + std::cerr << "*** ERROR: Failed to set subject name '" << snames[0] + << "' in certificate signing request." << std::endl; + return false; + } + } + } + Async::SslX509Extensions csr_exts; + csr_exts.addBasicConstraints("critical, CA:FALSE"); + csr_exts.addKeyUsage( + "critical, digitalSignature, keyEncipherment, keyAgreement"); + csr_exts.addExtKeyUsage("clientAuth"); + if (cfg().getValue(name(), "CERT_EMAIL", value) && !value.empty()) + { + csr_exts.addSubjectAltNames(std::string("email:") + value); + } + m_ssl_csr.addExtensions(csr_exts); + m_ssl_csr.setPublicKey(m_ssl_pkey); + m_ssl_csr.sign(m_ssl_pkey); + + if (!cfg().getValue(name(), "CERT_CSRFILE", m_csrfile)) + { + m_csrfile = m_pki_dir + "/" + m_callsign + ".csr"; + } + Async::SslCertSigningReq req(nullptr); + if (!req.readPemFile(m_csrfile) || !req.verify(m_ssl_pkey) || + (m_ssl_csr.digest() != req.digest())) + { + std::cout << name() << ": Saving certificate signing request file '" + << m_csrfile << "'" << std::endl; + //std::cout << "### New CSR" << std::endl; + if (!m_ssl_csr.writePemFile(m_csrfile)) + { + // FIXME: Read SSL error stack + + std::cerr << "*** ERROR: Failed to write certificate signing " + "request file to '" + << m_csrfile << "' in logic '" << name() << "'" + << std::endl; + return false; + } + } + //m_ssl_csr.print(); + + if (!cfg().getValue(name(), "CERT_CRTFILE", m_crtfile)) + { + m_crtfile = m_pki_dir + "/" + m_callsign + ".crt"; + } + + if (!loadClientCertificate()) + { + std::cerr << "*** WARNING[" << name() << "]: Failed to load client " + "certificate. Ifnoring on-disk stored certificate file '" + << m_crtfile << "'." << std::endl; + } + + if (!cfg().getValue(name(), "CERT_CAFILE", m_cafile)) + { + m_cafile = m_pki_dir + "/ca-bundle.crt"; + } + if (!m_ssl_ctx.setCaCertificateFile(m_cafile)) + { + std::cerr << "*** WARNING[" << name() << "]: Failed to read CA file '" + << m_cafile << "'. Will try to retrieve it from the server." + << std::endl; + //return false; + } + string event_handler_str; if (!cfg().getValue(name(), "EVENT_HANDLER", event_handler_str) || event_handler_str.empty()) @@ -356,7 +545,7 @@ bool ReflectorLogic::initialize(Async::Config& cfgobj, const std::string& logic_ std::numeric_limits::max(), m_tg_select_timeout, true)) { - std::cout << "*** ERROR[" << name() + std::cerr << "*** ERROR[" << name() << "]: Illegal value (" << m_tg_select_timeout << ") for TG_SELECT_TIMEOUT" << std::endl; return false; @@ -775,22 +964,24 @@ void ReflectorLogic::onConnected(void) << m_con.remoteHost() << ":" << m_con.remotePort() << " (" << (m_con.isPrimary() ? "primary" : "secondary") << ")" << std::endl; - sendMsg(MsgProtoVer()); + sendMsg(proto_ver); m_udp_heartbeat_tx_cnt = m_udp_heartbeat_tx_cnt_reset; m_udp_heartbeat_rx_cnt = UDP_HEARTBEAT_RX_CNT_RESET; m_tcp_heartbeat_tx_cnt = TCP_HEARTBEAT_TX_CNT_RESET; m_tcp_heartbeat_rx_cnt = TCP_HEARTBEAT_RX_CNT_RESET; m_heartbeat_timer.setEnable(true); - m_next_udp_tx_seq = 0; + //m_next_udp_tx_seq = 0; m_next_udp_rx_seq = 0; timerclear(&m_last_talker_timestamp); - m_con_state = STATE_EXPECT_AUTH_CHALLENGE; - m_con.setMaxFrameSize(ReflectorMsg::MAX_PREAUTH_FRAME_SIZE); + //m_con_state = STATE_EXPECT_AUTH_CHALLENGE; + m_con.setMaxFrameSize(ReflectorMsg::MAX_PRE_SSL_SETUP_SIZE); + m_con_state = STATE_EXPECT_CA_INFO; + //m_con.setMaxFrameSize(ReflectorMsg::MAX_PREAUTH_FRAME_SIZE); processEvent("reflector_connection_status_update 1"); } /* ReflectorLogic::onConnected */ -void ReflectorLogic::onDisconnected(TcpConnection *con, +void ReflectorLogic::onDisconnected(TcpConnection*, TcpConnection::DisconnectReason reason) { cout << name() << ": Disconnected from " << m_con.remoteHost() << ":" @@ -800,7 +991,7 @@ void ReflectorLogic::onDisconnected(TcpConnection *con, m_reconnect_timer.setEnable(reason == TcpConnection::DR_ORDERED_DISCONNECT); delete m_udp_sock; m_udp_sock = 0; - m_next_udp_tx_seq = 0; + //m_next_udp_tx_seq = 0; m_next_udp_rx_seq = 0; m_heartbeat_timer.setEnable(false); if (m_flush_timeout_timer.isEnabled()) @@ -818,9 +1009,88 @@ void ReflectorLogic::onDisconnected(TcpConnection *con, } /* ReflectorLogic::onDisconnected */ -void ReflectorLogic::onFrameReceived(FramedTcpConnection *con, +bool ReflectorLogic::onVerifyPeer(TcpConnection *con, bool preverify_ok, + X509_STORE_CTX *x509_store_ctx) +{ + //std::cout << "### ReflectorLogic::onVerifyPeer: preverify_ok=" + // << (preverify_ok ? "yes" : "no") << std::endl; + + Async::SslX509 cert(*x509_store_ctx); + preverify_ok = preverify_ok && !cert.isNull(); + preverify_ok = preverify_ok && !cert.commonName().empty(); + if (!preverify_ok) + { + std::cout << "*** ERROR[" << name() + << "]: Certificate verification failed for reflector server" + << std::endl; + std::cout << "------------- Peer Certificate --------------" << std::endl; + cert.print(); + std::cout << "---------------------------------------------" << std::endl; + } + + return preverify_ok; +} /* ReflectorLogic::onVerifyPeer */ + + +void ReflectorLogic::onSslConnectionReady(TcpConnection*) +{ + std::cout << name() << ": Encrypted connection established" << std::endl; + + if (m_con_state != STATE_EXPECT_SSL_CON_READY) + { + std::cerr << "*** ERROR[" + << name() << "]: Unexpected SSL connection readiness" + << std::endl; + disconnect(); + return; + } + + if (m_con.sslVerifyResult() != X509_V_OK) + { + std::cerr << "*** ERROR[" + << name() << "]: SSL Certificate verification failed" + << std::endl; + disconnect(); + return; + } + + auto peer_cert = m_con.sslPeerCertificate(); + //std::cout << "### Common Name=" << peer_cert.commonName() << std::endl; + + bool cert_match_host = false; + std::string remote_name = m_con.remoteHostName(); + if (!remote_name.empty()) + { + if (remote_name[remote_name.size()-1] == '.') + { + remote_name.erase(remote_name.size()-1); + } + //std::cout << "### Remote hostname=" << remote_name << std::endl; + cert_match_host |= peer_cert.matchHost(remote_name); + } + cert_match_host |= peer_cert.matchIp(m_con.remoteHost()); + if (!cert_match_host) + { + std::cerr << "*** EROR[" << name() + << "]: The server certificate does not match the remote " + "hostname ("<< remote_name << ") nor the IP address (" + << m_con.remoteHost() << ")" + << std::endl; + disconnect(); + return; + } + + m_con.setMaxFrameSize(ReflectorMsg::MAX_POST_SSL_SETUP_SIZE); + + m_con_state = STATE_EXPECT_AUTH_ANSWER; +} /* ReflectorLogic::onSslConnectionReady */ + + +void ReflectorLogic::onFrameReceived(FramedTcpConnection*, std::vector& data) { + //std::cout << "### ReflectorLogic::onFrameReceived: data.size()=" + // << data.size() << std::endl; char *buf = reinterpret_cast(&data.front()); int len = data.size(); @@ -830,13 +1100,13 @@ void ReflectorLogic::onFrameReceived(FramedTcpConnection *con, ReflectorMsg header; if (!header.unpack(ss)) { - cout << "*** ERROR[" << name() - << "]: Unpacking failed for TCP message header\n"; + std::cerr << "*** ERROR[" << name() + << "]: Unpacking failed for TCP message header" << std::endl; disconnect(); return; } - if ((header.type() > 100) && !isLoggedIn()) + if ((header.type() > 100) && !isTcpLoggedIn()) { cerr << "*** ERROR[" << name() << "]: Unexpected protocol message received" << endl; @@ -862,6 +1132,21 @@ void ReflectorLogic::onFrameReceived(FramedTcpConnection *con, case MsgAuthOk::TYPE: handleMsgAuthOk(); break; + case MsgCAInfo::TYPE: + handleMsgCAInfo(ss); + break; + case MsgStartEncryption::TYPE: + handleMsgStartEncryption(); + break; + case MsgCABundle::TYPE: + handleMsgCABundle(ss); + break; + case MsgClientCsrRequest::TYPE: + handleMsgClientCsrRequest(); + break; + case MsgClientCert::TYPE: + handleMsgClientCert(ss); + break; case MsgServerInfo::TYPE: handleMsgServerInfo(ss); break; @@ -883,6 +1168,9 @@ void ReflectorLogic::onFrameReceived(FramedTcpConnection *con, case MsgRequestQsy::TYPE: handleMsgRequestQsy(ss); break; + case MsgStartUdpEncryption::TYPE: + handlMsgStartUdpEncryption(ss); + break; default: // Better just ignoring unknown messages for easier addition of protocol // messages while being backwards compatible @@ -915,21 +1203,39 @@ void ReflectorLogic::handleMsgProtoVerDowngrade(std::istream& is) MsgProtoVerDowngrade msg; if (!msg.unpack(is)) { - cerr << "*** ERROR[" << name() << "]: Could not unpack MsgProtoVerDowngrade" << endl; + std::cerr << "*** ERROR[" << name() + << "]: Could not unpack MsgProtoVerDowngrade" << std::endl; disconnect(); return; } - cout << name() << ": Server too old and we cannot downgrade to protocol version " - << msg.majorVer() << "." << msg.minorVer() << " from " - << MsgProtoVer::MAJOR << "." << MsgProtoVer::MINOR - << endl; - disconnect(); +#if 0 + if (msg.majorVer() == 2) + { + std::cout << name() + << ": The server is requesting protocol downgrade to v" + << msg.majorVer() << "." << msg.minorVer() << ". Complying." + << std::endl; + sendMsg(MsgProtoVer(msg.majorVer(), msg.minorVer())); + m_con_state = STATE_EXPECT_AUTH_CHALLENGE; + } + else +#endif + { + std::cout << name() + << ": Server too old and we cannot downgrade to protocol version " + << msg.majorVer() << "." << msg.minorVer() << " from " + << proto_ver.majorVer() << "." << proto_ver.minorVer() + << std::endl; + disconnect(); + } } /* ReflectorLogic::handleMsgProtoVerDowngrade */ void ReflectorLogic::handleMsgAuthChallenge(std::istream& is) { - if (m_con_state != STATE_EXPECT_AUTH_CHALLENGE) + if ((m_con_state != STATE_EXPECT_AUTH_ANSWER) /* && + (m_con_state != STATE_EXPECT_CERT) && + (m_con_state != STATE_EXPECT_AUTH_RESPONSE) */) { cerr << "*** ERROR[" << name() << "]: Unexpected MsgAuthChallenge\n"; disconnect(); @@ -939,7 +1245,8 @@ void ReflectorLogic::handleMsgAuthChallenge(std::istream& is) MsgAuthChallenge msg; if (!msg.unpack(is)) { - cerr << "*** ERROR[" << name() << "]: Could not unpack MsgAuthChallenge\n"; + std::cerr << "*** ERROR[" << name() + << "]: Could not unpack MsgAuthChallenge" << std::endl; disconnect(); return; } @@ -950,25 +1257,344 @@ void ReflectorLogic::handleMsgAuthChallenge(std::istream& is) disconnect(); return; } - sendMsg(MsgAuthResponse(m_callsign, m_auth_key, challenge)); - m_con_state = STATE_EXPECT_AUTH_OK; + + std::string auth_key; + cfg().getValue(name(), "AUTH_KEY", auth_key); + sendMsg(MsgAuthResponse(m_callsign, auth_key, challenge)); + //m_con_state = STATE_EXPECT_AUTH_ANSWER; } /* ReflectorLogic::handleMsgAuthChallenge */ void ReflectorLogic::handleMsgAuthOk(void) { - if (m_con_state != STATE_EXPECT_AUTH_OK) + if (m_con_state != STATE_EXPECT_AUTH_ANSWER) { cerr << "*** ERROR[" << name() << "]: Unexpected MsgAuthOk\n"; disconnect(); return; } - cout << name() << ": Authentication OK" << endl; + std::cout << name() << ": Authentication OK" << std::endl; m_con_state = STATE_EXPECT_SERVER_INFO; m_con.setMaxFrameSize(ReflectorMsg::MAX_POSTAUTH_FRAME_SIZE); + + auto cert = m_con.sslCertificate(); + if (!cert.isNull()) + { + struct stat csrst, crtst; + if ((stat(m_csrfile.c_str(), &csrst) == 0) && + (stat(m_crtfile.c_str(), &crtst) == 0) && + (csrst.st_mtim.tv_sec > crtst.st_mtim.tv_sec)) + { + //std::cout << "### CSR mtime=" << csrst.st_mtim.tv_sec + // << " CRT mtime=" << crtst.st_mtim.tv_sec + // << std::endl; + std::cout << name() + << ": The CSR is newer than the certificate. Sending " + "certificate signing request to server." << std::endl; + sendMsg(MsgClientCsr(m_ssl_csr.pem())); + } + } } /* ReflectorLogic::handleMsgAuthOk */ +void ReflectorLogic::handleMsgCAInfo(std::istream& is) +{ + if (m_con_state != STATE_EXPECT_CA_INFO) + { + std::cerr << "*** ERROR[" << name() + << "]: Unexpected MsgCAInfo" << std::endl; + disconnect(); + return; + } + + MsgCAInfo msg; + if (!msg.unpack(is)) + { + std::cerr << "*** ERROR[" << name() << "]: Could not unpack MsgCAInfo" + << std::endl; + disconnect(); + return; + } + + //std::cout << "### ca_size=" << msg.pemSize() + // //<< " ca_url='" << msg.url() << "'" + // << std::endl; + //std::cout << "### Message digest size: " << msg.md().size() << std::endl; + //hexdump(msg.md()); + + std::ifstream ca_ifs(m_cafile); + bool request_ca_bundle = !ca_ifs.good(); + if (ca_ifs.good()) + { + std::string ca_pem(std::istreambuf_iterator{ca_ifs}, {}); + auto ca_md = Async::Digest().md("sha256", ca_pem); + request_ca_bundle = (ca_md != msg.md()); + if (request_ca_bundle) + { + std::cout << "### Local CA PEM\n" << ca_pem << std::endl; + std::cout << "SHA256 Digest\n"; + hexdump(ca_md); + + // FIXME: Don't overwrite CA bundle if we have one already. To do + // that we need to implement verification of the new bundle. + std::cout << "*** WARNING[" << name() + << "]: You need to update your CA bundle to the latest " + "version. Contact the reflector sysop." << std::endl; + request_ca_bundle = false; + } + } + ca_ifs.close(); + if (request_ca_bundle) + { + //std::cout << "### Requesting CA Bundle" << std::endl; + sendMsg(MsgCABundleRequest()); + m_con_state = STATE_EXPECT_CA_BUNDLE; + } + else + { + //std::cout << "### Requesting encrypted communications channel" + // << std::endl; + m_con.setMaxFrameSize(ReflectorMsg::MAX_PRE_SSL_SETUP_SIZE); + sendMsg(MsgStartEncryptionRequest()); + m_con_state = STATE_EXPECT_START_ENCRYPTION; + } + // FIXME: Handle CA bundle download + + //if (m_ssl_ctx.caCertificateFileIsSet()) + //{ + // sendMsg(MsgStartEncryption(false)); + // //m_con.enableSsl(true); + // //m_con_state = STATE_EXPECT_SSL_CON_READY; + // m_con_state = STATE_EXPECT_CA_BUNDLE; + //} + //else + //{ + //} +} /* ReflectorLogic::handleMsgCAInfo */ + + +void ReflectorLogic::handleMsgCABundle(std::istream& is) +{ + std::cout << "### ReflectorLogic::handleMsgCABundle" << std::endl; + + if (m_con_state != STATE_EXPECT_CA_BUNDLE) + { + std::cerr << "*** ERROR[" << name() + << "]: Unexpected MsgCABundle" << std::endl; + disconnect(); + return; + } + MsgCABundle msg; + if (!msg.unpack(is)) + { + std::cerr << "*** ERROR[" << name() << "]: Could not unpack MsgCABundle" + << std::endl; + disconnect(); + return; + } + + std::cout << "### CA:\n" << msg.caPem() << std::endl; + std::cout << "### Signature:\n"; + hexdump(msg.signature()); + Async::SslX509 signing_cert; + signing_cert.readPem(msg.certPem()); + std::cout << "### Signing cert chain:\n" << std::string(48, '-') << "\n"; + signing_cert.print(); + std::cout << std::string(48, '-') << "\n"; + + if (msg.caPem().empty()) + { + std::cerr << "*** ERROR[" << name() << "]: Received empty CA bundle" + << std::endl; + disconnect(); + return; + } + + Async::Digest dgst; + auto signing_cert_pubkey = signing_cert.publicKey(); + std::cout << "### Signing public key:\n" + << signing_cert_pubkey.publicKeyPem() << std::endl; + bool signature_ok = + dgst.signVerifyInit(MsgCABundle::MD_ALG, signing_cert_pubkey) && + dgst.signVerifyUpdate(msg.caPem()) && + dgst.signVerifyFinal(msg.signature()); + std::cout << "### Signature check: " << (signature_ok ? "OK" : "FAIL") + << std::endl; + if (!signature_ok) + { + std::cout << "*** WARNING[" << name() + << "]: Received CA bundle with invalid signature" << std::endl; + disconnect(); + return; + } + + // FIXME: Verify signing certificate against old CA + + if (!msg.caPem().empty()) + { + std::cout << name() << ": Writing received CA bundle to '" << m_cafile + << "'" << std::endl; + std::ofstream ofs(m_cafile); + if (ofs.good()) + { + ofs << msg.caPem(); + ofs.close(); + } + else + { + std::cerr << "*** ERROR[" << name() << "]: Could not write CA file '" + << m_cafile << "'" << std::endl; + } + + if (!m_ssl_ctx.setCaCertificateFile(m_cafile)) + { + std::cerr << "*** ERROR[" << name() << "]: Failed to read CA file '" + << m_cafile << "'" << std::endl; + disconnect(); + return; + } + } + + sendMsg(MsgStartEncryptionRequest()); + m_con_state = STATE_EXPECT_START_ENCRYPTION; +} /* ReflectorLogic::handleMsgCABundle */ + + +void ReflectorLogic::handleMsgStartEncryption(void) +{ + //std::cout << "### ReflectorLogic::handleMsgStartEncryption" << std::endl; + + if (m_con_state != STATE_EXPECT_START_ENCRYPTION) + { + std::cerr << "*** ERROR[" << name() + << "]: Unexpected MsgStartEncryption" << std::endl; + disconnect(); + return; + } + + std::cout << name() << ": Setting up encrypted communications channel" + << std::endl; + + m_con.enableSsl(true); + m_con_state = STATE_EXPECT_SSL_CON_READY; +} /* ReflectorLogic::handleMsgStartEncryption */ + + +void ReflectorLogic::handleMsgClientCsrRequest(void) +{ + if (m_con_state != STATE_EXPECT_AUTH_ANSWER) + { + std::cerr << "*** ERROR[" << name() << "]: Unexpected MsgClientCsrRequest" + << std::endl; + disconnect(); + return; + } + + //Async::SslCertSigningReq req; + //if (!req.readPemFile(m_csrfile) || req.isNull()) + //{ + // std::cerr << "*** ERROR[" << name() << "]: Missing or invalid CSR file " + // "'" << m_csrfile << "'" << std::endl; + // disconnect(); + // return; + //} + + std::cout << name() << ": Sending requested Certificate Signing Request " + "to server" << std::endl; + + sendMsg(MsgClientCsr(m_ssl_csr.pem())); + //m_con_state = STATE_EXPECT_CERT; +} /* ReflectorLogic::handleMsgClientCsrRequest */ + + +void ReflectorLogic::handleMsgClientCert(std::istream& is) +{ + if (m_con_state < STATE_EXPECT_AUTH_ANSWER) + { + std::cerr << "*** ERROR[" << name() << "]: Unexpected MsgClientCert" + << std::endl; + disconnect(); + return; + } + MsgClientCert msg; + if (!msg.unpack(is)) + { + cerr << "*** ERROR[" << name() << "]: Could not unpack MsgClientCert\n"; + disconnect(); + return; + } + + if (msg.certPem().empty()) + { + std::cout << name() << ": Received an empty certificate. " + //<< "Sending Certificate Signing Request to server." + << std::endl; + //sendMsg(MsgClientCsr(m_ssl_csr.pem())); + disconnect(); + return; + } + + std::cout << name() << ": Received certificate from server" + << std::endl; + Async::SslX509 cert; + if (!cert.readPem(msg.certPem()) || cert.isNull()) + { + std::cerr << "*** ERROR[" << name() + << "]: Failed to parse certificate PEM data from server" + << std::endl; + disconnect(); + return; + } + std::cout << "---------- New Client Certificate -----------" << std::endl; + cert.print(); + std::cout << "---------------------------------------------" << std::endl; + + if (cert.publicKey() != m_ssl_csr.publicKey()) + { + std::cerr << "*** ERROR[" << name() << "]: The client certificate " + "received from the server does not match our current " + "private key. " + //"Sending Certificate Signing Request." + << std::endl; + //sendMsg(MsgClientCsr(m_ssl_csr.pem())); + disconnect(); + return; + } + + std::ofstream ofs(m_crtfile); + if (!ofs.good() || !(ofs << msg.certPem())) + { + std::cerr << "*** ERROR[" << name() + << "]: Failed to write certificate file to \"" + << m_crtfile << "\"" << std::endl; + disconnect(); + return; + } + ofs.close(); + + //if (!cert.writePemFile(m_crtfile)) + //{ + // std::cerr << "*** ERROR[" << name() + // << "]: Failed to write certificate file to \"" + // << m_crtfile << "\"" << std::endl; + // disconnect(); + // return; + //} + + if (!loadClientCertificate()) + { + std::cout << name() << ": Failed to load client certificate. " + //<< "Sending certificate signing request to server" + << std::endl; + //sendMsg(MsgClientCsr(m_ssl_csr.pem())); + disconnect(); + return; + } + + reconnect(); +} /* ReflectorLogic::handleMsgClientCert */ + + void ReflectorLogic::handleMsgServerInfo(std::istream& is) { if (m_con_state != STATE_EXPECT_SERVER_INFO) @@ -1023,20 +1649,68 @@ void ReflectorLogic::handleMsgServerInfo(std::istream& is) cout << name() << ": "; if (!selected_codec.empty()) { - cout << "Using audio codec \"" << selected_codec << "\""; + std::cout << "Using audio codec \"" << selected_codec << "\"" << std::endl; + } + else + { + std::cout << "No supported codec :-(" << std::endl; + disconnect(); + return; + } + + /* + const Async::EncryptedUdpSocket::Cipher* cipher = nullptr; + for (const auto& cipher_name : msg.udpCiphers()) + { + cipher = EncryptedUdpSocket::fetchCipher(cipher_name); + if (cipher != nullptr) + { + break; + } + } + */ + std::cout << name() << ": "; + const auto cipher = EncryptedUdpSocket::fetchCipher(UdpCipher::NAME); + if (cipher != nullptr) + { + std::cout << "Using UDP cipher " << EncryptedUdpSocket::cipherName(cipher) + << std::endl; } else { - cout << "No supported codec :-("; + std::cout << "Unsupported UDP cipher " << UdpCipher::NAME + << " :-(" << std::endl; + disconnect(); + return; } - cout << endl; delete m_udp_sock; - m_udp_sock = new UdpSocket; + m_udp_cipher_iv_cntr = 1; + m_udp_sock = new Async::EncryptedUdpSocket; + m_udp_cipher_iv_rand.resize(UdpCipher::IVRANDLEN); + const char* err = "unknown reason"; + if ((err="memory allocation failure", (m_udp_sock == nullptr)) || + (err="initialization failure", !m_udp_sock->initOk()) || + (err="unsupported cipher", !m_udp_sock->setCipher(cipher)) || + (err="cipher IV rand generation failure", + !Async::EncryptedUdpSocket::randomBytes(m_udp_cipher_iv_rand)) || + (err="cipher key generation failure", !m_udp_sock->setCipherKey())) + { + std::cerr << "*** ERROR[" << name() << "]: Could not create UDP socket " + "due to " << err << std::endl; + delete m_udp_sock; + m_udp_sock = nullptr; + disconnect(); + return; + } + m_udp_sock->setCipherAADLength(UdpCipher::AADLEN); + m_udp_sock->setTagLength(UdpCipher::TAGLEN); + m_udp_sock->cipherDataReceived.connect( + mem_fun(*this, &ReflectorLogic::udpCipherDataReceived)); m_udp_sock->dataReceived.connect( mem_fun(*this, &ReflectorLogic::udpDatagramReceived)); - m_con_state = STATE_CONNECTED; + m_con_state = STATE_EXPECT_START_UDP_ENCRYPTION; std::ostringstream node_info_os; Json::StreamWriterBuilder builder; @@ -1045,7 +1719,8 @@ void ReflectorLogic::handleMsgServerInfo(std::istream& is) Json::StreamWriter* writer = builder.newStreamWriter(); writer->write(m_node_info, &node_info_os); delete writer; - MsgNodeInfo node_info_msg(node_info_os.str()); + MsgNodeInfo node_info_msg(m_udp_cipher_iv_rand, m_udp_sock->cipherKey(), + node_info_os.str()); sendMsg(node_info_msg); #if 0 @@ -1094,18 +1769,23 @@ void ReflectorLogic::handleMsgServerInfo(std::istream& is) sendMsg(node_info); #endif - if (m_selected_tg > 0) - { - cout << name() << ": Selecting TG #" << m_selected_tg << endl; - sendMsg(MsgSelectTG(m_selected_tg)); - } + //if (m_selected_tg > 0) + //{ + // cout << name() << ": Selecting TG #" << m_selected_tg << endl; + // sendMsg(MsgSelectTG(m_selected_tg)); + //} - if (!m_monitor_tgs.empty()) - { - sendMsg(MsgTgMonitor( - std::set(m_monitor_tgs.begin(), m_monitor_tgs.end()))); - } - sendUdpMsg(MsgUdpHeartbeat()); + //if (!m_monitor_tgs.empty()) + //{ + // sendMsg(MsgTgMonitor( + // std::set(m_monitor_tgs.begin(), m_monitor_tgs.end()))); + //} + + //sendUdpMsg(MsgUdpHeartbeat()); + // Send an empty datagram to open upp any firewalls + //m_udp_sock->UdpSocket::write( + // m_con.remoteHost(), m_con.remotePort(), nullptr, 0); + //sendUdpRegisterMsg(); } /* ReflectorLogic::handleMsgServerInfo */ @@ -1267,6 +1947,31 @@ void ReflectorLogic::handleMsgRequestQsy(std::istream& is) } /* ReflectorLogic::handleMsgRequestQsy */ +void ReflectorLogic::handlMsgStartUdpEncryption(std::istream& is) +{ + //std::cout << "### ReflectorLogic::handlMsgStartUdpEncryption" << std::endl; + + if (m_con_state != STATE_EXPECT_START_UDP_ENCRYPTION) + { + std::cerr << "*** ERROR[" << name() + << "]: Unexpected MsgStartUdpEncryption message" << std::endl; + disconnect(); + return; + } + + MsgStartUdpEncryption msg; + if (!msg.unpack(is)) + { + std::cerr << "*** ERROR[" << name() + << "]: Could not unpack MsgStartUdpEncryption" << std::endl; + disconnect(); + return; + } + m_con_state = STATE_EXPECT_UDP_HEARTBEAT; + sendUdpRegisterMsg(); +} /* ReflectorLogic::handlMsgStartUdpEncryption */ + + void ReflectorLogic::sendMsg(const ReflectorMsg& msg) { if (!isConnected()) @@ -1287,6 +1992,9 @@ void ReflectorLogic::sendMsg(const ReflectorMsg& msg) } if (m_con.write(ss.str().data(), ss.str().size()) == -1) { + std::cerr << "*** ERROR[" << name() + << "]: Failed to write message to network connection" + << std::endl; disconnect(); } } /* ReflectorLogic::sendMsg */ @@ -1319,14 +2027,45 @@ void ReflectorLogic::flushEncodedAudio(void) } /* ReflectorLogic::flushEncodedAudio */ +bool ReflectorLogic::udpCipherDataReceived(const IpAddress& addr, uint16_t port, + void *buf, int count) +{ + if (static_cast(count) < UdpCipher::AADLEN) + { + std::cout << "### ReflectorLogic::udpCipherDataReceived: Datagram too " + "short to hold associated data" << std::endl; + return true; + } + stringstream ss; + ss.write(reinterpret_cast(buf), UdpCipher::AADLEN); + if (!m_aad.unpack(ss)) + { + std::cout << "*** WARNING: Unpacking associated data failed for UDP " + "datagram from " << addr << ":" << port << std::endl; + return true; + } + //std::cout << "### ReflectorLogic::udpCipherDataReceived: m_aad.iv_cntr=" + // << m_aad.iv_cntr << std::endl; + m_udp_sock->setCipherIV(UdpCipher::IV{m_udp_cipher_iv_rand, 0, + m_aad.iv_cntr}); + return false; +} /* ReflectorLogic::udpCipherDataReceived */ + + void ReflectorLogic::udpDatagramReceived(const IpAddress& addr, uint16_t port, - void *buf, int count) + void* aad, void *buf, int count) { - if (!isLoggedIn()) + if (!isTcpLoggedIn()) { return; } + if (aad != nullptr) + { + //std::cout << "### ReflectorLogic::udpDatagramReceived: m_aad.iv_cntr=" + // << m_aad.iv_cntr << std::endl; + } + if (addr != m_con.remoteHost()) { cout << "*** WARNING[" << name() @@ -1353,35 +2092,60 @@ void ReflectorLogic::udpDatagramReceived(const IpAddress& addr, uint16_t port, return; } - if (header.clientId() != m_client_id) - { - cout << "*** WARNING[" << name() - << "]: UDP packet received with wrong client id " - << header.clientId() << ". Should be " << m_client_id << "." << endl; - return; - } + //if (header.clientId() != m_client_id) + //{ + // cout << "*** WARNING[" << name() + // << "]: UDP packet received with wrong client id " + // << header.clientId() << ". Should be " << m_client_id << "." << endl; + // return; + //} // Check sequence number - uint16_t udp_rx_seq_diff = header.sequenceNum() - m_next_udp_rx_seq; - if (udp_rx_seq_diff > 0x7fff) // Frame out of sequence (ignore) + if (m_aad.iv_cntr < m_next_udp_rx_seq) // Frame out of sequence (ignore) { - cout << name() - << ": Dropping out of sequence UDP frame with seq=" - << header.sequenceNum() << endl; + std::cout << name() + << ": Dropping out of sequence UDP frame with seq=" + << m_aad.iv_cntr << std::endl; return; } - else if (udp_rx_seq_diff > 0) // Frame lost + else if (m_aad.iv_cntr > m_next_udp_rx_seq) // Frame lost { - cout << name() << ": UDP frame(s) lost. Expected seq=" - << m_next_udp_rx_seq - << " but received " << header.sequenceNum() - << ". Resetting next expected sequence number to " - << (header.sequenceNum() + 1) << endl; + std::cout << name() << ": UDP frame(s) lost. Expected seq=" + << m_next_udp_rx_seq + << " but received " << m_aad.iv_cntr + << ". Resetting next expected sequence number to " + << (m_aad.iv_cntr + 1) << std::endl; } - m_next_udp_rx_seq = header.sequenceNum() + 1; + m_next_udp_rx_seq = m_aad.iv_cntr + 1; m_udp_heartbeat_rx_cnt = UDP_HEARTBEAT_RX_CNT_RESET; + if ((m_con_state == STATE_EXPECT_UDP_HEARTBEAT) && + (header.type() == MsgUdpHeartbeat::TYPE)) + { + std::cout << name() << ": Bidirectional UDP communication verified" + << std::endl; + m_con.markAsEstablished(); + m_con_state = STATE_CONNECTED; + + if (m_selected_tg > 0) + { + cout << name() << ": Selecting TG #" << m_selected_tg << endl; + sendMsg(MsgSelectTG(m_selected_tg)); + } + + if (!m_monitor_tgs.empty()) + { + sendMsg(MsgTgMonitor( + std::set(m_monitor_tgs.begin(), m_monitor_tgs.end()))); + } + } + + if (!isLoggedIn()) + { + return; + } + switch (header.type()) { case MsgUdpHeartbeat::TYPE: @@ -1424,14 +2188,9 @@ void ReflectorLogic::udpDatagramReceived(const IpAddress& addr, uint16_t port, } } /* ReflectorLogic::udpDatagramReceived */ - -void ReflectorLogic::sendUdpMsg(const ReflectorUdpMsg& msg) +void ReflectorLogic::sendUdpMsg(const UdpCipher::AAD& aad, + const ReflectorUdpMsg& msg) { - if (!isLoggedIn()) - { - return; - } - m_udp_heartbeat_tx_cnt = m_udp_heartbeat_tx_cnt_reset; if (m_udp_sock == 0) @@ -1439,19 +2198,46 @@ void ReflectorLogic::sendUdpMsg(const ReflectorUdpMsg& msg) return; } - ReflectorUdpMsg header(msg.type(), m_client_id, m_next_udp_tx_seq++); + ReflectorUdpMsg header(msg.type()); ostringstream ss; if (!header.pack(ss) || !msg.pack(ss)) { - cerr << "*** ERROR[" << name() - << "]: Failed to pack reflector TCP message\n"; + std::cerr << "*** ERROR[" << name() + << "]: Failed to pack reflector UDP message" << std::endl; + return; + } + m_udp_sock->setCipherIV(UdpCipher::IV{m_udp_cipher_iv_rand, m_client_id, + aad.iv_cntr}); + std::ostringstream adss; + if (!aad.pack(adss)) + { + std::cout << "*** WARNING: Packing associated data failed for UDP " + "datagram to " << m_con.remoteHost() << ":" + << m_con.remotePort() << std::endl; return; } m_udp_sock->write(m_con.remoteHost(), m_con.remotePort(), + adss.str().data(), adss.str().size(), ss.str().data(), ss.str().size()); } /* ReflectorLogic::sendUdpMsg */ +void ReflectorLogic::sendUdpMsg(const ReflectorUdpMsg& msg) +{ + if (!isLoggedIn()) + { + return; + } + sendUdpMsg(UdpCipher::AAD{m_udp_cipher_iv_cntr++}, msg); +} /* ReflectorLogic::sendUdpMsg */ + + +void ReflectorLogic::sendUdpRegisterMsg(void) +{ + sendUdpMsg(UdpCipher::InitialAAD{m_client_id}, MsgUdpHeartbeat()); +} /* ReflectorLogic::sendUdpRegisterMsg */ + + void ReflectorLogic::connect(void) { if (!isConnected()) @@ -1460,6 +2246,7 @@ void ReflectorLogic::connect(void) std::cout << name() << ": Connecting to service " << m_con.service() << std::endl; m_con.connect(); + m_con.setSslContext(m_ssl_ctx, false); } } /* ReflectorLogic::connect */ @@ -1519,7 +2306,14 @@ void ReflectorLogic::handleTimerTick(Async::Timer *t) if (--m_udp_heartbeat_tx_cnt == 0) { - sendUdpMsg(MsgUdpHeartbeat()); + if (m_con_state == STATE_EXPECT_UDP_HEARTBEAT) + { + sendUdpRegisterMsg(); + } + else if (isLoggedIn()) + { + sendUdpMsg(MsgUdpHeartbeat()); + } } if (--m_tcp_heartbeat_tx_cnt == 0) @@ -1863,6 +2657,53 @@ void ReflectorLogic::handlePlayDtmf(const std::string& digit, int amp, } /* ReflectorLogic::handlePlayDtmf */ +bool ReflectorLogic::loadClientCertificate(void) +{ + if (m_ssl_cert.readPemFile(m_crtfile) && + !m_ssl_cert.isNull() && + //cert.verify(m_ssl_pkey) && + m_ssl_cert.timeIsWithinRange()) + { + if (!m_ssl_ctx.setCertificateFiles(m_keyfile, m_crtfile)) + { + std::cerr << "*** ERROR: Failed to read and verify key ('" + << m_keyfile << "') and certificate ('" + << m_crtfile << "') files in logic \"" << name() << "'. " + << "If key- and cert-file does not match, the certificate " + "has expired, or is invalid for any other reason, you " + "need to remove the cert file in order to trigger the " + "generation of a new one signed by the SvxReflector " + "manager. If there is an access problem you need to fix " + "the permissions of the key- and certificate files." + << std::endl; + return false; + } + } + return true; +} /* ReflectorLogic::loadClientCertificate */ + + +void ReflectorLogic::csrAddSubjectNamesFromConfig(void) +{ + const std::string prefix = "CERT_SUBJ_"; + for (const auto& section : cfg().listSection(name())) + { + const std::string sname = section.substr(prefix.size()); + std::string value; + if ((section.rfind(prefix, 0) == 0) && + cfg().getValue(name(), prefix + sname, value) && + !value.empty()) + { + if (!m_ssl_csr.addSubjectName(sname, value)) + { + std::cerr << "*** WARNING: Failed to set subject name '" << sname + << "' in certificate signing request." << std::endl; + } + } + } +} /* ReflectorLogic::csrAddSubjectName */ + + /* * This file has not been truncated */ diff --git a/src/svxlink/svxlink/ReflectorLogic.h b/src/svxlink/svxlink/ReflectorLogic.h index 52d1d473d..d721a715b 100644 --- a/src/svxlink/svxlink/ReflectorLogic.h +++ b/src/svxlink/svxlink/ReflectorLogic.h @@ -61,6 +61,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ****************************************************************************/ #include "LogicBase.h" +#include "../reflector/ReflectorMsg.h" /**************************************************************************** @@ -71,7 +72,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA namespace Async { - class UdpSocket; + class EncryptedUdpSocket; class AudioValve; }; @@ -197,9 +198,21 @@ class ReflectorLogic : public LogicBase typedef enum { - STATE_DISCONNECTED, STATE_EXPECT_AUTH_CHALLENGE, STATE_EXPECT_AUTH_OK, - STATE_EXPECT_SERVER_INFO, STATE_CONNECTED + STATE_DISCONNECTED, + STATE_EXPECT_CA_INFO, + STATE_EXPECT_AUTH_CHALLENGE, + STATE_EXPECT_START_ENCRYPTION, + STATE_EXPECT_CA_BUNDLE, + STATE_EXPECT_SSL_CON_READY, + STATE_EXPECT_AUTH_ANSWER, + STATE_EXPECT_AUTH_OK, + STATE_EXPECT_SERVER_INFO, + STATE_EXPECT_START_UDP_ENCRYPTION, + STATE_EXPECT_UDP_HEARTBEAT, + STATE_CONNECTED } ConState; + static const ConState STATE_TCP_CONNECTED = + STATE_EXPECT_START_UDP_ENCRYPTION; typedef Async::TcpPrioClient FramedTcpClient; typedef std::set MonitorTgsSet; @@ -214,15 +227,14 @@ class ReflectorLogic : public LogicBase std::string m_reflector_host; FramedTcpClient m_con; unsigned m_msg_type; - Async::UdpSocket* m_udp_sock; - uint32_t m_client_id; - std::string m_auth_key; + Async::EncryptedUdpSocket* m_udp_sock; + ReflectorUdpMsg::ClientId m_client_id; std::string m_callsign; Async::AudioStreamStateDetector* m_logic_con_in; Async::AudioStreamStateDetector* m_logic_con_out; Async::Timer m_reconnect_timer; - uint16_t m_next_udp_tx_seq; - uint16_t m_next_udp_rx_seq; + //uint16_t m_next_udp_tx_seq; + UdpCipher::IVCntr m_next_udp_rx_seq; Async::Timer m_heartbeat_timer; Async::AudioDecoder* m_dec; Async::Timer m_flush_timeout_timer; @@ -254,15 +266,30 @@ class ReflectorLogic : public LogicBase bool m_mute_first_tx_rem; Async::Timer m_tmp_monitor_timer; int m_tmp_monitor_timeout; + Async::SslContext m_ssl_ctx; + Async::SslKeypair m_ssl_pkey; + Async::SslCertSigningReq m_ssl_csr; + Async::SslX509 m_ssl_cert; + std::string m_pki_dir; + std::string m_cafile; + std::string m_crtfile; + std::string m_keyfile; + std::string m_csrfile; bool m_use_prio; Async::Timer m_qsy_pending_timer; bool m_verbose; + std::vector m_udp_cipher_iv_rand; + UdpCipher::IVCntr m_udp_cipher_iv_cntr; + UdpCipher::AAD m_aad; ReflectorLogic(const ReflectorLogic&); ReflectorLogic& operator=(const ReflectorLogic&); void onConnected(void); void onDisconnected(Async::TcpConnection *con, Async::TcpConnection::DisconnectReason reason); + bool onVerifyPeer(Async::TcpConnection *con, bool preverify_ok, + X509_STORE_CTX *x509_store_ctx); + void onSslConnectionReady(Async::TcpConnection* con); void onFrameReceived(Async::FramedTcpConnection *con, std::vector& data); void handleMsgError(std::istream& is); @@ -274,18 +301,29 @@ class ReflectorLogic : public LogicBase void handleMsgTalkerStart(std::istream& is); void handleMsgTalkerStop(std::istream& is); void handleMsgRequestQsy(std::istream& is); + void handlMsgStartUdpEncryption(std::istream& is); void handleMsgAuthOk(void); + void handleMsgCAInfo(std::istream& is); + void handleMsgStartEncryption(void); + void handleMsgCABundle(std::istream& is); + void handleMsgClientCsrRequest(void); + void handleMsgClientCert(std::istream& is); void handleMsgServerInfo(std::istream& is); void sendMsg(const ReflectorMsg& msg); void sendEncodedAudio(const void *buf, int count); void flushEncodedAudio(void); + bool udpCipherDataReceived(const Async::IpAddress& addr, uint16_t port, + void *buf, int count); void udpDatagramReceived(const Async::IpAddress& addr, uint16_t port, - void *buf, int count); + void* aad, void *buf, int count); + void sendUdpMsg(const UdpCipher::AAD& aad, const ReflectorUdpMsg& msg); void sendUdpMsg(const ReflectorUdpMsg& msg); + void sendUdpRegisterMsg(void); void connect(void); void disconnect(void); void reconnect(void); bool isConnected(void) const; + bool isTcpLoggedIn(void) const { return m_con_state >= STATE_TCP_CONNECTED; } bool isLoggedIn(void) const { return m_con_state == STATE_CONNECTED; } void allEncodedSamplesFlushed(void); void flushTimeout(Async::Timer *t=0); @@ -306,6 +344,8 @@ class ReflectorLogic : public LogicBase void handlePlaySilence(int duration); void handlePlayTone(int fq, int amp, int duration); void handlePlayDtmf(const std::string& digit, int amp, int duration); + bool loadClientCertificate(void); + void csrAddSubjectNamesFromConfig(void); }; /* class ReflectorLogic */ diff --git a/src/svxlink/svxlink/ReflectorV2Logic.cpp b/src/svxlink/svxlink/ReflectorV2Logic.cpp new file mode 100644 index 000000000..b2a3ba7bb --- /dev/null +++ b/src/svxlink/svxlink/ReflectorV2Logic.cpp @@ -0,0 +1,1856 @@ +/** +@file ReflectorLogic.cpp +@brief A logic core that connect to the SvxReflector +@author Tobias Blomberg / SM0SVX +@date 2017-02-12 + +\verbatim +SvxLink - A Multi Purpose Voice Services System for Ham Radio Use +Copyright (C) 2003-2022 Tobias Blomberg / SM0SVX + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +\endverbatim +*/ + +/**************************************************************************** + * + * System Includes + * + ****************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include + + +/**************************************************************************** + * + * Project Includes + * + ****************************************************************************/ + +#include +#include +#include +#include + + +/**************************************************************************** + * + * Local Includes + * + ****************************************************************************/ + +#include "ReflectorV2Logic.h" +#include "../reflector/ReflectorMsg.h" +#include "EventHandler.h" + + +/**************************************************************************** + * + * Namespaces to use + * + ****************************************************************************/ + +using namespace std; +using namespace Async; + + + +/**************************************************************************** + * + * Defines & typedefs + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Local class definitions + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Prototypes + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Exported Global Variables + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Exported Global functions + * + ****************************************************************************/ + +extern "C" { + LogicBase* construct(void) { return new ReflectorLogic; } +} + + +/**************************************************************************** + * + * Local Global Variables + * + ****************************************************************************/ + +namespace { + MsgProtoVer proto_ver(2, 0); +}; + + +/**************************************************************************** + * + * Public member functions + * + ****************************************************************************/ + +ReflectorLogic::ReflectorLogic(void) + : m_msg_type(0), m_udp_sock(0), + m_logic_con_in(0), m_logic_con_out(0), + m_reconnect_timer(60000, Timer::TYPE_ONESHOT, false), + m_next_udp_tx_seq(0), m_next_udp_rx_seq(0), + m_heartbeat_timer(1000, Timer::TYPE_PERIODIC, false), m_dec(0), + m_flush_timeout_timer(3000, Timer::TYPE_ONESHOT, false), + m_udp_heartbeat_tx_cnt_reset(DEFAULT_UDP_HEARTBEAT_TX_CNT_RESET), + m_udp_heartbeat_tx_cnt(0), m_udp_heartbeat_rx_cnt(0), + m_tcp_heartbeat_tx_cnt(0), m_tcp_heartbeat_rx_cnt(0), + m_con_state(STATE_DISCONNECTED), m_enc(0), m_default_tg(0), + m_tg_select_timeout(DEFAULT_TG_SELECT_TIMEOUT), + m_tg_select_timer(1000, Async::Timer::TYPE_PERIODIC), + m_tg_select_timeout_cnt(0), m_selected_tg(0), m_previous_tg(0), + m_event_handler(0), + m_report_tg_timer(500, Async::Timer::TYPE_ONESHOT, false), + m_tg_local_activity(false), m_last_qsy(0), m_logic_con_in_valve(0), + m_mute_first_tx_loc(true), m_mute_first_tx_rem(false), + m_tmp_monitor_timer(1000, Async::Timer::TYPE_PERIODIC), + m_tmp_monitor_timeout(DEFAULT_TMP_MONITOR_TIMEOUT), m_use_prio(true), + m_qsy_pending_timer(-1), m_verbose(true) +{ + m_reconnect_timer.expired.connect( + sigc::hide(mem_fun(*this, &ReflectorLogic::reconnect))); + m_heartbeat_timer.expired.connect( + mem_fun(*this, &ReflectorLogic::handleTimerTick)); + m_flush_timeout_timer.expired.connect( + mem_fun(*this, &ReflectorLogic::flushTimeout)); + timerclear(&m_last_talker_timestamp); + + m_tg_select_timer.expired.connect(sigc::hide( + sigc::mem_fun(*this, &ReflectorLogic::tgSelectTimerExpired))); + m_report_tg_timer.expired.connect(sigc::hide( + sigc::mem_fun(*this, &ReflectorLogic::processTgSelectionEvent))); + m_tmp_monitor_timer.expired.connect(sigc::hide( + sigc::mem_fun(*this, &ReflectorLogic::checkTmpMonitorTimeout))); + m_qsy_pending_timer.expired.connect(sigc::hide( + sigc::mem_fun(*this, &ReflectorLogic::qsyPendingTimeout))); + + m_con.connected.connect( + sigc::mem_fun(*this, &ReflectorLogic::onConnected)); + m_con.disconnected.connect( + sigc::mem_fun(*this, &ReflectorLogic::onDisconnected)); + m_con.frameReceived.connect( + sigc::mem_fun(*this, &ReflectorLogic::onFrameReceived)); + m_con.setMaxFrameSize(ReflectorMsg::MAX_PREAUTH_FRAME_SIZE); +} /* ReflectorLogic::ReflectorLogic */ + + +bool ReflectorLogic::initialize(Async::Config& cfgobj, const std::string& logic_name) +{ + // Must create logic connection objects before calling LogicBase::initialize + m_logic_con_in = new Async::AudioStreamStateDetector; + m_logic_con_in->sigStreamStateChanged.connect( + sigc::mem_fun(*this, &ReflectorLogic::onLogicConInStreamStateChanged)); + m_logic_con_out = new Async::AudioStreamStateDetector; + m_logic_con_out->sigStreamStateChanged.connect( + sigc::mem_fun(*this, &ReflectorLogic::onLogicConOutStreamStateChanged)); + + if (!LogicBase::initialize(cfgobj, logic_name)) + { + return false; + } + + cfg().getValue(name(), "VERBOSE", m_verbose); + + std::vector hosts; + if (cfg().getValue(name(), "HOST", hosts)) + { + std::cout << "*** WARNING: The " << name() + << "/HOST configuration variable is deprecated. " + "Use HOSTS instead." << std::endl; + } + cfg().getValue(name(), "HOSTS", hosts); + std::string srv_domain; + cfg().getValue(name(), "DNS_DOMAIN", srv_domain); + if (srv_domain.empty() && hosts.empty()) + { + std::cerr << "*** ERROR: At least one of HOSTS or DNS_DOMAIN must be " + "specified in " << name() << std::endl; + return false; + } + + if (!srv_domain.empty()) + { + m_con.setService("svxreflector", "tcp", srv_domain); + } + if (!hosts.empty()) + { + uint16_t reflector_port = 5300; + if (cfg().getValue(name(), "PORT", reflector_port)) + { + std::cout << "*** WARNING: The " << name() + << "/PORT configuration variable is deprecated. " + "Use HOST_PORT instead." << std::endl; + } + cfg().getValue(name(), "HOST_PORT", reflector_port); + DnsResourceRecordSRV::Prio prio = 100; + cfg().getValue(name(), "HOST_PRIO", prio); + DnsResourceRecordSRV::Prio prio_inc = 1; + cfg().getValue(name(), "HOST_PRIO_INC", prio_inc); + DnsResourceRecordSRV::Weight weight = 100 / hosts.size(); + cfg().getValue(name(), "HOST_WEIGHT", weight); + for (const auto& host_spec : hosts) + { + std::string host = host_spec; + uint16_t port = reflector_port; + auto colon = host.find(':'); + if (colon != std::string::npos) + { + host = host_spec.substr(0, colon); + port = atoi(host_spec.substr(colon+1).c_str()); + } + m_con.addStaticSRVRecord(0, prio, weight, port, host); + prio += prio_inc; + } + } + + if (!cfg().getValue(name(), "CALLSIGN", m_callsign) || m_callsign.empty()) + { + std::cerr << "*** ERROR: " << name() + << "/CALLSIGN missing in configuration or is empty" << std::endl; + return false; + } + + if (!cfg().getValue(name(), "AUTH_KEY", m_auth_key) || m_auth_key.empty()) + { + std::cerr << "*** ERROR: " << name() + << "/AUTH_KEY missing in configuration or is empty" << std::endl; + return false; + } + if (m_auth_key == "Change this key now!") + { + cerr << "*** ERROR: You must change " << name() << "/AUTH_KEY from the " + "default value" << endl; + return false; + } + + string event_handler_str; + if (!cfg().getValue(name(), "EVENT_HANDLER", event_handler_str) || + event_handler_str.empty()) + { + std::cerr << "*** ERROR: Config variable " << name() + << "/EVENT_HANDLER not set or empty" << std::endl; + return false; + } + + std::vector monitor_tgs; + cfg().getValue(name(), "MONITOR_TGS", monitor_tgs); + for (std::vector::iterator it=monitor_tgs.begin(); + it!=monitor_tgs.end(); ++it) + { + std::istringstream is(*it); + MonitorTgEntry mte; + is >> mte.tg; + char modifier; + while (is >> modifier) + { + if (modifier == '+') + { + mte.prio += 1; + } + else + { + cerr << "*** ERROR: Illegal format for config variable MONITOR_TGS " + << "entry \"" << *it << "\"" << endl; + return false; + } + } + m_monitor_tgs.insert(mte); + } + +#if 0 + string audio_codec("GSM"); + if (AudioDecoder::isAvailable("OPUS") && AudioEncoder::isAvailable("OPUS")) + { + audio_codec = "OPUS"; + } + else if (AudioDecoder::isAvailable("SPEEX") && + AudioEncoder::isAvailable("SPEEX")) + { + audio_codec = "SPEEX"; + } + cfg().getValue(name(), "AUDIO_CODEC", audio_codec); +#endif + + AudioSource *prev_src = m_logic_con_in; + + cfg().getValue(name(), "MUTE_FIRST_TX_LOC", m_mute_first_tx_loc); + cfg().getValue(name(), "MUTE_FIRST_TX_REM", m_mute_first_tx_rem); + if (m_mute_first_tx_loc || m_mute_first_tx_rem) + { + m_logic_con_in_valve = new Async::AudioValve; + m_logic_con_in_valve->setOpen(false); + prev_src->registerSink(m_logic_con_in_valve); + prev_src = m_logic_con_in_valve; + } + + m_enc_endpoint = prev_src; + prev_src = 0; + + // Create dummy audio codec used before setting the real encoder + if (!setAudioCodec("DUMMY")) { return false; } + prev_src = m_dec; + + // Create jitter buffer + AudioFifo *fifo = new Async::AudioFifo(2*INTERNAL_SAMPLE_RATE); + prev_src->registerSink(fifo, true); + prev_src = fifo; + unsigned jitter_buffer_delay = 0; + cfg().getValue(name(), "JITTER_BUFFER_DELAY", jitter_buffer_delay); + if (jitter_buffer_delay > 0) + { + fifo->setPrebufSamples(jitter_buffer_delay * INTERNAL_SAMPLE_RATE / 1000); + } + + prev_src->registerSink(m_logic_con_out, true); + prev_src = 0; + + cfg().getValue(name(), "DEFAULT_TG", m_default_tg); + if (!cfg().getValue(name(), "TG_SELECT_TIMEOUT", 1U, + std::numeric_limits::max(), + m_tg_select_timeout, true)) + { + std::cout << "*** ERROR[" << name() + << "]: Illegal value (" << m_tg_select_timeout + << ") for TG_SELECT_TIMEOUT" << std::endl; + return false; + } + + int qsy_pending_timeout = -1; + if (cfg().getValue(name(), "QSY_PENDING_TIMEOUT", qsy_pending_timeout) && + (qsy_pending_timeout > 0)) + { + m_qsy_pending_timer.setTimeout(1000 * qsy_pending_timeout); + } + + m_event_handler = new EventHandler(event_handler_str, name()); + if (LinkManager::hasInstance()) + { + m_event_handler->playFile.connect( + sigc::mem_fun(*this, &ReflectorLogic::handlePlayFile)); + m_event_handler->playSilence.connect( + sigc::mem_fun(*this, &ReflectorLogic::handlePlaySilence)); + m_event_handler->playTone.connect( + sigc::mem_fun(*this, &ReflectorLogic::handlePlayTone)); + m_event_handler->playDtmf.connect( + sigc::mem_fun(*this, &ReflectorLogic::handlePlayDtmf)); + } + m_event_handler->setConfigValue.connect( + sigc::mem_fun(cfg(), &Async::Config::setValue)); + m_event_handler->setVariable("logic_name", name().c_str()); + + m_event_handler->processEvent("namespace eval Logic {}"); + list cfgvars = cfg().listSection(name()); + list::const_iterator cfgit; + for (cfgit=cfgvars.begin(); cfgit!=cfgvars.end(); ++cfgit) + { + string var = "Logic::CFG_" + *cfgit; + string value; + cfg().getValue(name(), *cfgit, value); + m_event_handler->setVariable(var, value); + } + + if (!m_event_handler->initialize()) + { + return false; + } + + cfg().getValue(name(), "TMP_MONITOR_TIMEOUT", m_tmp_monitor_timeout); + + std::string node_info_file; + if (cfg().getValue(name(), "NODE_INFO_FILE", node_info_file)) + { + std::ifstream node_info_is(node_info_file.c_str(), std::ios::in); + if (node_info_is.good()) + { + try + { + if (!(node_info_is >> m_node_info)) + { + std::cerr << "*** ERROR: Failure while reading node information file " + "\"" << node_info_file << "\"" + << std::endl; + return false; + } + } + catch (const Json::Exception& e) + { + std::cerr << "*** ERROR: Failure while reading node information " + "file \"" << node_info_file << "\": " + << e.what() + << std::endl; + return false; + } + } + else + { + std::cerr << "*** ERROR: Could not open node information file " + "\"" << node_info_file << "\"" + << std::endl; + return false; + } + } + m_node_info["sw"] = "SvxLink"; + m_node_info["swVer"] = SVXLINK_VERSION; + + cfg().getValue(name(), "UDP_HEARTBEAT_INTERVAL", + m_udp_heartbeat_tx_cnt_reset); + + connect(); + + return true; +} /* ReflectorLogic::initialize */ + + +void ReflectorLogic::remoteCmdReceived(LogicBase* src_logic, + const std::string& cmd) +{ + //cout << "### src_logic=" << src_logic->name() << " cmd=" << cmd << endl; + if (cmd == "*") + { + processEvent("report_tg_status"); + } + //else if (cmd[0] == '0') // Help + //{ + + //} + else if (cmd[0] == '1') // Select TG + { + const std::string subcmd(cmd.substr(1)); + if (!subcmd.empty()) // Select specified TG + { + istringstream is(subcmd); + uint32_t tg; + if (is >> tg) + { + selectTg(tg, "tg_command_activation", true); + m_tg_local_activity = true; + m_use_prio = false; + } + else + { + processEvent(std::string("command_failed ") + cmd); + } + } + else // Select previous TG + { + selectTg(m_previous_tg, "tg_command_activation", true); + m_tg_local_activity = true; + m_use_prio = false; + } + } + else if (cmd[0] == '2') // QSY + { + if ((m_selected_tg != 0) && isLoggedIn()) + { + const std::string subcmd(cmd.substr(1)); + if (subcmd.empty()) + { + cout << name() << ": Requesting QSY to random TG" << endl; + sendMsg(MsgRequestQsy()); + } + else + { + std::istringstream is(subcmd); + uint32_t tg = 0; + if (is >> tg) + { + cout << name() << ": Requesting QSY to TG #" << tg << endl; + sendMsg(MsgRequestQsy(tg)); + } + else + { + processEvent("tg_qsy_failed"); + } + } + } + else + { + processEvent("tg_qsy_failed"); + } + } + else if (cmd == "3") // Follow last QSY + { + if ((m_last_qsy > 0) && (m_last_qsy != m_selected_tg)) + { + selectTg(m_last_qsy, "tg_command_activation", true); + m_tg_local_activity = true; + m_use_prio = false; + } + else + { + processEvent(std::string("command_failed ") + cmd); + } + } + else if (cmd[0] == '4') // Temporarily monitor talk group + { + std::ostringstream os; + const std::string subcmd(cmd.substr(1)); + if ((m_tmp_monitor_timeout > 0) && !subcmd.empty()) + { + istringstream is(subcmd); + uint32_t tg = 0; + if (is >> tg) + { + const MonitorTgsSet::iterator it = m_monitor_tgs.find(tg); + if (it != m_monitor_tgs.end()) + { + if ((*it).timeout > 0) + { + std::cout << name() << ": Refresh temporary monitor for TG #" + << tg << std::endl; + // NOTE: (*it).timeout is mutable + (*it).timeout = m_tmp_monitor_timeout; + os << "tmp_monitor_add " << tg; + } + else + { + std::cout << "*** WARNING: Not allowed to add a temporary montior " + "for TG #" << tg << " which is being permanently " + "monitored" << std::endl; + os << "command_failed " << cmd; + } + } + else + { + std::cout << name() << ": Add temporary monitor for TG #" + << tg << std::endl; + MonitorTgEntry mte(tg); + mte.timeout = m_tmp_monitor_timeout; + m_monitor_tgs.insert(mte); + sendMsg(MsgTgMonitor(std::set( + m_monitor_tgs.begin(), m_monitor_tgs.end()))); + os << "tmp_monitor_add " << tg; + } + } + else + { + std::cout << "*** WARNING: Failed to parse temporary TG monitor " + "command: " << cmd << std::endl; + os << "command_failed " << cmd; + } + } + else + { + std::cout << "*** WARNING: Ignoring temporary TG monitoring command (" + << cmd << ") since that function is not enabled or there " + "were no TG specified" << std::endl; + os << "command_failed " << cmd; + } + processEvent(os.str()); + } + else + { + processEvent(std::string("unknown_command ") + cmd); + } +} /* ReflectorLogic::remoteCmdReceived */ + + +void ReflectorLogic::remoteReceivedTgUpdated(LogicBase *logic, uint32_t tg) +{ + //cout << "### ReflectorLogic::remoteReceivedTgUpdated: logic=" + // << logic->name() << " tg=" << tg + // << " m_mute_first_tx_loc=" << m_mute_first_tx_loc << endl; + if ((m_selected_tg == 0) && (tg > 0)) + { + selectTg(tg, "tg_local_activation", !m_mute_first_tx_loc); + m_tg_local_activity = !m_mute_first_tx_loc; + m_use_prio = false; + } +} /* ReflectorLogic::remoteReceivedTgUpdated */ + + +void ReflectorLogic::remoteReceivedPublishStateEvent( + LogicBase *logic, const std::string& event_name, const std::string& data) +{ + //cout << "### ReflectorLogic::remoteReceivedPublishStateEvent:" + // << " logic=" << logic->name() + // << " event_name=" << event_name + // << " data=" << data + // << endl; + //sendMsg(MsgStateEvent(logic->name(), event_name, msg)); + + if (event_name == "Voter:sql_state") + { + //MsgUdpSignalStrengthValues msg; + MsgSignalStrengthValues msg; + std::istringstream is(data); + Json::Value rx_arr; + is >> rx_arr; + for (Json::Value::ArrayIndex i = 0; i != rx_arr.size(); i++) + { + Json::Value& rx_data = rx_arr[i]; + std::string name = rx_data.get("name", "").asString(); + std::string id_str = rx_data.get("id", "?").asString(); + if (id_str.size() != 1) + { + return; + } + char id = id_str[0]; + int siglev = rx_data.get("siglev", 0).asInt(); + siglev = std::min(std::max(siglev, 0), 100); + bool is_enabled = rx_data.get("enabled", false).asBool(); + bool sql_open = rx_data.get("sql_open", false).asBool(); + bool is_active = rx_data.get("active", false).asBool(); + //MsgUdpSignalStrengthValues::Rx rx(id, siglev); + MsgSignalStrengthValues::Rx rx(id, siglev); + rx.setEnabled(is_enabled); + rx.setSqlOpen(sql_open); + rx.setActive(is_active); + msg.pushBack(rx); + } + //sendUdpMsg(msg); + sendMsg(msg); + } + else if (event_name == "Rx:sql_state") + { + //MsgUdpSignalStrengthValues msg; + MsgSignalStrengthValues msg; + std::istringstream is(data); + Json::Value rx_data; + is >> rx_data; + std::string name = rx_data.get("name", "").asString(); + std::string id_str = rx_data.get("id", "?").asString(); + if (id_str.size() != 1) + { + return; + } + char id = id_str[0]; + int siglev = rx_data.get("siglev", 0).asInt(); + siglev = std::min(std::max(siglev, 0), 100); + bool sql_open = rx_data.get("sql_open", false).asBool(); + //MsgUdpSignalStrengthValues::Rx rx(id, siglev); + MsgSignalStrengthValues::Rx rx(id, siglev); + rx.setEnabled(true); + rx.setSqlOpen(sql_open); + rx.setActive(sql_open); + msg.pushBack(rx); + //sendUdpMsg(msg); + sendMsg(msg); + } + else if (event_name == "Tx:state") + { + MsgTxStatus msg; + std::istringstream is(data); + Json::Value tx_data; + is >> tx_data; + std::string name = tx_data.get("name", "").asString(); + std::string id_str = tx_data.get("id", "?").asString(); + if (id_str.size() != 1) + { + return; + } + char id = id_str[0]; + if (id != '\0') + { + bool transmit = tx_data.get("transmit", false).asBool(); + MsgTxStatus::Tx tx(id); + tx.setTransmit(transmit); + msg.pushBack(tx); + sendMsg(msg); + } + } + else if (event_name == "MultiTx:state") + { + MsgTxStatus msg; + std::istringstream is(data); + Json::Value tx_arr; + is >> tx_arr; + for (Json::Value::ArrayIndex i = 0; i != tx_arr.size(); i++) + { + Json::Value& tx_data = tx_arr[i]; + std::string name = tx_data.get("name", "").asString(); + std::string id_str = tx_data.get("id", "").asString(); + if (id_str.size() != 1) + { + return; + } + char id = id_str[0]; + if (id != '\0') + { + bool transmit = tx_data.get("transmit", false).asBool(); + MsgTxStatus::Tx tx(id); + tx.setTransmit(transmit); + msg.pushBack(tx); + } + } + sendMsg(msg); + } +} /* ReflectorLogic::remoteReceivedPublishStateEvent */ + + +/**************************************************************************** + * + * Protected member functions + * + ****************************************************************************/ + +ReflectorLogic::~ReflectorLogic(void) +{ + disconnect(); + delete m_event_handler; + m_event_handler = 0; + delete m_udp_sock; + m_udp_sock = 0; + delete m_logic_con_in; + m_logic_con_in = 0; + delete m_enc; + m_enc = 0; + delete m_dec; + m_dec = 0; + delete m_logic_con_in_valve; + m_logic_con_in_valve = 0; +} /* ReflectorLogic::~ReflectorLogic */ + + + + +/**************************************************************************** + * + * Private member functions + * + ****************************************************************************/ + +void ReflectorLogic::onConnected(void) +{ + std::cout << name() << ": Connection established to " + << m_con.remoteHost() << ":" << m_con.remotePort() + << " (" << (m_con.isPrimary() ? "primary" : "secondary") << ")" + << std::endl; + sendMsg(proto_ver); + m_udp_heartbeat_tx_cnt = m_udp_heartbeat_tx_cnt_reset; + m_udp_heartbeat_rx_cnt = UDP_HEARTBEAT_RX_CNT_RESET; + m_tcp_heartbeat_tx_cnt = TCP_HEARTBEAT_TX_CNT_RESET; + m_tcp_heartbeat_rx_cnt = TCP_HEARTBEAT_RX_CNT_RESET; + m_heartbeat_timer.setEnable(true); + m_next_udp_tx_seq = 0; + m_next_udp_rx_seq = 0; + timerclear(&m_last_talker_timestamp); + m_con_state = STATE_EXPECT_AUTH_CHALLENGE; + m_con.setMaxFrameSize(ReflectorMsg::MAX_PREAUTH_FRAME_SIZE); + processEvent("reflector_connection_status_update 1"); +} /* ReflectorLogic::onConnected */ + + +void ReflectorLogic::onDisconnected(TcpConnection *con, + TcpConnection::DisconnectReason reason) +{ + cout << name() << ": Disconnected from " << m_con.remoteHost() << ":" + << m_con.remotePort() << ": " + << TcpConnection::disconnectReasonStr(reason) << endl; + //m_reconnect_timer.setTimeout(1000 + std::rand() % 5000); + m_reconnect_timer.setEnable(reason == TcpConnection::DR_ORDERED_DISCONNECT); + delete m_udp_sock; + m_udp_sock = 0; + m_next_udp_tx_seq = 0; + m_next_udp_rx_seq = 0; + m_heartbeat_timer.setEnable(false); + if (m_flush_timeout_timer.isEnabled()) + { + m_flush_timeout_timer.setEnable(false); + m_enc->allEncodedSamplesFlushed(); + } + if (timerisset(&m_last_talker_timestamp)) + { + m_dec->flushEncodedSamples(); + timerclear(&m_last_talker_timestamp); + } + m_con_state = STATE_DISCONNECTED; + processEvent("reflector_connection_status_update 0"); +} /* ReflectorLogic::onDisconnected */ + + +void ReflectorLogic::onFrameReceived(FramedTcpConnection *con, + std::vector& data) +{ + char *buf = reinterpret_cast(&data.front()); + int len = data.size(); + + stringstream ss; + ss.write(buf, len); + + ReflectorMsg header; + if (!header.unpack(ss)) + { + cout << "*** ERROR[" << name() + << "]: Unpacking failed for TCP message header\n"; + disconnect(); + return; + } + + if ((header.type() > 100) && !isLoggedIn()) + { + cerr << "*** ERROR[" << name() << "]: Unexpected protocol message received" + << endl; + disconnect(); + return; + } + + m_tcp_heartbeat_rx_cnt = TCP_HEARTBEAT_RX_CNT_RESET; + + switch (header.type()) + { + case MsgHeartbeat::TYPE: + break; + case MsgError::TYPE: + handleMsgError(ss); + break; + case MsgProtoVerDowngrade::TYPE: + handleMsgProtoVerDowngrade(ss); + break; + case MsgAuthChallenge::TYPE: + handleMsgAuthChallenge(ss); + break; + case MsgAuthOk::TYPE: + handleMsgAuthOk(); + break; + case MsgServerInfo::TYPE: + handleMsgServerInfo(ss); + break; + case MsgNodeList::TYPE: + handleMsgNodeList(ss); + break; + case MsgNodeJoined::TYPE: + handleMsgNodeJoined(ss); + break; + case MsgNodeLeft::TYPE: + handleMsgNodeLeft(ss); + break; + case MsgTalkerStart::TYPE: + handleMsgTalkerStart(ss); + break; + case MsgTalkerStop::TYPE: + handleMsgTalkerStop(ss); + break; + case MsgRequestQsy::TYPE: + handleMsgRequestQsy(ss); + break; + default: + // Better just ignoring unknown messages for easier addition of protocol + // messages while being backwards compatible + + //cerr << "*** WARNING[" << name() + // << "]: Unknown protocol message received: msg_type=" + // << header.type() << endl; + break; + } +} /* ReflectorLogic::onFrameReceived */ + + +void ReflectorLogic::handleMsgError(std::istream& is) +{ + MsgError msg; + if (!msg.unpack(is)) + { + cerr << "*** ERROR[" << name() << "]: Could not unpack MsgAuthError" << endl; + disconnect(); + return; + } + cout << name() << ": Error message received from server: " << msg.message() + << endl; + disconnect(); +} /* ReflectorLogic::handleMsgError */ + + +void ReflectorLogic::handleMsgProtoVerDowngrade(std::istream& is) +{ + MsgProtoVerDowngrade msg; + if (!msg.unpack(is)) + { + cerr << "*** ERROR[" << name() << "]: Could not unpack MsgProtoVerDowngrade" << endl; + disconnect(); + return; + } + cout << name() << ": Server too old and we cannot downgrade to protocol version " + << msg.majorVer() << "." << msg.minorVer() << " from " + << proto_ver.majorVer() << "." << proto_ver.minorVer() + << endl; + disconnect(); +} /* ReflectorLogic::handleMsgProtoVerDowngrade */ + + +void ReflectorLogic::handleMsgAuthChallenge(std::istream& is) +{ + if (m_con_state != STATE_EXPECT_AUTH_CHALLENGE) + { + cerr << "*** ERROR[" << name() << "]: Unexpected MsgAuthChallenge\n"; + disconnect(); + return; + } + + MsgAuthChallenge msg; + if (!msg.unpack(is)) + { + cerr << "*** ERROR[" << name() << "]: Could not unpack MsgAuthChallenge\n"; + disconnect(); + return; + } + const uint8_t *challenge = msg.challenge(); + if (challenge == 0) + { + cerr << "*** ERROR[" << name() << "]: Illegal challenge received\n"; + disconnect(); + return; + } + sendMsg(MsgAuthResponse(m_callsign, m_auth_key, challenge)); + m_con_state = STATE_EXPECT_AUTH_OK; +} /* ReflectorLogic::handleMsgAuthChallenge */ + + +void ReflectorLogic::handleMsgAuthOk(void) +{ + if (m_con_state != STATE_EXPECT_AUTH_OK) + { + cerr << "*** ERROR[" << name() << "]: Unexpected MsgAuthOk\n"; + disconnect(); + return; + } + cout << name() << ": Authentication OK" << endl; + m_con_state = STATE_EXPECT_SERVER_INFO; + m_con.setMaxFrameSize(ReflectorMsg::MAX_POSTAUTH_FRAME_SIZE); +} /* ReflectorLogic::handleMsgAuthOk */ + + +void ReflectorLogic::handleMsgServerInfo(std::istream& is) +{ + if (m_con_state != STATE_EXPECT_SERVER_INFO) + { + cerr << "*** ERROR[" << name() << "]: Unexpected MsgServerInfo\n"; + disconnect(); + return; + } + MsgServerInfo msg; + if (!msg.unpack(is)) + { + cerr << "*** ERROR[" << name() << "]: Could not unpack MsgServerInfo\n"; + disconnect(); + return; + } + m_client_id = msg.clientId(); + + //cout << "### MsgServerInfo: clientId=" << msg.clientId() + // << " codecs="; + //std::copy(msg.codecs().begin(), msg.codecs().end(), + // std::ostream_iterator(cout, " ")); + //cout << " nodes="; + //std::copy(msg.nodes().begin(), msg.nodes().end(), + // std::ostream_iterator(cout, " ")); + //cout << endl; + + cout << name() << ": Connected nodes: "; + const vector& nodes = msg.nodes(); + if (!nodes.empty()) + { + vector::const_iterator it = nodes.begin(); + cout << *it++; + for (; it != nodes.end(); ++it) + { + cout << ", " << *it; + } + } + cout << endl; + + string selected_codec; + for (vector::const_iterator it = msg.codecs().begin(); + it != msg.codecs().end(); + ++it) + { + if (codecIsAvailable(*it)) + { + selected_codec = *it; + setAudioCodec(selected_codec); + break; + } + } + cout << name() << ": "; + if (!selected_codec.empty()) + { + cout << "Using audio codec \"" << selected_codec << "\""; + } + else + { + cout << "No supported codec :-("; + } + cout << endl; + + delete m_udp_sock; + m_udp_sock = new UdpSocket; + m_udp_sock->dataReceived.connect( + mem_fun(*this, &ReflectorLogic::udpDatagramReceived)); + + m_con_state = STATE_CONNECTED; + + std::ostringstream node_info_os; + Json::StreamWriterBuilder builder; + builder["commentStyle"] = "None"; + builder["indentation"] = ""; //The JSON document is written on a single line + Json::StreamWriter* writer = builder.newStreamWriter(); + writer->write(m_node_info, &node_info_os); + delete writer; + MsgNodeInfoV2 node_info_msg(node_info_os.str()); + sendMsg(node_info_msg); + +#if 0 + // Set up RX and TX sites node information + MsgNodeInfo::RxSite rx_site; + MsgNodeInfo::TxSite tx_site; + rx_site.setRxName("Rx1"); + tx_site.setTxName("Tx1"); + rx_site.setQthName(cfg().getValue("LocationInfo", "QTH_NAME")); + tx_site.setQthName(cfg().getValue("LocationInfo", "QTH_NAME")); + int32_t antenna_height = 0; + if (cfg().getValue("LocationInfo", "ANTENNA_HEIGHT", antenna_height)) + { + rx_site.setAntennaHeight(antenna_height); + tx_site.setAntennaHeight(antenna_height); + } + float antenna_dir = -1.0f; + cfg().getValue("LocationInfo", "ANTENNA_DIR", antenna_dir); + rx_site.setAntennaDirection(antenna_dir); + tx_site.setAntennaDirection(antenna_dir); + double rf_frequency = 0; + cfg().getValue("LocationInfo", "FREQUENCY", rf_frequency); + if (rf_frequency < 0.0f) + { + rf_frequency = 0.0f; + } + rx_site.setRfFrequency(static_cast(1000000.0f * rf_frequency)); + tx_site.setRfFrequency(static_cast(1000000.0f * rf_frequency)); + vector ctcss_frequencies; + //ctcss_frequencies.push_back(136.5); + //rx_site.setCtcssFrequencies(ctcss_frequencies); + //tx_site.setCtcssFrequencies(ctcss_frequencies); + float tx_power = 0.0f; + cfg().getValue("LocationInfo", "TX_POWER", tx_power); + tx_site.setTxPower(tx_power); + MsgNodeInfo::TxSites tx_sites; + tx_sites.push_back(tx_site); + MsgNodeInfo::RxSites rx_sites; + rx_sites.push_back(rx_site); + + // Send node information to the server + MsgNodeInfo node_info; node_info + .setSwInfo("SvxLink v" SVXLINK_VERSION) + .setTxSites(tx_sites) + .setRxSites(rx_sites); + sendMsg(node_info); +#endif + + if (m_selected_tg > 0) + { + cout << name() << ": Selecting TG #" << m_selected_tg << endl; + sendMsg(MsgSelectTG(m_selected_tg)); + } + + if (!m_monitor_tgs.empty()) + { + sendMsg(MsgTgMonitor( + std::set(m_monitor_tgs.begin(), m_monitor_tgs.end()))); + } + sendUdpMsg(MsgUdpHeartbeat()); + +} /* ReflectorLogic::handleMsgServerInfo */ + + +void ReflectorLogic::handleMsgNodeList(std::istream& is) +{ + MsgNodeList msg; + if (!msg.unpack(is)) + { + cerr << "*** ERROR[" << name() << "]: Could not unpack MsgNodeList\n"; + disconnect(); + return; + } + cout << name() << ": Connected nodes: "; + const vector& nodes = msg.nodes(); + if (!nodes.empty()) + { + vector::const_iterator it = nodes.begin(); + cout << *it++; + for (; it != nodes.end(); ++it) + { + cout << ", " << *it; + } + } + cout << endl; +} /* ReflectorLogic::handleMsgNodeList */ + + +void ReflectorLogic::handleMsgNodeJoined(std::istream& is) +{ + MsgNodeJoined msg; + if (!msg.unpack(is)) + { + cerr << "*** ERROR[" << name() << "]: Could not unpack MsgNodeJoined\n"; + disconnect(); + return; + } + if (m_verbose) + { + std::cout << name() << ": Node joined: " << msg.callsign() << std::endl; + } +} /* ReflectorLogic::handleMsgNodeJoined */ + + +void ReflectorLogic::handleMsgNodeLeft(std::istream& is) +{ + MsgNodeLeft msg; + if (!msg.unpack(is)) + { + cerr << "*** ERROR[" << name() << "]: Could not unpack MsgNodeLeft\n"; + disconnect(); + return; + } + if (m_verbose) + { + std::cout << name() << ": Node left: " << msg.callsign() << std::endl; + } +} /* ReflectorLogic::handleMsgNodeLeft */ + + +void ReflectorLogic::handleMsgTalkerStart(std::istream& is) +{ + MsgTalkerStart msg; + if (!msg.unpack(is)) + { + cerr << "*** ERROR[" << name() << "]: Could not unpack MsgTalkerStart\n"; + disconnect(); + return; + } + cout << name() << ": Talker start on TG #" << msg.tg() << ": " + << msg.callsign() << endl; + + // Select the incoming TG if idle + if (m_tg_select_timeout_cnt == 0) + { + selectTg(msg.tg(), "tg_remote_activation", !m_mute_first_tx_rem); + } + else if (m_use_prio) + { + uint32_t selected_tg_prio = 0; + MonitorTgsSet::const_iterator selected_tg_it = + m_monitor_tgs.find(MonitorTgEntry(m_selected_tg)); + if (selected_tg_it != m_monitor_tgs.end()) + { + selected_tg_prio = selected_tg_it->prio; + } + MonitorTgsSet::const_iterator talker_tg_it = + m_monitor_tgs.find(MonitorTgEntry(msg.tg())); + if ((talker_tg_it != m_monitor_tgs.end()) && + (talker_tg_it->prio > selected_tg_prio)) + { + std::cout << name() << ": Activity on prioritized TG #" + << msg.tg() << ". Switching!" << std::endl; + selectTg(msg.tg(), "tg_remote_prio_activation", !m_mute_first_tx_rem); + } + } + + std::ostringstream ss; + ss << "talker_start " << msg.tg() << " " << msg.callsign(); + processEvent(ss.str()); +} /* ReflectorLogic::handleMsgTalkerStart */ + + +void ReflectorLogic::handleMsgTalkerStop(std::istream& is) +{ + MsgTalkerStop msg; + if (!msg.unpack(is)) + { + cerr << "*** ERROR[" << name() << "]: Could not unpack MsgTalkerStop\n"; + disconnect(); + return; + } + cout << name() << ": Talker stop on TG #" << msg.tg() << ": " + << msg.callsign() << endl; + + std::ostringstream ss; + ss << "talker_stop " << msg.tg() << " " << msg.callsign(); + processEvent(ss.str()); +} /* ReflectorLogic::handleMsgTalkerStop */ + + +void ReflectorLogic::handleMsgRequestQsy(std::istream& is) +{ + MsgRequestQsy msg; + if (!msg.unpack(is)) + { + cerr << "*** ERROR[" << name() << "]: Could not unpack MsgRequestQsy\n"; + disconnect(); + return; + } + cout << name() << ": Server QSY request for TG #" << msg.tg() << endl; + if (m_tg_local_activity) + { + selectTg(msg.tg(), "tg_qsy", true); + } + else + { + m_last_qsy = msg.tg(); + selectTg(0, "", false); + std::ostringstream os; + if (m_qsy_pending_timer.timeout() > 0) + { + cout << name() << ": Server QSY request pending" << endl; + os << "tg_qsy_pending " << msg.tg(); + m_qsy_pending_timer.setEnable(true); + m_use_prio = false; + m_tg_select_timeout_cnt = 1 + m_qsy_pending_timer.timeout() / 1000; + } + else + { + cout << name() + << ": Server QSY request ignored due to no local activity" << endl; + os << "tg_qsy_ignored " << msg.tg(); + m_use_prio = true; + m_tg_select_timeout_cnt = 0; + } + processEvent(os.str()); + } +} /* ReflectorLogic::handleMsgRequestQsy */ + + +void ReflectorLogic::sendMsg(const ReflectorMsg& msg) +{ + if (!isConnected()) + { + return; + } + + m_tcp_heartbeat_tx_cnt = TCP_HEARTBEAT_TX_CNT_RESET; + + ostringstream ss; + ReflectorMsg header(msg.type()); + if (!header.pack(ss) || !msg.pack(ss)) + { + cerr << "*** ERROR[" << name() + << "]: Failed to pack reflector TCP message\n"; + disconnect(); + return; + } + if (m_con.write(ss.str().data(), ss.str().size()) == -1) + { + disconnect(); + } +} /* ReflectorLogic::sendMsg */ + + +void ReflectorLogic::sendEncodedAudio(const void *buf, int count) +{ + if (!isLoggedIn()) + { + return; + } + + if (m_flush_timeout_timer.isEnabled()) + { + m_flush_timeout_timer.setEnable(false); + } + sendUdpMsg(MsgUdpAudio(buf, count)); +} /* ReflectorLogic::sendEncodedAudio */ + + +void ReflectorLogic::flushEncodedAudio(void) +{ + if (!isLoggedIn()) + { + flushTimeout(); + return; + } + sendUdpMsg(MsgUdpFlushSamples()); + m_flush_timeout_timer.setEnable(true); +} /* ReflectorLogic::flushEncodedAudio */ + + +void ReflectorLogic::udpDatagramReceived(const IpAddress& addr, uint16_t port, + void *buf, int count) +{ + if (!isLoggedIn()) + { + return; + } + + if (addr != m_con.remoteHost()) + { + cout << "*** WARNING[" << name() + << "]: UDP packet received from wrong source address " + << addr << ". Should be " << m_con.remoteHost() << "." << endl; + return; + } + if (port != m_con.remotePort()) + { + cout << "*** WARNING[" << name() + << "]: UDP packet received with wrong source port number " + << port << ". Should be " << m_con.remotePort() << "." << endl; + return; + } + + stringstream ss; + ss.write(reinterpret_cast(buf), count); + + ReflectorUdpMsgV2 header; + if (!header.unpack(ss)) + { + cout << "*** WARNING[" << name() + << "]: Unpacking failed for UDP message header" << endl; + return; + } + + if (header.clientId() != m_client_id) + { + cout << "*** WARNING[" << name() + << "]: UDP packet received with wrong client id " + << header.clientId() << ". Should be " << m_client_id << "." << endl; + return; + } + + // Check sequence number + uint16_t udp_rx_seq_diff = header.sequenceNum() - m_next_udp_rx_seq; + if (udp_rx_seq_diff > 0x7fff) // Frame out of sequence (ignore) + { + cout << name() + << ": Dropping out of sequence UDP frame with seq=" + << header.sequenceNum() << endl; + return; + } + else if (udp_rx_seq_diff > 0) // Frame lost + { + cout << name() << ": UDP frame(s) lost. Expected seq=" + << m_next_udp_rx_seq + << " but received " << header.sequenceNum() + << ". Resetting next expected sequence number to " + << (header.sequenceNum() + 1) << endl; + } + m_next_udp_rx_seq = header.sequenceNum() + 1; + + m_udp_heartbeat_rx_cnt = UDP_HEARTBEAT_RX_CNT_RESET; + + switch (header.type()) + { + case MsgUdpHeartbeat::TYPE: + break; + + case MsgUdpAudio::TYPE: + { + MsgUdpAudio msg; + if (!msg.unpack(ss)) + { + cerr << "*** WARNING[" << name() << "]: Could not unpack MsgUdpAudio\n"; + return; + } + if (!msg.audioData().empty()) + { + gettimeofday(&m_last_talker_timestamp, NULL); + m_dec->writeEncodedSamples( + &msg.audioData().front(), msg.audioData().size()); + } + break; + } + + case MsgUdpFlushSamples::TYPE: + m_dec->flushEncodedSamples(); + timerclear(&m_last_talker_timestamp); + break; + + case MsgUdpAllSamplesFlushed::TYPE: + m_enc->allEncodedSamplesFlushed(); + break; + + default: + // Better ignoring unknown protocol messages for easier addition of new + // messages while still being backwards compatible + + //cerr << "*** WARNING[" << name() + // << "]: Unknown UDP protocol message received: msg_type=" + // << header.type() << endl; + break; + } +} /* ReflectorLogic::udpDatagramReceived */ + + +void ReflectorLogic::sendUdpMsg(const ReflectorUdpMsg& msg) +{ + if (!isLoggedIn()) + { + return; + } + + m_udp_heartbeat_tx_cnt = m_udp_heartbeat_tx_cnt_reset; + + if (m_udp_sock == 0) + { + return; + } + + ReflectorUdpMsgV2 header(msg.type(), m_client_id, m_next_udp_tx_seq++); + ostringstream ss; + if (!header.pack(ss) || !msg.pack(ss)) + { + cerr << "*** ERROR[" << name() + << "]: Failed to pack reflector TCP message\n"; + return; + } + m_udp_sock->write(m_con.remoteHost(), m_con.remotePort(), + ss.str().data(), ss.str().size()); +} /* ReflectorLogic::sendUdpMsg */ + + +void ReflectorLogic::connect(void) +{ + if (!isConnected()) + { + m_reconnect_timer.setEnable(false); + std::cout << name() << ": Connecting to service " << m_con.service() + << std::endl; + m_con.connect(); + } +} /* ReflectorLogic::connect */ + + +void ReflectorLogic::disconnect(void) +{ + bool was_connected = m_con.isConnected(); + m_con.disconnect(); + if (was_connected) + { + onDisconnected(&m_con, TcpConnection::DR_ORDERED_DISCONNECT); + } + m_con_state = STATE_DISCONNECTED; +} /* ReflectorLogic::disconnect */ + + +void ReflectorLogic::reconnect(void) +{ + disconnect(); + connect(); +} /* ReflectorLogic::reconnect */ + + +bool ReflectorLogic::isConnected(void) const +{ + return m_con.isConnected(); +} /* ReflectorLogic::isConnected */ + + +void ReflectorLogic::allEncodedSamplesFlushed(void) +{ + sendUdpMsg(MsgUdpAllSamplesFlushed()); +} /* ReflectorLogic::allEncodedSamplesFlushed */ + + +void ReflectorLogic::flushTimeout(Async::Timer *t) +{ + m_flush_timeout_timer.setEnable(false); + m_enc->allEncodedSamplesFlushed(); +} /* ReflectorLogic::flushTimeout */ + + +void ReflectorLogic::handleTimerTick(Async::Timer *t) +{ + if (timerisset(&m_last_talker_timestamp)) + { + struct timeval now, diff; + gettimeofday(&now, NULL); + timersub(&now, &m_last_talker_timestamp, &diff); + if (diff.tv_sec > 3) + { + cout << name() << ": Last talker audio timeout" << endl; + m_dec->flushEncodedSamples(); + timerclear(&m_last_talker_timestamp); + } + } + + if (--m_udp_heartbeat_tx_cnt == 0) + { + sendUdpMsg(MsgUdpHeartbeat()); + } + + if (--m_tcp_heartbeat_tx_cnt == 0) + { + sendMsg(MsgHeartbeat()); + } + + if (--m_udp_heartbeat_rx_cnt == 0) + { + cout << name() << ": UDP Heartbeat timeout" << endl; + disconnect(); + } + + if (--m_tcp_heartbeat_rx_cnt == 0) + { + cout << name() << ": Heartbeat timeout" << endl; + disconnect(); + } +} /* ReflectorLogic::handleTimerTick */ + + +bool ReflectorLogic::setAudioCodec(const std::string& codec_name) +{ + delete m_enc; + m_enc = Async::AudioEncoder::create(codec_name); + if (m_enc == 0) + { + cerr << "*** ERROR[" << name() + << "]: Failed to initialize " << codec_name + << " audio encoder" << endl; + m_enc = Async::AudioEncoder::create("DUMMY"); + assert(m_enc != 0); + return false; + } + m_enc->writeEncodedSamples.connect( + mem_fun(*this, &ReflectorLogic::sendEncodedAudio)); + m_enc->flushEncodedSamples.connect( + mem_fun(*this, &ReflectorLogic::flushEncodedAudio)); + m_enc_endpoint->registerSink(m_enc, false); + + string opt_prefix(m_enc->name()); + opt_prefix += "_ENC_"; + list names = cfg().listSection(name()); + for (list::const_iterator nit=names.begin(); nit!=names.end(); ++nit) + { + if ((*nit).find(opt_prefix) == 0) + { + string opt_value; + cfg().getValue(name(), *nit, opt_value); + string opt_name((*nit).substr(opt_prefix.size())); + m_enc->setOption(opt_name, opt_value); + } + } + m_enc->printCodecParams(); + + AudioSink *sink = 0; + if (m_dec != 0) + { + sink = m_dec->sink(); + m_dec->unregisterSink(); + delete m_dec; + } + m_dec = Async::AudioDecoder::create(codec_name); + if (m_dec == 0) + { + cerr << "*** ERROR[" << name() + << "]: Failed to initialize " << codec_name + << " audio decoder" << endl; + m_dec = Async::AudioDecoder::create("DUMMY"); + assert(m_dec != 0); + return false; + } + m_dec->allEncodedSamplesFlushed.connect( + mem_fun(*this, &ReflectorLogic::allEncodedSamplesFlushed)); + if (sink != 0) + { + m_dec->registerSink(sink, true); + } + + opt_prefix = string(m_dec->name()) + "_DEC_"; + names = cfg().listSection(name()); + for (list::const_iterator nit=names.begin(); nit!=names.end(); ++nit) + { + if ((*nit).find(opt_prefix) == 0) + { + string opt_value; + cfg().getValue(name(), *nit, opt_value); + string opt_name((*nit).substr(opt_prefix.size())); + m_dec->setOption(opt_name, opt_value); + } + } + m_dec->printCodecParams(); + + return true; +} /* ReflectorLogic::setAudioCodec */ + + +bool ReflectorLogic::codecIsAvailable(const std::string &codec_name) +{ + return AudioEncoder::isAvailable(codec_name) && + AudioDecoder::isAvailable(codec_name); +} /* ReflectorLogic::codecIsAvailable */ + + +void ReflectorLogic::onLogicConInStreamStateChanged(bool is_active, + bool is_idle) +{ + //cout << "### ReflectorLogic::onLogicConInStreamStateChanged: is_active=" + // << is_active << " is_idle=" << is_idle << endl; + if (is_idle) + { + if (m_qsy_pending_timer.isEnabled()) + { + std::ostringstream os; + os << "tg_qsy_on_sql " << m_last_qsy; + processEvent(os.str()); + selectTg(m_last_qsy, "", true); + m_qsy_pending_timer.setEnable(false); + m_tg_local_activity = true; + m_use_prio = false; + } + } + else + { + if ((m_logic_con_in_valve != 0) && m_tg_local_activity) + { + m_logic_con_in_valve->setOpen(true); + } + if (m_tg_select_timeout_cnt == 0) // No TG currently selected + { + if (m_default_tg > 0) + { + selectTg(m_default_tg, "tg_default_activation", !m_mute_first_tx_loc); + } + } + m_qsy_pending_timer.reset(); + m_tg_local_activity = true; + m_use_prio = false; + m_tg_select_timeout_cnt = m_tg_select_timeout; + } + + if (!m_tg_selection_event.empty()) + { + //processTgSelectionEvent(); + m_report_tg_timer.reset(); + m_report_tg_timer.setEnable(true); + } + + checkIdle(); +} /* ReflectorLogic::onLogicConInStreamStateChanged */ + + +void ReflectorLogic::onLogicConOutStreamStateChanged(bool is_active, + bool is_idle) +{ + //cout << "### ReflectorLogic::onLogicConOutStreamStateChanged: is_active=" + // << is_active << " is_idle=" << is_idle << endl; + if (!is_idle && (m_tg_select_timeout_cnt > 0)) + { + m_tg_select_timeout_cnt = m_tg_select_timeout; + } + + if (!m_tg_selection_event.empty()) + { + //processTgSelectionEvent(); + m_report_tg_timer.reset(); + m_report_tg_timer.setEnable(true); + } + + checkIdle(); +} /* ReflectorLogic::onLogicConOutStreamStateChanged */ + + +void ReflectorLogic::tgSelectTimerExpired(void) +{ + //cout << "### ReflectorLogic::tgSelectTimerExpired: m_tg_select_timeout_cnt=" + // << m_tg_select_timeout_cnt << endl; + if (m_tg_select_timeout_cnt > 0) + { + if (m_logic_con_out->isIdle() && m_logic_con_in->isIdle() && + (--m_tg_select_timeout_cnt == 0)) + { + selectTg(0, "tg_selection_timeout", false); + } + } +} /* ReflectorLogic::tgSelectTimerExpired */ + + +void ReflectorLogic::selectTg(uint32_t tg, const std::string& event, bool unmute) +{ + cout << name() << ": Selecting TG #" << tg << endl; + + m_tg_selection_event.clear(); + if (!event.empty()) + { + ostringstream os; + os << event << " " << tg << " " << m_selected_tg; + m_tg_selection_event = os.str(); + m_report_tg_timer.reset(); + m_report_tg_timer.setEnable(true); + } + + if (tg != m_selected_tg) + { + sendMsg(MsgSelectTG(tg)); + if (m_selected_tg != 0) + { + m_previous_tg = m_selected_tg; + } + m_selected_tg = tg; + if (tg == 0) + { + m_tg_local_activity = false; + m_use_prio = true; + } + else + { + m_tg_local_activity = !m_logic_con_in->isIdle(); + m_qsy_pending_timer.setEnable(false); + } + m_event_handler->setVariable(name() + "::selected_tg", m_selected_tg); + m_event_handler->setVariable(name() + "::previous_tg", m_previous_tg); + + ostringstream os; + os << "tg_selected " << m_selected_tg << " " << m_previous_tg; + processEvent(os.str()); + } + m_tg_select_timeout_cnt = (tg > 0) ? m_tg_select_timeout : 0; + + if (m_logic_con_in_valve != 0) + { + m_logic_con_in_valve->setOpen(unmute); + } +} /* ReflectorLogic::selectTg */ + + +void ReflectorLogic::processEvent(const std::string& event) +{ + m_event_handler->processEvent(name() + "::" + event); + checkIdle(); +} /* ReflectorLogic::processEvent */ + + +void ReflectorLogic::processTgSelectionEvent(void) +{ + if (!m_logic_con_out->isIdle() || !m_logic_con_in->isIdle() || + m_tg_selection_event.empty()) + { + return; + } + processEvent(m_tg_selection_event); + m_tg_selection_event.clear(); +} /* ReflectorLogic::processTgSelectionEvent */ + + +void ReflectorLogic::checkTmpMonitorTimeout(void) +{ + bool changed = false; + MonitorTgsSet::iterator it = m_monitor_tgs.begin(); + while (it != m_monitor_tgs.end()) + { + MonitorTgsSet::iterator next=it; + ++next; + const MonitorTgEntry& mte = *it; + if (mte.timeout > 0) + { + // NOTE: mte.timeout is mutable + if (--mte.timeout <= 0) + { + std::cout << name() << ": Temporary monitor timeout for TG #" + << mte.tg << std::endl; + changed = true; + m_monitor_tgs.erase(it); + std::ostringstream os; + os << "tmp_monitor_remove " << mte.tg; + processEvent(os.str()); + } + } + it = next; + } + if (changed) + { + sendMsg(MsgTgMonitor(std::set( + m_monitor_tgs.begin(), m_monitor_tgs.end()))); + } +} /* ReflectorLogic::checkTmpMonitorTimeout */ + + +void ReflectorLogic::qsyPendingTimeout(void) +{ + m_qsy_pending_timer.setEnable(false); + m_use_prio = true; + m_tg_select_timeout_cnt = 0; + cout << name() + << ": Server QSY request ignored due to no local activity" << endl; + std::ostringstream os; + os << "tg_qsy_ignored " << m_last_qsy; + processEvent(os.str()); +} /* ReflectorLogic::qsyPendingTimeout */ + + +bool ReflectorLogic::isIdle(void) +{ + return m_logic_con_out->isIdle() && m_logic_con_in->isIdle(); +} /* ReflectorLogic::isIdle */ + + +void ReflectorLogic::checkIdle(void) +{ + setIdle(isIdle()); +} /* ReflectorLogic::checkIdle */ + + +void ReflectorLogic::handlePlayFile(const std::string& path) +{ + setIdle(false); + LinkManager::instance()->playFile(this, path); +} /* ReflectorLogic::handlePlayFile */ + + +void ReflectorLogic::handlePlaySilence(int duration) +{ + setIdle(false); + LinkManager::instance()->playSilence(this, duration); +} /* ReflectorLogic::handlePlaySilence */ + + +void ReflectorLogic::handlePlayTone(int fq, int amp, int duration) +{ + setIdle(false); + LinkManager::instance()->playTone(this, fq, amp, duration); +} /* ReflectorLogic::handlePlayTone */ + + +void ReflectorLogic::handlePlayDtmf(const std::string& digit, int amp, + int duration) +{ + setIdle(false); + LinkManager::instance()->playDtmf(this, digit, amp, duration); +} /* ReflectorLogic::handlePlayDtmf */ + + +/* + * This file has not been truncated + */ diff --git a/src/svxlink/svxlink/ReflectorV2Logic.h b/src/svxlink/svxlink/ReflectorV2Logic.h new file mode 100644 index 000000000..6d4edabf1 --- /dev/null +++ b/src/svxlink/svxlink/ReflectorV2Logic.h @@ -0,0 +1,317 @@ +/** +@file ReflectorLogic.h +@brief A logic core that connect to the SvxReflector +@author Tobias Blomberg / SM0SVX +@date 2017-02-12 + +\verbatim +SvxLink - A Multi Purpose Voice Services System for Ham Radio Use +Copyright (C) 2003-2022 Tobias Blomberg / SM0SVX + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +\endverbatim +*/ + +#ifndef REFLECTOR_LOGIC_INCLUDED +#define REFLECTOR_LOGIC_INCLUDED + + +/**************************************************************************** + * + * System Includes + * + ****************************************************************************/ + +#include +#include +#include + + +/**************************************************************************** + * + * Project Includes + * + ****************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include + + +/**************************************************************************** + * + * Local Includes + * + ****************************************************************************/ + +#include "LogicBase.h" + + +/**************************************************************************** + * + * Forward declarations + * + ****************************************************************************/ + +namespace Async +{ + class UdpSocket; + class AudioValve; +}; + +class ReflectorMsg; +class ReflectorUdpMsg; +class EventHandler; + + +/**************************************************************************** + * + * Forward declarations of classes inside of the declared namespace + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Defines & typedefs + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Exported Global Variables + * + ****************************************************************************/ + + + +/**************************************************************************** + * + * Class definitions + * + ****************************************************************************/ + +/** +@brief A logic core that connect to the SvxReflector +@author Tobias Blomberg / SM0SVX +@date 2017-02-12 +*/ +class ReflectorLogic : public LogicBase +{ + public: + /** + * @brief Default constructor + */ + ReflectorLogic(void); + + /** + * @brief Initialize the logic core + * @param cfgobj A previously initialized configuration object + * @param plugin_name The name of the logic core + * @return Returns \em true on success or \em false on failure + */ + virtual bool initialize(Async::Config& cfgobj, + const std::string& logic_name) override; + + /** + * @brief Get the audio pipe sink used for writing audio into this logic + * @return Returns an audio pipe sink object + */ + virtual Async::AudioSink *logicConIn(void) { return m_logic_con_in; } + + /** + * @brief Get the audio pipe source used for reading audio from this logic + * @return Returns an audio pipe source object + */ + virtual Async::AudioSource *logicConOut(void) { return m_logic_con_out; } + + /** + * @brief A command has been received from another logic + * @param cmd The received command + * + * This function is typically called when a link activation command is + * issued to connect two or more logics together. + */ + virtual void remoteCmdReceived(LogicBase* src_logic, + const std::string& cmd); + + /** + * @brief A linked logic has updated its recieved talk group + * @param logic The pointer to the remote logic object + * @param tg The new received talk group + */ + virtual void remoteReceivedTgUpdated(LogicBase *logic, uint32_t tg); + + /** + * @brief A linked logic has published a state event + * @param logic The pointer to the remote logic object + * @param event_name The name of the event + * @param data The state update data + * + * This function is called when a linked logic has published a state update + * event message. A state update message is a free text message that can be + * used by subscribers to act on certain state changes within SvxLink. The + * event name must be unique within SvxLink. The recommended format is + * :, e.g. Rx:sql_state. + */ + virtual void remoteReceivedPublishStateEvent( + LogicBase *logic, const std::string& event_name, + const std::string& data); + + protected: + /** + * @brief Destructor + */ + virtual ~ReflectorLogic(void) override; + + private: + struct MonitorTgEntry + { + uint32_t tg; + uint8_t prio; + mutable int timeout; + MonitorTgEntry(uint32_t tg=0) : tg(tg), prio(0), timeout(0) {} + bool operator<(const MonitorTgEntry& mte) const { return tg < mte.tg; } + bool operator==(const MonitorTgEntry& mte) const { return tg == mte.tg; } + operator uint32_t(void) const { return tg; } + }; + + typedef enum + { + STATE_DISCONNECTED, STATE_EXPECT_AUTH_CHALLENGE, STATE_EXPECT_AUTH_OK, + STATE_EXPECT_SERVER_INFO, STATE_CONNECTED + } ConState; + + typedef Async::TcpPrioClient FramedTcpClient; + typedef std::set MonitorTgsSet; + + static const unsigned DEFAULT_UDP_HEARTBEAT_TX_CNT_RESET = 15; + static const unsigned UDP_HEARTBEAT_RX_CNT_RESET = 60; + static const unsigned TCP_HEARTBEAT_TX_CNT_RESET = 10; + static const unsigned TCP_HEARTBEAT_RX_CNT_RESET = 15; + static const unsigned DEFAULT_TG_SELECT_TIMEOUT = 30; + static const int DEFAULT_TMP_MONITOR_TIMEOUT = 3600; + + std::string m_reflector_host; + FramedTcpClient m_con; + unsigned m_msg_type; + Async::UdpSocket* m_udp_sock; + uint32_t m_client_id; + std::string m_auth_key; + std::string m_callsign; + Async::AudioStreamStateDetector* m_logic_con_in; + Async::AudioStreamStateDetector* m_logic_con_out; + Async::Timer m_reconnect_timer; + uint16_t m_next_udp_tx_seq; + uint16_t m_next_udp_rx_seq; + Async::Timer m_heartbeat_timer; + Async::AudioDecoder* m_dec; + Async::Timer m_flush_timeout_timer; + unsigned m_udp_heartbeat_tx_cnt_reset; + unsigned m_udp_heartbeat_tx_cnt; + unsigned m_udp_heartbeat_rx_cnt; + unsigned m_tcp_heartbeat_tx_cnt; + unsigned m_tcp_heartbeat_rx_cnt; + struct timeval m_last_talker_timestamp; + ConState m_con_state; + Async::AudioEncoder* m_enc; + uint32_t m_default_tg; + unsigned m_tg_select_timeout; + Async::Timer m_tg_select_timer; + unsigned m_tg_select_timeout_cnt; + uint32_t m_selected_tg; + uint32_t m_previous_tg; + EventHandler* m_event_handler; + Async::Timer m_report_tg_timer; + std::string m_tg_selection_event; + bool m_tg_local_activity; + uint32_t m_last_qsy; + MonitorTgsSet m_monitor_tgs; + Json::Value m_node_info; + Async::AudioSource* m_enc_endpoint; + Async::AudioValve* m_logic_con_in_valve; + bool m_mute_first_tx_loc; + bool m_mute_first_tx_rem; + Async::Timer m_tmp_monitor_timer; + int m_tmp_monitor_timeout; + bool m_use_prio; + Async::Timer m_qsy_pending_timer; + bool m_verbose; + + ReflectorLogic(const ReflectorLogic&); + ReflectorLogic& operator=(const ReflectorLogic&); + void onConnected(void); + void onDisconnected(Async::TcpConnection *con, + Async::TcpConnection::DisconnectReason reason); + void onFrameReceived(Async::FramedTcpConnection *con, + std::vector& data); + void handleMsgError(std::istream& is); + void handleMsgProtoVerDowngrade(std::istream& is); + void handleMsgAuthChallenge(std::istream& is); + void handleMsgNodeList(std::istream& is); + void handleMsgNodeJoined(std::istream& is); + void handleMsgNodeLeft(std::istream& is); + void handleMsgTalkerStart(std::istream& is); + void handleMsgTalkerStop(std::istream& is); + void handleMsgRequestQsy(std::istream& is); + void handleMsgAuthOk(void); + void handleMsgServerInfo(std::istream& is); + void sendMsg(const ReflectorMsg& msg); + void sendEncodedAudio(const void *buf, int count); + void flushEncodedAudio(void); + void udpDatagramReceived(const Async::IpAddress& addr, uint16_t port, + void *buf, int count); + void sendUdpMsg(const ReflectorUdpMsg& msg); + void connect(void); + void disconnect(void); + void reconnect(void); + bool isConnected(void) const; + bool isLoggedIn(void) const { return m_con_state == STATE_CONNECTED; } + void allEncodedSamplesFlushed(void); + void flushTimeout(Async::Timer *t=0); + void handleTimerTick(Async::Timer *t); + bool setAudioCodec(const std::string& codec_name); + bool codecIsAvailable(const std::string &codec_name); + void tgSelectTimerExpired(void); + void onLogicConInStreamStateChanged(bool is_active, bool is_idle); + void onLogicConOutStreamStateChanged(bool is_active, bool is_idle); + void selectTg(uint32_t tg, const std::string& event, bool unmute); + void processEvent(const std::string& event); + void processTgSelectionEvent(void); + void checkTmpMonitorTimeout(void); + void qsyPendingTimeout(void); + void checkIdle(void); + bool isIdle(void); + void handlePlayFile(const std::string& path); + void handlePlaySilence(int duration); + void handlePlayTone(int fq, int amp, int duration); + void handlePlayDtmf(const std::string& digit, int amp, int duration); + +}; /* class ReflectorLogic */ + + +#endif /* REFLECTOR_LOGIC_INCLUDED */ + + +/* + * This file has not been truncated + */ diff --git a/src/svxlink/svxlink/svxlink.conf.in b/src/svxlink/svxlink/svxlink.conf.in index 98929f518..8f7326152 100644 --- a/src/svxlink/svxlink/svxlink.conf.in +++ b/src/svxlink/svxlink/svxlink.conf.in @@ -96,7 +96,10 @@ HOSTS=reflector-primary.example.org,reflector-secondary.example.org:5301 #HOST_PRIO_INC=1 #HOST_WEIGHT=10 CALLSIGN="MYCALL" -AUTH_KEY="Change this key now!" +#CERT_KEYFILE=@SVX_SYSCONF_INSTALL_DIR@/pki/MYCALL.key +#CERT_CRTFILE=@SVX_SYSCONF_INSTALL_DIR@/pki/MYCALL.crt +#CERT_CAFILE=@SVX_SYSCONF_INSTALL_DIR@/pki/ca.pem +#AUTH_KEY="Change this key now!" #JITTER_BUFFER_DELAY=0 #DEFAULT_TG=999 #MONITOR_TGS=99901,99902,99903 @@ -112,6 +115,19 @@ EVENT_HANDLER=@SVX_SHARE_INSTALL_DIR@/events.tcl QSY_PENDING_TIMEOUT=15 #DEFAULT_LANG=en_US #VERBOSE=1 +#CERT_PKI_DIR="@SVX_LOCAL_STATE_DIR@/pki" +#CERT_KEYFILE=@SVX_LOCAL_STATE_DIR@/pki/MYCALL.key +#CERT_CSRFILE=@SVX_LOCAL_STATE_DIR@/pki/MYCALL.csr +#CERT_CRTFILE=@SVX_LOCAL_STATE_DIR@/pki/MYCALL.crt +#CERT_CAFILE=@SVX_LOCAL_STATE_DIR@/pki/ca-bundle.pem +#CERT_SUBJ_givenName=John +#CERT_SUBJ_surname=Doe +#CERT_SUBJ_organizationalUnitName=SvxLink +#CERT_SUBJ_organizationName=SSA +#CERT_SUBJ_localityName=Stockholm +#CERT_SUBJ_stateOrProvinceName=Södermanland +#CERT_SUBJ_countryName=SE +#CERT_EMAIL=mycall@example.com [LinkToR4] CONNECT_LOGICS=RepeaterLogic:94:SK3AB,SimplexLogic:92:SK3CD diff --git a/src/template.h b/src/template.h index ed6ccb748..9f729fb51 100644 --- a/src/template.h +++ b/src/template.h @@ -27,7 +27,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /** @example MyNamespaceTemplate_demo.cpp -An example of how to use the Template class +An example of how to use the MyNamespace::Template class */ #ifndef TEMPLATE_INCLUDED diff --git a/src/valgrind.supp b/src/valgrind.supp index ef40d79fb..eb80c2fcd 100644 --- a/src/valgrind.supp +++ b/src/valgrind.supp @@ -9,6 +9,15 @@ fun:_ZN10TrxHandler10initializeEv fun:main } +{ + gcry_check_version + Memcheck:Leak + match-leak-kinds: reachable + fun:malloc + ... + fun:gcry_check_version + ... +} { dl_init Memcheck:Leak diff --git a/src/versions b/src/versions index 61cc34503..f26ec7573 100644 --- a/src/versions +++ b/src/versions @@ -8,10 +8,10 @@ QTEL=1.2.5 LIBECHOLIB=1.3.4 # Version for the Async library -LIBASYNC=1.7.0.99.0 +LIBASYNC=1.7.99.0 # SvxLink versions -SVXLINK=1.8.0 +SVXLINK=1.8.99.0 MODULE_HELP=1.0.0 MODULE_PARROT=1.1.1 MODULE_ECHO_LINK=1.6.0 @@ -37,4 +37,4 @@ DEVCAL=1.0.3 SVXSERVER=0.0.6 # Version for SvxReflector -SVXREFLECTOR=1.2.99.0 +SVXREFLECTOR=1.2.99.1