diff --git a/CMakeLists.txt b/CMakeLists.txt index 86046f84..ad8df43c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,15 +51,19 @@ set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE amd64) if (CROSS_COMPILE_ARM) set(CMAKE_C_COMPILER /usr/bin/arm-linux-gnueabihf-gcc) set(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabihf-g++) - set(ARCH arm) - set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE arm) + set(ARCH armhf) + set(CMAKE_SYSTEM_PROCESSOR armhf) + set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE armhf) + set(OPENSSL_ROOT_DIR /usr/lib/arm-linux-gnueabihf) message(CHECK_START "Cross compiling for 32-bit ARM - ${CMAKE_C_COMPILER}") endif (CROSS_COMPILE_ARM) if (CROSS_COMPILE_AARCH64) set(CMAKE_C_COMPILER /usr/bin/aarch64-linux-gnu-gcc) set(CMAKE_CXX_COMPILER /usr/bin/aarch64-linux-gnu-g++) set(ARCH arm64) + set(CMAKE_SYSTEM_PROCESSOR arm64) set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE arm64) + set(OPENSSL_ROOT_DIR /usr/lib/aarch64-linux-gnu) message(CHECK_START "Cross compiling for 64-bit ARM - ${CMAKE_C_COMPILER}") endif (CROSS_COMPILE_AARCH64) @@ -78,14 +82,25 @@ if (CROSS_COMPILE_RPI_ARM) GIT_REPOSITORY https://github.com/raspberrypi/tools.git ) FetchContent_MakeAvailable(RPiTools) - set(CMAKE_C_COMPILER ${CMAKE_CURRENT_BINARY_DIR}/_deps/rpitools-src/arm-bcm2708/arm-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc) - set(CMAKE_CXX_COMPILER ${CMAKE_CURRENT_BINARY_DIR}/_deps/rpitools-src/arm-bcm2708/arm-linux-gnueabihf/bin/arm-linux-gnueabihf-g++) + set(CMAKE_C_COMPILER ${CMAKE_CURRENT_BINARY_DIR}/_deps/rpitools-src/arm-bcm2708/arm-rpi-4.9.3-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc) + set(CMAKE_CXX_COMPILER ${CMAKE_CURRENT_BINARY_DIR}/_deps/rpitools-src/arm-bcm2708/arm-rpi-4.9.3-linux-gnueabihf/bin/arm-linux-gnueabihf-g++) + + message(CHECK_START "Apply OpenSSL library binaries for cross compling (old RPi) 32-bit ARM - ${CMAKE_CURRENT_BINARY_DIR}/_deps/rpitools-src") + execute_process(COMMAND tar xzf contrib/openssl_cross_patch.RPI_ARM.tar.gz -C ${CMAKE_CURRENT_BINARY_DIR}/_deps/rpitools-src WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}) + + set(OPENSSL_ROOT_DIR ${CMAKE_CURRENT_BINARY_DIR}/_deps/rpitools-src/arm-bcm2708/arm-rpi-4.9.3-linux-gnueabihf/arm-linux-gnueabihf/sysroot/usr/lib) else() - set(CMAKE_C_COMPILER ${RPI_ARM_TOOLS}/arm-bcm2708/arm-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc) - set(CMAKE_CXX_COMPILER ${RPI_ARM_TOOLS}/arm-bcm2708/arm-linux-gnueabihf/bin/arm-linux-gnueabihf-g++) + set(CMAKE_C_COMPILER ${RPI_ARM_TOOLS}/arm-bcm2708/arm-rpi-4.9.3-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc) + set(CMAKE_CXX_COMPILER ${RPI_ARM_TOOLS}/arm-bcm2708/arm-rpi-4.9.3-linux-gnueabihf/bin/arm-linux-gnueabihf-g++) + + message(CHECK_START "Apply OpenSSL library binaries for cross compling (old RPi) 32-bit ARM - ${RPI_ARM_TOOLS}") + execute_process(COMMAND tar xzf contrib/openssl_cross_patch.RPI_ARM.tar.gz -C ${RPI_ARM_TOOLS} WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}) + + set(OPENSSL_ROOT_DIR ${RPI_ARM_TOOLS}/arm-bcm2708/arm-rpi-4.9.3-linux-gnueabihf/arm-linux-gnueabihf/sysroot/usr/lib) endif () set(ARCH armhf) + set(CMAKE_SYSTEM_PROCESSOR armhf) set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE armhf) message(CHECK_START "Cross compiling for (old RPi) 32-bit ARM - ${CMAKE_C_COMPILER}") @@ -94,6 +109,13 @@ if (CROSS_COMPILE_RPI_ARM) message(CHECK_START "Enable TUI support - no; for simplicity RPI_ARM cross-compiling does not support TUI.") endif (CROSS_COMPILE_RPI_ARM) +# search for programs in the build host directories +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + +# for libraries and headers in the target directories +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) + # Standard CMake options set(THREADS_PREFER_PTHREAD_FLAG ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) @@ -163,11 +185,6 @@ if (ENABLE_TUI_SUPPORT AND NOT FC_INCLUDED) set(FINALCUT_INCLUDE_DIR ${CMAKE_CURRENT_BINARY_DIR}/_deps/finalcut-src/src) endif (ENABLE_TUI_SUPPORT AND NOT FC_INCLUDED) -if (ENABLE_TCP_SSL) - find_package(OpenSSL REQUIRED) - include_directories("${OPENSSL_INCLUDE_DIR}") -endif (ENABLE_TCP_SSL) - # # Set GIT_VER compiler directive # diff --git a/README.md b/README.md index 30764ac3..28fc5fa4 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,11 @@ The DVM Host software requires the library dependancies below. Generally, the so ### Dependencies -`apt-get install libasio-dev libncurses-dev` +`apt-get install libasio-dev libncurses-dev libssl-dev` - ASIO Library (https://think-async.com/Asio/); on Debian/Ubuntu Linux's: `apt-get install libasio-dev` - ncurses; on Debian/Ubuntu Linux's: `apt-get install libncurses-dev` +- OpenSSL; on Debian/Ubuntu Linux's: `apt-get install libssl-dev` Alternatively, if you download the ASIO library from the ASIO website and extract it to a location, you can specify the path to the ASIO library using: `-DWITH_ASIO=/path/to/asio`. This method is required when cross-compiling for old Raspberry Pi ARM 32 bit. @@ -31,7 +32,7 @@ If cross-compiling ensure you install the appropriate libraries, for example for ``` sudo dpkg --add-architecture arm64 sudo apt-get update -sudo apt-get install libasio-dev:arm64 libncurses-dev:arm64 +sudo apt-get install libasio-dev:arm64 libncurses-dev:arm64 libssl-dev:arm64 ``` ### Build Instructions diff --git a/configs/config.example.yml b/configs/config.example.yml index 8a2adecb..fe838c43 100644 --- a/configs/config.example.yml +++ b/configs/config.example.yml @@ -76,6 +76,12 @@ network: restAddress: 127.0.0.1 # Port number for REST API to listen on. restPort: 9990 + # Flag indicating whether or not REST API is operating in SSL mode. + restSsl: false + # HTTPS/TLS certificate. + restSslCertificate: web.crt + # HTTPS/TLS key file. + restSslKey: web.key # REST API authentication password. restPassword: "PASSWORD" # Flag indicating whether or not verbose REST API debug logging is enabled. @@ -359,6 +365,8 @@ system: restPort: 0 # REST API access password for control channel. restPassword: "PASSWORD" + # Flag indicating whether or not REST API is operating in SSL mode. + restSsl: false # Flag indicating voice channels will notify the control channel of traffic status. notifyEnable: true @@ -376,6 +384,8 @@ system: restPort: 9990 # REST API access password for voice channel. restPassword: "PASSWORD" + # Flag indicating whether or not REST API is operating in SSL mode. + restSsl: false secure: # AES-128 16-byte Key (used for LLA) diff --git a/configs/fne-config.example.yml b/configs/fne-config.example.yml index 9549b4c5..69693952 100644 --- a/configs/fne-config.example.yml +++ b/configs/fne-config.example.yml @@ -161,6 +161,12 @@ system: restAddress: 127.0.0.1 # Port number for REST API to listen on. restPort: 9990 + # Flag indicating whether or not REST API is operating in SSL mode. + restSsl: false + # HTTPS/TLS certificate. + restSslCertificate: web.crt + # HTTPS/TLS key file. + restSslKey: web.key # REST API authentication password. restPassword: "PASSWORD" # Flag indicating whether or not verbose REST API debug logging is enabled. diff --git a/configs/monitor-config.example.yml b/configs/monitor-config.example.yml index 78452b5f..3dbe63fd 100644 --- a/configs/monitor-config.example.yml +++ b/configs/monitor-config.example.yml @@ -22,4 +22,6 @@ channels: # REST API Port number for channel. restPort: 9990 # REST API access password for channel. - restPassword: "PASSWORD" \ No newline at end of file + restPassword: "PASSWORD" + # Flag indicating whether or not REST API is operating in SSL mode. + restSsl: false diff --git a/contrib/openssl_cross_patch.RPI_ARM.tar.gz b/contrib/openssl_cross_patch.RPI_ARM.tar.gz new file mode 100644 index 00000000..29c5aac5 Binary files /dev/null and b/contrib/openssl_cross_patch.RPI_ARM.tar.gz differ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 257f59cb..fb50d73c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -31,11 +31,11 @@ endif (ENABLE_SETUP_TUI) add_executable(dvmhost ${common_INCLUDE} ${dvmhost_SRC}) if (ENABLE_SETUP_TUI) - target_link_libraries(dvmhost PRIVATE common asio::asio finalcut Threads::Threads util) + target_link_libraries(dvmhost PRIVATE common ${OPENSSL_LIBRARIES} asio::asio finalcut Threads::Threads util) else() - target_link_libraries(dvmhost PRIVATE common asio::asio Threads::Threads util) + target_link_libraries(dvmhost PRIVATE common ${OPENSSL_LIBRARIES} asio::asio Threads::Threads util) endif (ENABLE_SETUP_TUI) -target_include_directories(dvmhost PRIVATE src src/host) +target_include_directories(dvmhost PRIVATE ${OPENSSL_INCLUDE_DIR} src src/host) set(CPACK_SET_DESTDIR true) set(CPACK_PACKAGING_INSTALL_PREFIX "/usr/local") @@ -64,8 +64,8 @@ include(CPack) # include(src/fne/CMakeLists.txt) add_executable(dvmfne ${common_INCLUDE} ${dvmfne_SRC}) -target_link_libraries(dvmfne PRIVATE common asio::asio Threads::Threads) -target_include_directories(dvmfne PRIVATE src src/host src/fne) +target_link_libraries(dvmfne PRIVATE common ${OPENSSL_LIBRARIES} asio::asio Threads::Threads) +target_include_directories(dvmfne PRIVATE ${OPENSSL_INCLUDE_DIR} src src/host src/fne) # ## dvmmon @@ -73,8 +73,8 @@ target_include_directories(dvmfne PRIVATE src src/host src/fne) if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_MONITOR)) include(src/monitor/CMakeLists.txt) add_executable(dvmmon ${common_INCLUDE} ${dvmmon_SRC}) - target_link_libraries(dvmmon PRIVATE common asio::asio finalcut Threads::Threads) - target_include_directories(dvmmon PRIVATE src src/host src/monitor) + target_link_libraries(dvmmon PRIVATE common ${OPENSSL_LIBRARIES} asio::asio finalcut Threads::Threads) + target_include_directories(dvmmon PRIVATE ${OPENSSL_INCLUDE_DIR} src src/host src/monitor) endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_MONITOR)) # @@ -82,5 +82,5 @@ endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_MONITOR)) # include(src/remote/CMakeLists.txt) add_executable(dvmcmd ${common_INCLUDE} ${dvmcmd_SRC}) -target_link_libraries(dvmcmd PRIVATE common asio::asio Threads::Threads) -target_include_directories(dvmcmd PRIVATE src src/remote) +target_link_libraries(dvmcmd PRIVATE common ${OPENSSL_LIBRARIES} asio::asio Threads::Threads) +target_include_directories(dvmcmd PRIVATE ${OPENSSL_INCLUDE_DIR} src src/remote) diff --git a/src/CompilerOptions.cmake b/src/CompilerOptions.cmake index dadd9c42..dde00d88 100644 --- a/src/CompilerOptions.cmake +++ b/src/CompilerOptions.cmake @@ -173,3 +173,9 @@ if (HAVE_SENDMMSG) set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -DHAVE_SENDMMSG=1") set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -DHAVE_SENDMMSG=1") endif (HAVE_SENDMMSG) + +# are we enabling SSL support? +if (ENABLE_TCP_SSL) + find_package(OpenSSL REQUIRED) + include_directories("${OPENSSL_INCLUDE_DIR}") +endif (ENABLE_TCP_SSL) diff --git a/src/common/lookups/AffiliationLookup.h b/src/common/lookups/AffiliationLookup.h index f52da52f..bb547443 100644 --- a/src/common/lookups/AffiliationLookup.h +++ b/src/common/lookups/AffiliationLookup.h @@ -7,7 +7,7 @@ * @package DVM / Common Library * @license GPLv2 License (https://opensource.org/licenses/GPL-2.0) * -* Copyright (C) 2022 Bryan Biedenkapp, N2PLL +* Copyright (C) 2022,2024 Bryan Biedenkapp, N2PLL * */ #if !defined(__AFFILIATION_LOOKUP_H__) @@ -37,7 +37,8 @@ namespace lookups m_chNo(0U), m_address(), m_port(), - m_password() + m_password(), + m_ssl() { /* stub */ } @@ -47,12 +48,14 @@ namespace lookups /// REST API Address. /// REST API Port. /// REST API Password. - VoiceChData(uint8_t chId, uint32_t chNo, std::string address, uint16_t port, std::string password) : + /// Flag indicating REST is using SSL. + VoiceChData(uint8_t chId, uint32_t chNo, std::string address, uint16_t port, std::string password, bool ssl) : m_chId(chId), m_chNo(chNo), m_address(address), m_port(port), - m_password(password) + m_password(password), + m_ssl(ssl) { /* stub */ } @@ -68,6 +71,7 @@ namespace lookups m_address = data.m_address; m_port = data.m_port; m_password = data.m_password; + m_ssl = data.m_ssl; } return *this; @@ -89,6 +93,8 @@ namespace lookups __READONLY_PROPERTY_PLAIN(uint16_t, port); /// REST API Password. __READONLY_PROPERTY_PLAIN(std::string, password); + /// Flag indicating REST is using SSL. + __READONLY_PROPERTY_PLAIN(bool, ssl); }; // --------------------------------------------------------------------------- diff --git a/src/common/network/rest/http/HTTPServer.h b/src/common/network/rest/http/HTTPServer.h index fccd1bba..825c05ee 100644 --- a/src/common/network/rest/http/HTTPServer.h +++ b/src/common/network/rest/http/HTTPServer.h @@ -9,7 +9,7 @@ * @license BSL-1.0 License (https://opensource.org/license/bsl1-0-html) * * Copyright (c) 2003-2013 Christopher M. Kohlhoff -* Copyright (C) 2023 Bryan Biedenkapp, N2PLL +* Copyright (C) 2023,2024 Bryan Biedenkapp, N2PLL * */ #if !defined(__REST_HTTP__HTTP_SERVER_H__) @@ -57,15 +57,7 @@ namespace network { // open the acceptor with the option to reuse the address (i.e. SO_REUSEADDR) asio::ip::address ipAddress = asio::ip::address::from_string(address); - asio::ip::tcp::endpoint endpoint(ipAddress, port); - - m_acceptor.open(endpoint.protocol()); - m_acceptor.set_option(asio::ip::tcp::acceptor::reuse_address(true)); - m_acceptor.set_option(asio::socket_base::keep_alive(true)); - m_acceptor.bind(endpoint); - m_acceptor.listen(); - - accept(); + m_endpoint = asio::ip::tcp::endpoint(ipAddress, port); } /// Helper to set the HTTP request handlers. @@ -75,6 +67,19 @@ namespace network m_requestHandler = RequestHandlerType(std::forward(handler)); } + /// Open TCP acceptor. + void open() + { + // open the acceptor with the option to reuse the address (i.e. SO_REUSEADDR) + m_acceptor.open(m_endpoint.protocol()); + m_acceptor.set_option(asio::ip::tcp::acceptor::reuse_address(true)); + m_acceptor.set_option(asio::socket_base::keep_alive(true)); + m_acceptor.bind(m_endpoint); + m_acceptor.listen(); + + accept(); + } + /// Run the servers ASIO IO service loop. void run() { @@ -120,6 +125,8 @@ namespace network asio::io_service m_ioService; asio::ip::tcp::acceptor m_acceptor; + asio::ip::tcp::endpoint m_endpoint; + ServerConnectionManager m_connectionManager; asio::ip::tcp::socket m_socket; diff --git a/src/common/network/rest/http/SecureClientConnection.h b/src/common/network/rest/http/SecureClientConnection.h new file mode 100644 index 00000000..f930e597 --- /dev/null +++ b/src/common/network/rest/http/SecureClientConnection.h @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: GPL-2.0-only +/** +* Digital Voice Modem - Common Library +* GPLv2 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Common Library +* @license GPLv2 License (https://opensource.org/licenses/GPL-2.0) +* +* Copyright (C) 2024 Bryan Biedenkapp, N2PLL +* +*/ +#if !defined(__REST_HTTP__SECURE_CLIENT_CONNECTION_H__) +#define __REST_HTTP__SECURE_CLIENT_CONNECTION_H__ + +#if defined(ENABLE_TCP_SSL) + +#include "common/Defines.h" +#include "common/network/rest/http/HTTPLexer.h" +#include "common/network/rest/http/HTTPPayload.h" +#include "common/Log.h" + +#include +#include +#include +#include +#include +#include + +namespace network +{ + namespace rest + { + namespace http + { + // --------------------------------------------------------------------------- + // Class Declaration + // This class represents a single connection from a client. + // --------------------------------------------------------------------------- + + template + class SecureClientConnection { + public: + auto operator=(SecureClientConnection&) -> SecureClientConnection& = delete; + auto operator=(SecureClientConnection&&) -> SecureClientConnection& = delete; + SecureClientConnection(SecureClientConnection&) = delete; + + /// Initializes a new instance of the SecureClientConnection class. + explicit SecureClientConnection(asio::ip::tcp::socket socket, asio::ssl::context& context, RequestHandlerType& handler) : + m_socket(std::move(socket), context), + m_requestHandler(handler), + m_lexer(HTTPLexer(true)) + { + m_socket.set_verify_mode(asio::ssl::verify_none); + m_socket.set_verify_callback(std::bind(&SecureClientConnection::verify_certificate, this, std::placeholders::_1, std::placeholders::_2)); + } + + /// Start the first asynchronous operation for the connection. + void start() + { + m_socket.handshake(asio::ssl::stream_base::client); + read(); + } + /// Stop all asynchronous operations associated with the connection. + void stop() + { + try + { + ensureNoLinger(); + if (m_socket.lowest_layer().is_open()) { + m_socket.lowest_layer().close(); + } + } + catch(const std::exception&) { /* ignore */ } + } + + /// Helper to enable the SO_LINGER socket option during shutdown. + void ensureNoLinger() + { + try + { + // enable SO_LINGER timeout 0 + asio::socket_base::linger linger(true, 0); + m_socket.lowest_layer().set_option(linger); + } + catch(const asio::system_error& e) + { + asio::error_code ec = e.code(); + if (ec) { + ::LogError(LOG_REST, "SecureClientConnection::ensureNoLinger(), %s, code = %u", ec.message().c_str(), ec.value()); + } + } + } + + /// Perform an synchronous write operation. + void send(HTTPPayload request) + { + request.attachHostHeader(m_socket.lowest_layer().remote_endpoint()); + write(request); + } + private: + /// Perform an SSL certificate verification. + bool verify_certificate(bool preverified, asio::ssl::verify_context& context) + { + return true; // ignore always valid + } + + /// Perform an asynchronous read operation. + void read() + { + m_socket.async_read_some(asio::buffer(m_buffer), [=](asio::error_code ec, std::size_t bytes_transferred) { + if (!ec) { + HTTPLexer::ResultType result; + char* content; + + std::tie(result, content) = m_lexer.parse(m_request, m_buffer.data(), m_buffer.data() + bytes_transferred); + + std::string contentLength = m_request.headers.find("Content-Length"); + if (contentLength != "") { + size_t length = (size_t)::strtoul(contentLength.c_str(), NULL, 10); + m_request.content = std::string(content, length); + } + + m_request.headers.add("RemoteHost", m_socket.lowest_layer().remote_endpoint().address().to_string()); + + if (result == HTTPLexer::GOOD) { + m_requestHandler.handleRequest(m_request, m_reply); + } + else if (result == HTTPLexer::BAD) { + return; + } + else { + read(); + } + } + else if (ec != asio::error::operation_aborted) { + if (ec) { + ::LogError(LOG_REST, "SecureClientConnection::read(), %s, code = %u", ec.message().c_str(), ec.value()); + } + stop(); + } + }); + } + + /// Perform an synchronous write operation. + void write(HTTPPayload request) + { + try + { + m_socket.handshake(asio::ssl::stream_base::client); + + auto buffers = request.toBuffers(); + asio::write(m_socket, buffers); + } + catch(const asio::system_error& e) + { + asio::error_code ec = e.code(); + if (ec) { + ::LogError(LOG_REST, "SecureClientConnection::write(), %s, code = %u", ec.message().c_str(), ec.value()); + + try + { + // initiate graceful connection closure + asio::error_code ignored_ec; + m_socket.lowest_layer().shutdown(asio::ip::tcp::socket::shutdown_both, ignored_ec); + } + catch(const std::exception& e) { + ::LogError(LOG_REST, "SecureClientConnection::write(), %s, code = %u", ec.message().c_str(), ec.value()); + } + } + } + } + + asio::ssl::stream m_socket; + + RequestHandlerType& m_requestHandler; + + std::array m_buffer; + + HTTPPayload m_request; + HTTPLexer m_lexer; + HTTPPayload m_reply; + }; + } // namespace http + } // namespace rest +} // namespace network + +#endif // ENABLE_TCP_SSL + +#endif // __REST_HTTP__SECURE_CLIENT_CONNECTION_H__ diff --git a/src/common/network/rest/http/SecureHTTPClient.h b/src/common/network/rest/http/SecureHTTPClient.h new file mode 100644 index 00000000..41216e8c --- /dev/null +++ b/src/common/network/rest/http/SecureHTTPClient.h @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: GPL-2.0-only +/** +* Digital Voice Modem - Common Library +* GPLv2 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Common Library +* @license GPLv2 License (https://opensource.org/licenses/GPL-2.0) +* +* Copyright (C) 2024 Bryan Biedenkapp, N2PLL +* +*/ +#if !defined(__REST_HTTP__SECURE_HTTP_CLIENT_H__) +#define __REST_HTTP__SECURE_HTTP_CLIENT_H__ + +#if defined(ENABLE_TCP_SSL) + +#include "common/Defines.h" +#include "common/network/rest/http/SecureClientConnection.h" +#include "common/network/rest/http/HTTPRequestHandler.h" +#include "common/Thread.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace network +{ + namespace rest + { + namespace http + { + + // --------------------------------------------------------------------------- + // Class Declaration + // This class implements top-level routines of the secure HTTP client. + // --------------------------------------------------------------------------- + + template class ConnectionImpl = SecureClientConnection> + class SecureHTTPClient : private Thread { + public: + auto operator=(SecureHTTPClient&) -> SecureHTTPClient& = delete; + auto operator=(SecureHTTPClient&&) -> SecureHTTPClient& = delete; + SecureHTTPClient(SecureHTTPClient&) = delete; + + /// Initializes a new instance of the SecureHTTPClient class. + SecureHTTPClient(const std::string& address, uint16_t port) : + m_address(address), + m_port(port), + m_connection(nullptr), + m_ioContext(), + m_context(asio::ssl::context::tlsv12), + m_socket(m_ioContext), + m_requestHandler() + { + /* stub */ + } + /// Finalizes a instance of the SecureHTTPClient class. + ~SecureHTTPClient() override + { + if (m_connection != nullptr) { + close(); + } + } + + /// Helper to set the HTTP request handlers. + template + void setHandler(Handler&& handler) + { + m_requestHandler = RequestHandlerType(std::forward(handler)); + } + + /// Send HTTP request to HTTP server. + bool request(HTTPPayload& request) + { + if (m_completed) { + return false; + } + + asio::post(m_ioContext, [this, request]() { + std::lock_guard guard(m_lock); + { + if (m_connection != nullptr) { + m_connection->send(request); + } + } + }); + + return true; + } + + /// Opens connection to the network. + bool open() + { + if (m_completed) { + return false; + } + + return run(); + } + + /// Closes connection to the network. + void close() + { + if (m_completed) { + return; + } + + m_completed = true; + m_ioContext.stop(); + + wait(); + } + + private: + /// + void entry() override + { + if (m_completed) { + return; + } + + asio::ip::tcp::resolver resolver(m_ioContext); + auto endpoints = resolver.resolve(m_address, std::to_string(m_port)); + + try { + connect(endpoints); + + // the entry() call will block until all asynchronous operations + // have finished + m_ioContext.run(); + } + catch (std::exception&) { /* stub */ } + + if (m_connection != nullptr) { + m_connection->stop(); + } + } + + /// Perform an asynchronous connect operation. + void connect(asio::ip::basic_resolver_results& endpoints) + { + asio::connect(m_socket, endpoints); + + m_connection = std::make_unique(std::move(m_socket), m_context, m_requestHandler); + m_connection->start(); + } + + std::string m_address; + uint16_t m_port; + + typedef ConnectionImpl ConnectionType; + + std::unique_ptr m_connection; + + bool m_completed = false; + asio::io_context m_ioContext; + + asio::ssl::context m_context; + asio::ip::tcp::socket m_socket; + + RequestHandlerType m_requestHandler; + + std::mutex m_lock; + }; + } // namespace http + } // namespace rest +} // namespace network + +#endif // ENABLE_TCP_SSL + +#endif // __REST_HTTP__SECURE_HTTP_CLIENT_H__ diff --git a/src/common/network/rest/http/SecureHTTPServer.h b/src/common/network/rest/http/SecureHTTPServer.h new file mode 100644 index 00000000..218f7b05 --- /dev/null +++ b/src/common/network/rest/http/SecureHTTPServer.h @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: BSL-1.0 +/** +* Digital Voice Modem - Common Library +* BSL-1.0 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Common Library +* @derivedfrom CRUD (https://github.com/venediktov/CRUD) +* @license BSL-1.0 License (https://opensource.org/license/bsl1-0-html) +* +* Copyright (c) 2003-2013 Christopher M. Kohlhoff +* Copyright (C) 2024 Bryan Biedenkapp, N2PLL +* +*/ +#if !defined(__REST_HTTP__SECURE_HTTP_SERVER_H__) +#define __REST_HTTP__SECURE_HTTP_SERVER_H__ + +#if defined(ENABLE_TCP_SSL) + +#include "common/Defines.h" +#include "common/network/rest/http/SecureServerConnection.h" +#include "common/network/rest/http/ServerConnectionManager.h" +#include "common/network/rest/http/HTTPRequestHandler.h" + +#include +#include + +#include +#include +#include +#include +#include + +namespace network +{ + namespace rest + { + namespace http + { + + // --------------------------------------------------------------------------- + // Class Declaration + // This class implements top-level routines of the secure HTTP server. + // --------------------------------------------------------------------------- + + template class ConnectionImpl = SecureServerConnection> + class SecureHTTPServer { + public: + auto operator=(SecureHTTPServer&) -> SecureHTTPServer& = delete; + auto operator=(SecureHTTPServer&&) -> SecureHTTPServer& = delete; + SecureHTTPServer(SecureHTTPServer&) = delete; + + /// Initializes a new instance of the SecureHTTPServer class. + explicit SecureHTTPServer(const std::string& address, uint16_t port) : + m_ioService(), + m_acceptor(m_ioService), + m_connectionManager(), + m_context(asio::ssl::context::tlsv12), + m_socket(m_ioService), + m_requestHandler() + { + asio::ip::address ipAddress = asio::ip::address::from_string(address); + m_endpoint = asio::ip::tcp::endpoint(ipAddress, port); + } + + /// Helper to set the SSL certificate and private key. + bool setCertAndKey(const std::string& keyFile, const std::string& certFile) + { + try + { + m_context.use_certificate_chain_file(certFile); + m_context.use_private_key_file(keyFile, asio::ssl::context::pem); + return true; + } + catch(const std::exception& e) { + ::LogError(LOG_REST, "%s", e.what()); + return false; + } + } + + /// Helper to set the HTTP request handlers. + template + void setHandler(Handler&& handler) + { + m_requestHandler = RequestHandlerType(std::forward(handler)); + } + + /// Open TCP acceptor. + void open() + { + // open the acceptor with the option to reuse the address (i.e. SO_REUSEADDR) + m_acceptor.open(m_endpoint.protocol()); + m_acceptor.set_option(asio::ip::tcp::acceptor::reuse_address(true)); + m_acceptor.set_option(asio::socket_base::keep_alive(true)); + m_acceptor.bind(m_endpoint); + m_acceptor.listen(); + + accept(); + } + + /// Run the servers ASIO IO service loop. + void run() + { + // the run() call will block until all asynchronous operations + // have finished; while the server is running, there is always at least one + // asynchronous operation outstanding: the asynchronous accept call waiting + // for new incoming connections + m_ioService.run(); + } + + /// Helper to stop running ASIO IO services. + void stop() + { + // the server is stopped by cancelling all outstanding asynchronous + // operations; once all operations have finished the m_ioService::run() + // call will exit + m_acceptor.close(); + m_connectionManager.stopAll(); + } + + private: + /// Perform an asynchronous accept operation. + void accept() + { + m_acceptor.async_accept(m_socket, [this](asio::error_code ec) { + // check whether the server was stopped by a signal before this + // completion handler had a chance to run + if (!m_acceptor.is_open()) { + return; + } + + if (!ec) { + m_connectionManager.start(std::make_shared(std::move(m_socket), m_context, m_connectionManager, m_requestHandler)); + } + + accept(); + }); + } + + typedef ConnectionImpl ConnectionType; + typedef std::shared_ptr ConnectionTypePtr; + + asio::io_service m_ioService; + asio::ip::tcp::acceptor m_acceptor; + + asio::ip::tcp::endpoint m_endpoint; + + ServerConnectionManager m_connectionManager; + + asio::ssl::context m_context; + asio::ip::tcp::socket m_socket; + + std::string m_certFile; + std::string m_keyFile; + + RequestHandlerType m_requestHandler; + }; + } // namespace http + } // namespace rest +} // namespace network + +#endif // ENABLE_TCP_SSL + +#endif // __REST_HTTP__SECURE_HTTP_SERVER_H__ diff --git a/src/common/network/rest/http/SecureServerConnection.h b/src/common/network/rest/http/SecureServerConnection.h new file mode 100644 index 00000000..0fa1f7e1 --- /dev/null +++ b/src/common/network/rest/http/SecureServerConnection.h @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: BSL-1.0 +/** +* Digital Voice Modem - Common Library +* BSL-1.0 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Common Library +* @derivedfrom CRUD (https://github.com/venediktov/CRUD) +* @license BSL-1.0 License (https://opensource.org/license/bsl1-0-html) +* +* Copyright (c) 2003-2013 Christopher M. Kohlhoff +* Copyright (C) 2024 Bryan Biedenkapp, N2PLL +* +*/ +#if !defined(__REST_HTTP__SECURE_SERVER_CONNECTION_H__) +#define __REST_HTTP__SECURE_SERVER_CONNECTION_H__ + +#if defined(ENABLE_TCP_SSL) + +#include "common/Defines.h" +#include "common/network/rest/http/HTTPLexer.h" +#include "common/network/rest/http/HTTPPayload.h" +#include "common/Log.h" + +#include +#include +#include +#include +#include +#include + +namespace network +{ + namespace rest + { + namespace http + { + // --------------------------------------------------------------------------- + // Class Prototypes + // --------------------------------------------------------------------------- + + template class ServerConnectionManager; + + // --------------------------------------------------------------------------- + // Class Declaration + // This class represents a single connection from a client. + // --------------------------------------------------------------------------- + + template + class SecureServerConnection : public std::enable_shared_from_this> { + typedef SecureServerConnection selfType; + typedef std::shared_ptr selfTypePtr; + typedef ServerConnectionManager ConnectionManagerType; + public: + auto operator=(SecureServerConnection&) -> SecureServerConnection& = delete; + auto operator=(SecureServerConnection&&) -> SecureServerConnection& = delete; + SecureServerConnection(SecureServerConnection&) = delete; + + /// Initializes a new instance of the SecureServerConnection class. + explicit SecureServerConnection(asio::ip::tcp::socket socket, asio::ssl::context& context, ConnectionManagerType& manager, RequestHandlerType& handler, + bool persistent = false) : + m_socket(std::move(socket), context), + m_connectionManager(manager), + m_requestHandler(handler), + m_lexer(HTTPLexer(false)), + m_persistent(persistent) + { + /* stub */ + } + + /// Start the first asynchronous operation for the connection. + void start() { handshake(); } + /// Stop all asynchronous operations associated with the connection. + void stop() + { + try + { + if (m_socket.lowest_layer().is_open()) { + m_socket.lowest_layer().close(); + } + } + catch(const std::exception&) { /* ignore */ } + } + + private: + /// Perform an asynchronous SSL handshake. + void handshake() + { + if (!m_persistent) { + auto self(this->shared_from_this()); + } + + m_socket.async_handshake(asio::ssl::stream_base::server, [this](asio::error_code ec) { + if (!ec) { + read(); + } + }); + } + + /// Perform an asynchronous read operation. + void read() + { + if (!m_persistent) { + auto self(this->shared_from_this()); + } + + m_socket.async_read_some(asio::buffer(m_buffer), [=](asio::error_code ec, std::size_t bytes_transferred) { + if (!ec) { + HTTPLexer::ResultType result; + char* content; + + std::tie(result, content) = m_lexer.parse(m_request, m_buffer.data(), m_buffer.data() + bytes_transferred); + + std::string contentLength = m_request.headers.find("Content-Length"); + if (contentLength != "") { + size_t length = (size_t)::strtoul(contentLength.c_str(), NULL, 10); + m_request.content = std::string(content, length); + } + + m_request.headers.add("RemoteHost", m_socket.lowest_layer().remote_endpoint().address().to_string()); + + if (result == HTTPLexer::GOOD) { + m_requestHandler.handleRequest(m_request, m_reply); + write(); + } + else if (result == HTTPLexer::BAD) { + m_reply = HTTPPayload::statusPayload(HTTPPayload::BAD_REQUEST); + write(); + } + else { + read(); + } + } + else if (ec != asio::error::operation_aborted) { + if (ec) { + ::LogError(LOG_REST, "SecureServerConnection::read(), %s, code = %u", ec.message().c_str(), ec.value()); + } + m_connectionManager.stop(this->shared_from_this()); + } + }); + } + + /// Perform an asynchronous write operation. + void write() + { + if (!m_persistent) { + auto self(this->shared_from_this()); + } else { + m_reply.headers.add("Connection", "keep-alive"); + } + + auto buffers = m_reply.toBuffers(); + asio::async_write(m_socket, buffers, [=](asio::error_code ec, std::size_t) { + if (m_persistent) { + m_lexer.reset(); + m_reply.headers = HTTPHeaders(); + m_reply.status = HTTPPayload::OK; + m_reply.content = ""; + m_request = HTTPPayload(); + read(); + } + else { + if (!ec) { + try + { + // initiate graceful connection closure + asio::error_code ignored_ec; + m_socket.lowest_layer().shutdown(asio::ip::tcp::socket::shutdown_both, ignored_ec); + } + catch(const std::exception& e) { ::LogError(LOG_REST, "%s", ec.message().c_str()); } + } + + if (ec != asio::error::operation_aborted) { + if (ec) { + ::LogError(LOG_REST, "SecureServerConnection::write(), %s, code = %u", ec.message().c_str(), ec.value()); + } + m_connectionManager.stop(this->shared_from_this()); + } + } + }); + } + + asio::ssl::stream m_socket; + + ConnectionManagerType& m_connectionManager; + RequestHandlerType& m_requestHandler; + + std::array m_buffer; + + HTTPPayload m_request; + HTTPLexer m_lexer; + HTTPPayload m_reply; + + bool m_persistent; + }; + } // namespace http + } // namespace rest +} // namespace network + +#endif // ENABLE_TCP_SSL + +#endif // __REST_HTTP__SECURE_SERVER_CONNECTION_H__ diff --git a/src/common/network/tcp/SecureTcpClient.h b/src/common/network/tcp/SecureTcpClient.h index cceee58a..ad20e136 100644 --- a/src/common/network/tcp/SecureTcpClient.h +++ b/src/common/network/tcp/SecureTcpClient.h @@ -20,6 +20,7 @@ #include "common/network/tcp/Socket.h" #include +#include #include #include diff --git a/src/fne/HostFNE.cpp b/src/fne/HostFNE.cpp index 60904919..a118c3b5 100644 --- a/src/fne/HostFNE.cpp +++ b/src/fne/HostFNE.cpp @@ -364,6 +364,9 @@ bool HostFNE::initializeRESTAPI() std::string restApiAddress = systemConf["restAddress"].as("127.0.0.1"); uint16_t restApiPort = (uint16_t)systemConf["restPort"].as(REST_API_DEFAULT_PORT); std::string restApiPassword = systemConf["restPassword"].as(); + bool restApiEnableSSL = systemConf["restSsl"].as(false); + std::string restApiSSLCert = systemConf["restSslCertificate"].as("web.crt"); + std::string restApiSSLKey = systemConf["restSslKey"].as("web.key"); bool restApiDebug = systemConf["restDebug"].as(false); if (restApiPassword.length() > 64) { @@ -378,12 +381,26 @@ bool HostFNE::initializeRESTAPI() restApiEnable = false; } + if (restApiSSLCert.empty() && restApiEnableSSL) { + ::LogWarning(LOG_HOST, "REST API SSL certificate not provided; REST API SSL disabled."); + restApiEnableSSL = false; + } + + if (restApiSSLKey.empty() && restApiEnableSSL) { + ::LogWarning(LOG_HOST, "REST API SSL certificate private key not provided; REST API SSL disabled."); + restApiEnableSSL = false; + } + LogInfo("REST API Parameters"); LogInfo(" REST API Enabled: %s", restApiEnable ? "yes" : "no"); if (restApiEnable) { LogInfo(" REST API Address: %s", restApiAddress.c_str()); LogInfo(" REST API Port: %u", restApiPort); + LogInfo(" REST API SSL Enabled: %s", restApiEnableSSL ? "yes" : "no"); + LogInfo(" REST API SSL Certificate: %s", restApiSSLCert.c_str()); + LogInfo(" REST API SSL Private Key: %s", restApiSSLKey.c_str()); + if (restApiDebug) { LogInfo(" REST API Debug: yes"); } @@ -391,7 +408,7 @@ bool HostFNE::initializeRESTAPI() // initialize network remote command if (restApiEnable) { - m_RESTAPI = new RESTAPI(restApiAddress, restApiPort, restApiPassword, this, restApiDebug); + m_RESTAPI = new RESTAPI(restApiAddress, restApiPort, restApiPassword, restApiSSLKey, restApiSSLCert, restApiEnableSSL, this, restApiDebug); m_RESTAPI->setLookups(m_ridLookup, m_tidLookup); bool ret = m_RESTAPI->open(); if (!ret) { diff --git a/src/fne/network/FNENetwork.cpp b/src/fne/network/FNENetwork.cpp index 0bf5e064..0a0bccd5 100644 --- a/src/fne/network/FNENetwork.cpp +++ b/src/fne/network/FNENetwork.cpp @@ -1118,7 +1118,7 @@ void FNENetwork::writeWhitelistRIDs(uint32_t peerId) if (i == chunkCnt - 1U) { // this is a disgusting dirty hack... - listSize = abs((i * MAX_RID_LIST_CHUNK) - ridWhitelist.size()); + listSize = ::abs((long)((i * MAX_RID_LIST_CHUNK) - ridWhitelist.size())); } } @@ -1187,7 +1187,7 @@ void FNENetwork::writeBlacklistRIDs(uint32_t peerId) if (i == chunkCnt - 1U) { // this is a disgusting dirty hack... - listSize = abs((i * MAX_RID_LIST_CHUNK) - ridBlacklist.size()); + listSize = ::abs((long)((i * MAX_RID_LIST_CHUNK) - ridBlacklist.size())); } } diff --git a/src/fne/network/RESTAPI.cpp b/src/fne/network/RESTAPI.cpp index ea75fbd7..638edbc3 100644 --- a/src/fne/network/RESTAPI.cpp +++ b/src/fne/network/RESTAPI.cpp @@ -380,11 +380,19 @@ TalkgroupRuleGroupVoice jsonToTG(json::object& req, HTTPPayload& reply) /// Network Hostname/IP address to connect to. /// Network port number. /// Authentication password. -/// Instance of the Host class. +/// +/// +/// +/// Instance of the HostFNE class. /// -RESTAPI::RESTAPI(const std::string& address, uint16_t port, const std::string& password, HostFNE* host, bool debug) : +RESTAPI::RESTAPI(const std::string& address, uint16_t port, const std::string& password, + const std::string& keyFile, const std::string& certFile, bool enableSSL, HostFNE* host, bool debug) : m_dispatcher(debug), m_restServer(address, port), +#if defined(ENABLE_TCP_SSL) + m_restSecureServer(address, port), + m_enableSSL(enableSSL), +#endif // ENABLE_TCP_SSL m_random(), m_password(password), m_passwordHash(nullptr), @@ -417,6 +425,15 @@ RESTAPI::RESTAPI(const std::string& address, uint16_t port, const std::string& p Utils::dump("REST Password Hash", m_passwordHash, 32U); } +#if defined(ENABLE_TCP_SSL) + if (m_enableSSL) { + if (!m_restSecureServer.setCertAndKey(keyFile, certFile)) { + m_enableSSL = false; + ::LogError(LOG_REST, "failed to initialize SSL for HTTPS, disabling SSL"); + } + } +#endif // ENABLE_TCP_SSL + std::random_device rd; std::mt19937 mt(rd()); m_random = mt; @@ -454,7 +471,17 @@ void RESTAPI::setNetwork(network::FNENetwork* network) bool RESTAPI::open() { initializeEndpoints(); - m_restServer.setHandler(m_dispatcher); +#if defined(ENABLE_TCP_SSL) + if (m_enableSSL) { + m_restSecureServer.open(); + m_restSecureServer.setHandler(m_dispatcher); + } else { +#endif // ENABLE_TCP_SSL + m_restServer.open(); + m_restServer.setHandler(m_dispatcher); +#if defined(ENABLE_TCP_SSL) + } +#endif // ENABLE_TCP_SSL return run(); } @@ -464,7 +491,15 @@ bool RESTAPI::open() /// void RESTAPI::close() { - m_restServer.stop(); +#if defined(ENABLE_TCP_SSL) + if (m_enableSSL) { + m_restSecureServer.stop(); + } else { +#endif // ENABLE_TCP_SSL + m_restServer.stop(); +#if defined(ENABLE_TCP_SSL) + } +#endif // ENABLE_TCP_SSL wait(); } @@ -477,7 +512,15 @@ void RESTAPI::close() /// void RESTAPI::entry() { - m_restServer.run(); +#if defined(ENABLE_TCP_SSL) + if (m_enableSSL) { + m_restSecureServer.run(); + } else { +#endif // ENABLE_TCP_SSL + m_restServer.run(); +#if defined(ENABLE_TCP_SSL) + } +#endif // ENABLE_TCP_SSL } /// diff --git a/src/fne/network/RESTAPI.h b/src/fne/network/RESTAPI.h index 5fd46995..4a5f6ac7 100644 --- a/src/fne/network/RESTAPI.h +++ b/src/fne/network/RESTAPI.h @@ -16,6 +16,7 @@ #include "fne/Defines.h" #include "common/network/rest/RequestDispatcher.h" #include "common/network/rest/http/HTTPServer.h" +#include "common/network/rest/http/SecureHTTPServer.h" #include "common/lookups/RadioIdLookup.h" #include "common/lookups/TalkgroupRulesLookup.h" #include "common/Thread.h" @@ -40,7 +41,8 @@ namespace network { class HOST_SW_API FNENetwork; } class HOST_SW_API RESTAPI : private Thread { public: /// Initializes a new instance of the RESTAPI class. - RESTAPI(const std::string& address, uint16_t port, const std::string& password, HostFNE* host, bool debug); + RESTAPI(const std::string& address, uint16_t port, const std::string& password, const std::string& keyFile, const std::string& certFile, + bool enableSSL, HostFNE* host, bool debug); /// Finalizes a instance of the RESTAPI class. ~RESTAPI() override; @@ -60,6 +62,10 @@ private: typedef network::rest::http::HTTPPayload HTTPPayload; RESTDispatcherType m_dispatcher; network::rest::http::HTTPServer m_restServer; +#if defined(ENABLE_TCP_SSL) + network::rest::http::SecureHTTPServer m_restSecureServer; + bool m_enableSSL; +#endif // ENABLE_TCP_SSL std::mt19937 m_random; diff --git a/src/host/Host.Config.cpp b/src/host/Host.Config.cpp index 4571ca79..274ef1d4 100644 --- a/src/host/Host.Config.cpp +++ b/src/host/Host.Config.cpp @@ -7,7 +7,7 @@ * @package DVM / Modem Host Software * @license GPLv2 License (https://opensource.org/licenses/GPL-2.0) * -* Copyright (C) 2017-2023 Bryan Biedenkapp, N2PLL +* Copyright (C) 2017-2024 Bryan Biedenkapp, N2PLL * */ #include "Defines.h" @@ -187,12 +187,13 @@ bool Host::readParams() std::string restApiAddress = controlCh["restAddress"].as(""); uint16_t restApiPort = (uint16_t)controlCh["restPort"].as(REST_API_DEFAULT_PORT); std::string restApiPassword = controlCh["restPassword"].as(); + bool restSsl = controlCh["restSsl"].as(false); - VoiceChData data = VoiceChData(m_channelId, m_channelNo, restApiAddress, restApiPort, restApiPassword); + VoiceChData data = VoiceChData(m_channelId, m_channelNo, restApiAddress, restApiPort, restApiPassword, restSsl); m_controlChData = data; if (!m_controlChData.address().empty() && m_controlChData.port() > 0) { - ::LogInfoEx(LOG_HOST, "Control Channel REST API Address %s:%u", m_controlChData.address().c_str(), m_controlChData.port()); + ::LogInfoEx(LOG_HOST, "Control Channel REST API Address %s:%u SSL %u", m_controlChData.address().c_str(), m_controlChData.port(), restSsl); } else { ::LogInfoEx(LOG_HOST, "No Control Channel REST API Configured, CC notify disabled"); } @@ -234,10 +235,11 @@ bool Host::readParams() std::string restApiAddress = channel["restAddress"].as("127.0.0.1"); uint16_t restApiPort = (uint16_t)channel["restPort"].as(REST_API_DEFAULT_PORT); std::string restApiPassword = channel["restPassword"].as(); + bool restSsl = channel["restSsl"].as(false); - ::LogInfoEx(LOG_HOST, "Voice Channel Id %u Channel No $%04X REST API Address %s:%u", chId, chNo, restApiAddress.c_str(), restApiPort); + ::LogInfoEx(LOG_HOST, "Voice Channel Id %u Channel No $%04X REST API Address %s:%u SSL %u", chId, chNo, restApiAddress.c_str(), restApiPort, restSsl); - VoiceChData data = VoiceChData(chId, chNo, restApiAddress, restApiPort, restApiPassword); + VoiceChData data = VoiceChData(chId, chNo, restApiAddress, restApiPort, restApiPassword, restSsl); m_voiceChData[chNo] = data; m_voiceChNo.push_back(chNo); } @@ -658,6 +660,9 @@ bool Host::createNetwork() std::string restApiAddress = networkConf["restAddress"].as("127.0.0.1"); uint16_t restApiPort = (uint16_t)networkConf["restPort"].as(REST_API_DEFAULT_PORT); std::string restApiPassword = networkConf["restPassword"].as(); + bool restApiEnableSSL = networkConf["restSsl"].as(false); + std::string restApiSSLCert = networkConf["restSslCertificate"].as("web.crt"); + std::string restApiSSLKey = networkConf["restSslKey"].as("web.key"); bool restApiDebug = networkConf["restDebug"].as(false); uint32_t id = networkConf["id"].as(1000U); uint32_t jitter = networkConf["talkgroupHang"].as(360U); @@ -718,6 +723,16 @@ bool Host::createNetwork() restApiEnable = false; } + if (restApiSSLCert.empty() && restApiEnableSSL) { + ::LogWarning(LOG_HOST, "REST API SSL certificate not provided; REST API SSL disabled."); + restApiEnableSSL = false; + } + + if (restApiSSLKey.empty() && restApiEnableSSL) { + ::LogWarning(LOG_HOST, "REST API SSL certificate private key not provided; REST API SSL disabled."); + restApiEnableSSL = false; + } + IdenTable entry = m_idenTable->find(m_channelId); LogInfo("Network Parameters"); @@ -748,6 +763,10 @@ bool Host::createNetwork() LogInfo(" REST API Address: %s", restApiAddress.c_str()); LogInfo(" REST API Port: %u", restApiPort); + LogInfo(" REST API SSL Enabled: %s", restApiEnableSSL ? "yes" : "no"); + LogInfo(" REST API SSL Certificate: %s", restApiSSLCert.c_str()); + LogInfo(" REST API SSL Private Key: %s", restApiSSLKey.c_str()); + if (restApiDebug) { LogInfo(" REST API Debug: yes"); } @@ -781,7 +800,7 @@ bool Host::createNetwork() // initialize network remote command if (restApiEnable) { - m_RESTAPI = new RESTAPI(restApiAddress, restApiPort, restApiPassword, this, restApiDebug); + m_RESTAPI = new RESTAPI(restApiAddress, restApiPort, restApiPassword, restApiSSLKey, restApiSSLCert, restApiEnableSSL, this, restApiDebug); m_RESTAPI->setLookups(m_ridLookup, m_tidLookup); bool ret = m_RESTAPI->open(); if (!ret) { diff --git a/src/host/dmr/Slot.cpp b/src/host/dmr/Slot.cpp index 3db629ea..da8b484d 100644 --- a/src/host/dmr/Slot.cpp +++ b/src/host/dmr/Slot.cpp @@ -876,7 +876,7 @@ void Slot::init(Control* dmr, bool authoritative, uint32_t colorCode, SiteData s req["clear"].set(clear); RESTClient::send(voiceChData.address(), voiceChData.port(), voiceChData.password(), - HTTP_PUT, PUT_DMR_TSCC_PAYLOAD_ACT, req, tscc->m_debug); + HTTP_PUT, PUT_DMR_TSCC_PAYLOAD_ACT, req, voiceChData.ssl(), tscc->m_debug); } else { ::LogError(LOG_DMR, "DMR Slot %u, DT_CSBK, CSBKO_RAND (Random Access), failed to clear payload channel, chNo = %u, slot = %u", tscc->m_slotNo, chNo, slot); @@ -893,7 +893,7 @@ void Slot::init(Control* dmr, bool authoritative, uint32_t colorCode, SiteData s req["slot"].set(slot); RESTClient::send(voiceChData.address(), voiceChData.port(), voiceChData.password(), - HTTP_PUT, PUT_PERMIT_TG, req, m_dmr->m_debug); + HTTP_PUT, PUT_PERMIT_TG, req, voiceChData.ssl(), m_dmr->m_debug); } else { ::LogError(LOG_DMR, "DMR Slot %u, DT_CSBK, CSBKO_RAND (Random Access), failed to clear TG permit, chNo = %u, slot = %u", tscc->m_slotNo, chNo, slot); @@ -1115,7 +1115,7 @@ void Slot::notifyCC_ReleaseGrant(uint32_t dstId) req["slot"].set(slot); int ret = RESTClient::send(m_controlChData.address(), m_controlChData.port(), m_controlChData.password(), - HTTP_PUT, PUT_RELEASE_TG, req, m_debug); + HTTP_PUT, PUT_RELEASE_TG, req, m_controlChData.ssl(), m_debug); if (ret != network::rest::http::HTTPPayload::StatusType::OK) { ::LogError(LOG_DMR, "DMR Slot %u, failed to notify the CC %s:%u of the release of, dstId = %u", m_slotNo, m_controlChData.address().c_str(), m_controlChData.port(), dstId); } @@ -1148,7 +1148,7 @@ void Slot::notifyCC_TouchGrant(uint32_t dstId) req["slot"].set(slot); int ret = RESTClient::send(m_controlChData.address(), m_controlChData.port(), m_controlChData.password(), - HTTP_PUT, PUT_TOUCH_TG, req, m_debug); + HTTP_PUT, PUT_TOUCH_TG, req, m_controlChData.ssl(), m_debug); if (ret != network::rest::http::HTTPPayload::StatusType::OK) { ::LogError(LOG_DMR, "DMR Slot %u, failed to notify the CC %s:%u of the touch of, dstId = %u", m_slotNo, m_controlChData.address().c_str(), m_controlChData.port(), dstId); } diff --git a/src/host/dmr/packet/ControlSignaling.cpp b/src/host/dmr/packet/ControlSignaling.cpp index 81eacb76..785d1729 100644 --- a/src/host/dmr/packet/ControlSignaling.cpp +++ b/src/host/dmr/packet/ControlSignaling.cpp @@ -900,7 +900,7 @@ bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_ req["slot"].set(slot); int ret = RESTClient::send(voiceChData.address(), voiceChData.port(), voiceChData.password(), - HTTP_PUT, PUT_PERMIT_TG, req, m_tscc->m_debug); + HTTP_PUT, PUT_PERMIT_TG, req, voiceChData.ssl(), m_tscc->m_debug); if (ret != network::rest::http::HTTPPayload::StatusType::OK) { ::LogError(LOG_RF, "DMR Slot %u, DT_CSBK, CSBKO_RAND (Random Access), failed to permit TG for use, chNo = %u, slot = %u", m_tscc->m_slotNo, chNo, slot); m_tscc->m_affiliations->releaseGrant(dstId, false); @@ -951,7 +951,7 @@ bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_ req["voice"].set(voice); RESTClient::send(voiceChData.address(), voiceChData.port(), voiceChData.password(), - HTTP_PUT, PUT_DMR_TSCC_PAYLOAD_ACT, req, m_tscc->m_debug); + HTTP_PUT, PUT_DMR_TSCC_PAYLOAD_ACT, req, voiceChData.ssl(), m_tscc->m_debug); } else { ::LogError(LOG_RF, "DMR Slot %u, DT_CSBK, CSBKO_RAND (Random Access), failed to activate payload channel, chNo = %u, slot = %u", m_tscc->m_slotNo, chNo, slot); @@ -978,7 +978,7 @@ bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_ req["slot"].set(slot); int ret = RESTClient::send(voiceChData.address(), voiceChData.port(), voiceChData.password(), - HTTP_PUT, PUT_PERMIT_TG, req, m_tscc->m_debug); + HTTP_PUT, PUT_PERMIT_TG, req, voiceChData.ssl(), m_tscc->m_debug); if (ret != network::rest::http::HTTPPayload::StatusType::OK) { ::LogError(LOG_RF, "DMR Slot %u, DT_CSBK, CSBKO_RAND (Random Access), failed to permit TG for use, chNo = %u, slot = %u", m_tscc->m_slotNo, chNo, slot); m_tscc->m_affiliations->releaseGrant(dstId, false); @@ -1027,7 +1027,7 @@ bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_ req["voice"].set(voice); RESTClient::send(voiceChData.address(), voiceChData.port(), voiceChData.password(), - HTTP_PUT, PUT_DMR_TSCC_PAYLOAD_ACT, req, m_tscc->m_debug); + HTTP_PUT, PUT_DMR_TSCC_PAYLOAD_ACT, req, voiceChData.ssl(), m_tscc->m_debug); } else { ::LogError(LOG_RF, "DMR Slot %u, DT_CSBK, CSBKO_RAND (Random Access), failed to activate payload channel, chNo = %u, slot = %u", m_tscc->m_slotNo, chNo, slot); @@ -1185,7 +1185,7 @@ bool ControlSignaling::writeRF_CSBK_Data_Grant(uint32_t srcId, uint32_t dstId, u req["voice"].set(voice); RESTClient::send(voiceChData.address(), voiceChData.port(), voiceChData.password(), - HTTP_PUT, PUT_DMR_TSCC_PAYLOAD_ACT, req, m_tscc->m_debug); + HTTP_PUT, PUT_DMR_TSCC_PAYLOAD_ACT, req, voiceChData.ssl(), m_tscc->m_debug); } else { ::LogError(LOG_RF, "DMR Slot %u, DT_CSBK, CSBKO_RAND (Random Access), failed to activate payload channel, chNo = %u, slot = %u", m_tscc->m_slotNo, chNo, slot); @@ -1232,7 +1232,7 @@ bool ControlSignaling::writeRF_CSBK_Data_Grant(uint32_t srcId, uint32_t dstId, u req["voice"].set(voice); RESTClient::send(voiceChData.address(), voiceChData.port(), voiceChData.password(), - HTTP_PUT, PUT_DMR_TSCC_PAYLOAD_ACT, req, m_tscc->m_debug); + HTTP_PUT, PUT_DMR_TSCC_PAYLOAD_ACT, req, voiceChData.ssl(), m_tscc->m_debug); } else { ::LogError(LOG_RF, "DMR Slot %u, DT_CSBK, CSBKO_RAND (Random Access), failed to activate payload channel, chNo = %u, slot = %u", m_tscc->m_slotNo, chNo, slot); diff --git a/src/host/network/RESTAPI.cpp b/src/host/network/RESTAPI.cpp index 42558f33..3ed65697 100644 --- a/src/host/network/RESTAPI.cpp +++ b/src/host/network/RESTAPI.cpp @@ -135,11 +135,19 @@ bool parseRequestBody(const HTTPPayload& request, HTTPPayload& reply, json::obje /// Network Hostname/IP address to connect to. /// Network port number. /// Authentication password. +/// +/// +/// /// Instance of the Host class. /// -RESTAPI::RESTAPI(const std::string& address, uint16_t port, const std::string& password, Host* host, bool debug) : +RESTAPI::RESTAPI(const std::string& address, uint16_t port, const std::string& password, + const std::string& keyFile, const std::string& certFile, bool enableSSL, Host* host, bool debug) : m_dispatcher(debug), m_restServer(address, port), +#if defined(ENABLE_TCP_SSL) + m_restSecureServer(address, port), + m_enableSSL(enableSSL), +#endif // ENABLE_TCP_SSL m_random(), m_p25MFId(p25::P25_MFG_STANDARD), m_password(password), @@ -175,6 +183,15 @@ RESTAPI::RESTAPI(const std::string& address, uint16_t port, const std::string& p Utils::dump("REST Password Hash", m_passwordHash, 32U); } +#if defined(ENABLE_TCP_SSL) + if (m_enableSSL) { + if (!m_restSecureServer.setCertAndKey(keyFile, certFile)) { + m_enableSSL = false; + ::LogError(LOG_REST, "failed to initialize SSL for HTTPS, disabling SSL"); + } + } +#endif // ENABLE_TCP_SSL + std::random_device rd; std::mt19937 mt(rd()); m_random = mt; @@ -216,7 +233,17 @@ void RESTAPI::setProtocols(dmr::Control* dmr, p25::Control* p25, nxdn::Control* bool RESTAPI::open() { initializeEndpoints(); - m_restServer.setHandler(m_dispatcher); +#if defined(ENABLE_TCP_SSL) + if (m_enableSSL) { + m_restSecureServer.open(); + m_restSecureServer.setHandler(m_dispatcher); + } else { +#endif // ENABLE_TCP_SSL + m_restServer.open(); + m_restServer.setHandler(m_dispatcher); +#if defined(ENABLE_TCP_SSL) + } +#endif // ENABLE_TCP_SSL return run(); } @@ -226,7 +253,15 @@ bool RESTAPI::open() /// void RESTAPI::close() { - m_restServer.stop(); +#if defined(ENABLE_TCP_SSL) + if (m_enableSSL) { + m_restSecureServer.stop(); + } else { +#endif // ENABLE_TCP_SSL + m_restServer.stop(); +#if defined(ENABLE_TCP_SSL) + } +#endif // ENABLE_TCP_SSL wait(); } @@ -239,7 +274,15 @@ void RESTAPI::close() /// void RESTAPI::entry() { - m_restServer.run(); +#if defined(ENABLE_TCP_SSL) + if (m_enableSSL) { + m_restSecureServer.run(); + } else { +#endif // ENABLE_TCP_SSL + m_restServer.run(); +#if defined(ENABLE_TCP_SSL) + } +#endif // ENABLE_TCP_SSL } /// diff --git a/src/host/network/RESTAPI.h b/src/host/network/RESTAPI.h index a4cf31d2..0b946694 100644 --- a/src/host/network/RESTAPI.h +++ b/src/host/network/RESTAPI.h @@ -16,6 +16,7 @@ #include "Defines.h" #include "common/network/rest/RequestDispatcher.h" #include "common/network/rest/http/HTTPServer.h" +#include "common/network/rest/http/SecureHTTPServer.h" #include "common/lookups/RadioIdLookup.h" #include "common/lookups/TalkgroupRulesLookup.h" #include "common/Thread.h" @@ -42,7 +43,8 @@ namespace nxdn { class HOST_SW_API Control; } class HOST_SW_API RESTAPI : private Thread { public: /// Initializes a new instance of the RESTAPI class. - RESTAPI(const std::string& address, uint16_t port, const std::string& password, Host* host, bool debug); + RESTAPI(const std::string& address, uint16_t port, const std::string& password, const std::string& keyFile, const std::string& certFile, + bool enableSSL, Host* host, bool debug); /// Finalizes a instance of the RESTAPI class. ~RESTAPI() override; @@ -62,6 +64,10 @@ private: typedef network::rest::http::HTTPPayload HTTPPayload; RESTDispatcherType m_dispatcher; network::rest::http::HTTPServer m_restServer; +#if defined(ENABLE_TCP_SSL) + network::rest::http::SecureHTTPServer m_restSecureServer; + bool m_enableSSL; +#endif // ENABLE_TCP_SSL std::mt19937 m_random; diff --git a/src/host/nxdn/Control.cpp b/src/host/nxdn/Control.cpp index 1c31a92d..2545b934 100644 --- a/src/host/nxdn/Control.cpp +++ b/src/host/nxdn/Control.cpp @@ -297,7 +297,7 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw req["dstId"].set(dstId); RESTClient::send(voiceChData.address(), voiceChData.port(), voiceChData.password(), - HTTP_PUT, PUT_PERMIT_TG, req, m_debug); + HTTP_PUT, PUT_PERMIT_TG, req, voiceChData.ssl(), m_debug); } else { ::LogError(LOG_NXDN, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL_RESP ", failed to clear TG permit, chNo = %u", chNo); @@ -1052,7 +1052,7 @@ void Control::notifyCC_ReleaseGrant(uint32_t dstId) req["dstId"].set(dstId); int ret = RESTClient::send(m_controlChData.address(), m_controlChData.port(), m_controlChData.password(), - HTTP_PUT, PUT_RELEASE_TG, req, m_debug); + HTTP_PUT, PUT_RELEASE_TG, req, m_controlChData.ssl(), m_debug); if (ret != network::rest::http::HTTPPayload::StatusType::OK) { ::LogError(LOG_NXDN, "failed to notify the CC %s:%u of the release of, dstId = %u", m_controlChData.address().c_str(), m_controlChData.port(), dstId); } @@ -1083,7 +1083,7 @@ void Control::notifyCC_TouchGrant(uint32_t dstId) req["dstId"].set(dstId); int ret = RESTClient::send(m_controlChData.address(), m_controlChData.port(), m_controlChData.password(), - HTTP_PUT, PUT_TOUCH_TG, req, m_debug); + HTTP_PUT, PUT_TOUCH_TG, req, m_controlChData.ssl(), m_debug); if (ret != network::rest::http::HTTPPayload::StatusType::OK) { ::LogError(LOG_NXDN, "failed to notify the CC %s:%u of the touch of, dstId = %u", m_controlChData.address().c_str(), m_controlChData.port(), dstId); } diff --git a/src/host/nxdn/packet/ControlSignaling.cpp b/src/host/nxdn/packet/ControlSignaling.cpp index b6c53446..2bc0b04d 100644 --- a/src/host/nxdn/packet/ControlSignaling.cpp +++ b/src/host/nxdn/packet/ControlSignaling.cpp @@ -607,7 +607,7 @@ bool ControlSignaling::writeRF_Message_Grant(uint32_t srcId, uint32_t dstId, uin req["dstId"].set(dstId); int ret = RESTClient::send(voiceChData.address(), voiceChData.port(), voiceChData.password(), - HTTP_PUT, PUT_PERMIT_TG, req, m_nxdn->m_debug); + HTTP_PUT, PUT_PERMIT_TG, req, voiceChData.ssl(), m_nxdn->m_debug); if (ret != network::rest::http::HTTPPayload::StatusType::OK) { ::LogError((net) ? LOG_NET : LOG_RF, "NXDN, %s, failed to permit TG for use, chNo = %u", rcch->toString().c_str(), chNo); m_nxdn->m_affiliations.releaseGrant(dstId, false); diff --git a/src/host/p25/Control.cpp b/src/host/p25/Control.cpp index e1b5a85e..4aca7f2d 100644 --- a/src/host/p25/Control.cpp +++ b/src/host/p25/Control.cpp @@ -433,7 +433,7 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw req["dstId"].set(dstId); RESTClient::send(voiceChData.address(), voiceChData.port(), voiceChData.password(), - HTTP_PUT, PUT_PERMIT_TG, req, m_debug); + HTTP_PUT, PUT_PERMIT_TG, req, voiceChData.ssl(), m_debug); } else { ::LogError(LOG_P25, P25_TSDU_STR ", TSBK_IOSP_GRP_VCH (Group Voice Channel Grant), failed to clear TG permit, chNo = %u", chNo); @@ -1460,7 +1460,7 @@ void Control::notifyCC_ReleaseGrant(uint32_t dstId) req["dstId"].set(dstId); int ret = RESTClient::send(m_controlChData.address(), m_controlChData.port(), m_controlChData.password(), - HTTP_PUT, PUT_RELEASE_TG, req, m_debug); + HTTP_PUT, PUT_RELEASE_TG, req, m_controlChData.ssl(), m_debug); if (ret != network::rest::http::HTTPPayload::StatusType::OK) { ::LogError(LOG_P25, "failed to notify the CC %s:%u of the release of, dstId = %u", m_controlChData.address().c_str(), m_controlChData.port(), dstId); } @@ -1491,7 +1491,7 @@ void Control::notifyCC_TouchGrant(uint32_t dstId) req["dstId"].set(dstId); int ret = RESTClient::send(m_controlChData.address(), m_controlChData.port(), m_controlChData.password(), - HTTP_PUT, PUT_TOUCH_TG, req, m_debug); + HTTP_PUT, PUT_TOUCH_TG, req, m_controlChData.ssl(), m_debug); if (ret != network::rest::http::HTTPPayload::StatusType::OK) { ::LogError(LOG_P25, "failed to notify the CC %s:%u of the touch of, dstId = %u", m_controlChData.address().c_str(), m_controlChData.port(), dstId); } diff --git a/src/host/p25/packet/ControlSignaling.cpp b/src/host/p25/packet/ControlSignaling.cpp index bff12691..d53dee6b 100644 --- a/src/host/p25/packet/ControlSignaling.cpp +++ b/src/host/p25/packet/ControlSignaling.cpp @@ -2300,7 +2300,7 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_ req["dstId"].set(dstId); int ret = RESTClient::send(voiceChData.address(), voiceChData.port(), voiceChData.password(), - HTTP_PUT, PUT_PERMIT_TG, req, m_p25->m_debug); + HTTP_PUT, PUT_PERMIT_TG, req, voiceChData.ssl(), m_p25->m_debug); if (ret != network::rest::http::HTTPPayload::StatusType::OK) { ::LogError((net) ? LOG_NET : LOG_RF, P25_TSDU_STR ", TSBK_IOSP_GRP_VCH (Group Voice Channel Grant), failed to permit TG for use, chNo = %u", chNo); m_p25->m_affiliations.releaseGrant(dstId, false); @@ -2354,7 +2354,7 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_ req["dstId"].set(dstId); int ret = RESTClient::send(voiceChData.address(), voiceChData.port(), voiceChData.password(), - HTTP_PUT, PUT_PERMIT_TG, req, m_p25->m_debug); + HTTP_PUT, PUT_PERMIT_TG, req, voiceChData.ssl(), m_p25->m_debug); if (ret != network::rest::http::HTTPPayload::StatusType::OK) { ::LogError((net) ? LOG_NET : LOG_RF, P25_TSDU_STR ", TSBK_IOSP_UU_VCH (Unit-to-Unit Voice Channel Grant), failed to permit TG for use, chNo = %u", chNo); m_p25->m_affiliations.releaseGrant(dstId, false); diff --git a/src/monitor/InhibitSubscriberWnd.h b/src/monitor/InhibitSubscriberWnd.h index 82c078a3..7c41176e 100644 --- a/src/monitor/InhibitSubscriberWnd.h +++ b/src/monitor/InhibitSubscriberWnd.h @@ -7,7 +7,7 @@ * @package DVM / Host Monitor Software * @license GPLv2 License (https://opensource.org/licenses/GPL-2.0) * -* Copyright (C) 2023 Bryan Biedenkapp, N2PLL +* Copyright (C) 2023,2024 Bryan Biedenkapp, N2PLL * */ #if !defined(__INHIBIT_SUBSCRIBER_WND_H__) @@ -132,7 +132,7 @@ private: // callback REST API int ret = RESTClient::send(m_selectedCh.address(), m_selectedCh.port(), m_selectedCh.password(), - HTTP_PUT, method, req, g_debug); + HTTP_PUT, method, req, m_selectedCh.ssl(), g_debug); if (ret != network::rest::http::HTTPPayload::StatusType::OK) { ::LogError(LOG_HOST, "failed to send request %s to %s:%u", method.c_str(), m_selectedCh.address().c_str(), m_selectedCh.port()); } diff --git a/src/monitor/MonitorMainWnd.h b/src/monitor/MonitorMainWnd.h index d28f662a..52eff94e 100644 --- a/src/monitor/MonitorMainWnd.h +++ b/src/monitor/MonitorMainWnd.h @@ -168,10 +168,11 @@ private: std::string restApiAddress = channel["restAddress"].as("127.0.0.1"); uint16_t restApiPort = (uint16_t)channel["restPort"].as(REST_API_DEFAULT_PORT); std::string restApiPassword = channel["restPassword"].as(); + bool restSsl = channel["restSsl"].as(false); ::LogInfoEx(LOG_HOST, "Channel REST API Adddress %s:%u", restApiAddress.c_str(), restApiPort); - VoiceChData data = VoiceChData(0U, 0U, restApiAddress, restApiPort, restApiPassword); + VoiceChData data = VoiceChData(0U, 0U, restApiAddress, restApiPort, restApiPassword, restSsl); NodeStatusWnd* wnd = new NodeStatusWnd(this); wnd->setChData(data); diff --git a/src/monitor/NodeStatusWnd.h b/src/monitor/NodeStatusWnd.h index fe883feb..3f8d7c77 100644 --- a/src/monitor/NodeStatusWnd.h +++ b/src/monitor/NodeStatusWnd.h @@ -7,7 +7,7 @@ * @package DVM / Host Monitor Software * @license GPLv2 License (https://opensource.org/licenses/GPL-2.0) * -* Copyright (C) 2023 Bryan Biedenkapp, N2PLL +* Copyright (C) 2023,2024 Bryan Biedenkapp, N2PLL * */ #if !defined(__NODE_STATUS_WND_H__) @@ -264,7 +264,7 @@ private: json::object rsp = json::object(); int ret = RESTClient::send(m_chData.address(), m_chData.port(), m_chData.password(), - HTTP_GET, GET_STATUS, req, rsp, g_debug); + HTTP_GET, GET_STATUS, req, rsp, m_chData.ssl(), g_debug); if (ret != network::rest::http::HTTPPayload::StatusType::OK) { ::LogError(LOG_HOST, "failed to get status for %s:%u, chNo = %u", m_chData.address().c_str(), m_chData.port(), m_channelNo); ++m_failCnt; @@ -395,7 +395,7 @@ private: // callback REST API to get status of the channel we represent json::object req = json::object(); int ret = RESTClient::send(m_chData.address(), m_chData.port(), m_chData.password(), - HTTP_GET, GET_STATUS, req, g_debug); + HTTP_GET, GET_STATUS, req, m_chData.ssl(), g_debug); if (ret == network::rest::http::HTTPPayload::StatusType::OK) { m_failed = false; m_failCnt = 0U; diff --git a/src/monitor/PageSubscriberWnd.h b/src/monitor/PageSubscriberWnd.h index eff6a718..08aa31a3 100644 --- a/src/monitor/PageSubscriberWnd.h +++ b/src/monitor/PageSubscriberWnd.h @@ -7,7 +7,7 @@ * @package DVM / Host Monitor Software * @license GPLv2 License (https://opensource.org/licenses/GPL-2.0) * -* Copyright (C) 2023 Bryan Biedenkapp, N2PLL +* Copyright (C) 2023,2024 Bryan Biedenkapp, N2PLL * */ #if !defined(__PAGE_SUBSCRIBER_WND_H__) @@ -132,7 +132,7 @@ private: // callback REST API int ret = RESTClient::send(m_selectedCh.address(), m_selectedCh.port(), m_selectedCh.password(), - HTTP_PUT, method, req, g_debug); + HTTP_PUT, method, req, m_selectedCh.ssl(), g_debug); if (ret != network::rest::http::HTTPPayload::StatusType::OK) { ::LogError(LOG_HOST, "failed to send request %s to %s:%u", method.c_str(), m_selectedCh.address().c_str(), m_selectedCh.port()); } diff --git a/src/monitor/RadioCheckSubscriberWnd.h b/src/monitor/RadioCheckSubscriberWnd.h index 4ea737fd..805cf679 100644 --- a/src/monitor/RadioCheckSubscriberWnd.h +++ b/src/monitor/RadioCheckSubscriberWnd.h @@ -7,7 +7,7 @@ * @package DVM / Host Monitor Software * @license GPLv2 License (https://opensource.org/licenses/GPL-2.0) * -* Copyright (C) 2023 Bryan Biedenkapp, N2PLL +* Copyright (C) 2023,2024 Bryan Biedenkapp, N2PLL * */ #if !defined(__RADIO_CHECK_SUBSCRIBER_WND_H__) @@ -132,7 +132,7 @@ private: // callback REST API int ret = RESTClient::send(m_selectedCh.address(), m_selectedCh.port(), m_selectedCh.password(), - HTTP_PUT, method, req, g_debug); + HTTP_PUT, method, req, m_selectedCh.ssl(), g_debug); if (ret != network::rest::http::HTTPPayload::StatusType::OK) { ::LogError(LOG_HOST, "failed to send request %s to %s:%u", method.c_str(), m_selectedCh.address().c_str(), m_selectedCh.port()); } diff --git a/src/monitor/TransmitWndBase.h b/src/monitor/TransmitWndBase.h index 6895fc70..a5246057 100644 --- a/src/monitor/TransmitWndBase.h +++ b/src/monitor/TransmitWndBase.h @@ -7,7 +7,7 @@ * @package DVM / Host Monitor Software * @license GPLv2 License (https://opensource.org/licenses/GPL-2.0) * -* Copyright (C) 2023 Bryan Biedenkapp, N2PLL +* Copyright (C) 2023,2024 Bryan Biedenkapp, N2PLL * */ #if !defined(__TRANSMIT_WND_BASE_H__) @@ -100,7 +100,7 @@ protected: json::object rsp = json::object(); int ret = RESTClient::send(m_selectedCh.address(), m_selectedCh.port(), m_selectedCh.password(), - HTTP_GET, GET_STATUS, req, rsp, g_debug); + HTTP_GET, GET_STATUS, req, rsp, m_selectedCh.ssl(), g_debug); if (ret != network::rest::http::HTTPPayload::StatusType::OK) { ::LogError(LOG_HOST, "failed to get status for %s:%u", m_selectedCh.address().c_str(), m_selectedCh.port()); } diff --git a/src/monitor/UninhibitSubscriberWnd.h b/src/monitor/UninhibitSubscriberWnd.h index 4be62d1b..036dec05 100644 --- a/src/monitor/UninhibitSubscriberWnd.h +++ b/src/monitor/UninhibitSubscriberWnd.h @@ -7,7 +7,7 @@ * @package DVM / Host Monitor Software * @license GPLv2 License (https://opensource.org/licenses/GPL-2.0) * -* Copyright (C) 2023 Bryan Biedenkapp, N2PLL +* Copyright (C) 2023,2024 Bryan Biedenkapp, N2PLL * */ #if !defined(__UNINHIBIT_SUBSCRIBER_WND_H__) @@ -132,7 +132,7 @@ private: // callback REST API int ret = RESTClient::send(m_selectedCh.address(), m_selectedCh.port(), m_selectedCh.password(), - HTTP_PUT, method, req, g_debug); + HTTP_PUT, method, req, m_selectedCh.ssl(), g_debug); if (ret != network::rest::http::HTTPPayload::StatusType::OK) { ::LogError(LOG_HOST, "failed to send request %s to %s:%u", method.c_str(), m_selectedCh.address().c_str(), m_selectedCh.port()); } diff --git a/src/remote/RESTClient.cpp b/src/remote/RESTClient.cpp index 51e620f5..298896d7 100644 --- a/src/remote/RESTClient.cpp +++ b/src/remote/RESTClient.cpp @@ -7,13 +7,14 @@ * @package DVM / Remote Command Client * @license GPLv2 License (https://opensource.org/licenses/GPL-2.0) * -* Copyright (C) 2023 Bryan Biedenkapp, N2PLL +* Copyright (C) 2023,2024 Bryan Biedenkapp, N2PLL * */ #include "Defines.h" #include "common/edac/SHA256.h" #include "common/network/json/json.h" #include "common/network/rest/http/HTTPClient.h" +#include "common/network/rest/http/SecureHTTPClient.h" #include "common/network/rest/RequestDispatcher.h" #include "common/Thread.h" #include "common/Log.h" @@ -50,6 +51,7 @@ bool RESTClient::m_responseAvailable = false; HTTPPayload RESTClient::m_response; bool RESTClient::m_console = false; +bool RESTClient::m_enableSSL = false; bool RESTClient::m_debug = false; // --------------------------------------------------------------------------- @@ -95,8 +97,9 @@ bool parseResponseBody(const HTTPPayload& response, json::object& obj) /// Network Hostname/IP address to connect to. /// Network port number. /// Authentication password. +/// /// Flag indicating whether debug is enabled. -RESTClient::RESTClient(const std::string& address, uint32_t port, const std::string& password, bool debug) : +RESTClient::RESTClient(const std::string& address, uint32_t port, const std::string& password, bool enableSSL, bool debug) : m_address(address), m_port(port), m_password(password) @@ -105,6 +108,7 @@ RESTClient::RESTClient(const std::string& address, uint32_t port, const std::str assert(port > 0U); m_console = true; + m_enableSSL = enableSSL; m_debug = debug; } @@ -136,7 +140,7 @@ int RESTClient::send(const std::string method, const std::string endpoint, json: /// EXIT_SUCCESS, if command was sent, otherwise EXIT_FAILURE. int RESTClient::send(const std::string method, const std::string endpoint, json::object payload, json::object& response) { - return send(m_address, m_port, m_password, method, endpoint, payload, response, m_debug); + return send(m_address, m_port, m_password, method, endpoint, payload, response, m_enableSSL, m_debug); } /// @@ -148,13 +152,14 @@ int RESTClient::send(const std::string method, const std::string endpoint, json: /// REST API method. /// REST API endpoint. /// REST API endpoint payload. +/// /// Flag indicating whether debug is enabled. /// EXIT_SUCCESS, if command was sent, otherwise EXIT_FAILURE. int RESTClient::send(const std::string& address, uint32_t port, const std::string& password, const std::string method, - const std::string endpoint, json::object payload, bool debug) + const std::string endpoint, json::object payload, bool enableSSL, bool debug) { json::object rsp = json::object(); - return send(address, port, password, method, endpoint, payload, rsp, debug); + return send(address, port, password, method, endpoint, payload, rsp, enableSSL, debug); } /// @@ -167,10 +172,11 @@ int RESTClient::send(const std::string& address, uint32_t port, const std::strin /// REST API endpoint. /// REST API endpoint payload. /// REST API endpoint response. +/// /// Flag indicating whether debug is enabled. /// EXIT_SUCCESS, if command was sent, otherwise EXIT_FAILURE. int RESTClient::send(const std::string& address, uint32_t port, const std::string& password, const std::string method, - const std::string endpoint, json::object payload, json::object& response, bool debug) + const std::string endpoint, json::object payload, json::object& response, bool enableSSL, bool debug) { if (address.empty()) { return ERRNO_NO_ADDRESS; @@ -183,20 +189,37 @@ int RESTClient::send(const std::string& address, uint32_t port, const std::strin } int ret = EXIT_SUCCESS; + m_enableSSL = enableSSL; m_debug = debug; typedef network::rest::BasicRequestDispatcher RESTDispatcherType; RESTDispatcherType m_dispatcher(RESTClient::responseHandler); HTTPClient* client = nullptr; +#if defined(ENABLE_TCP_SSL) + SecureHTTPClient* sslClient = nullptr; +#endif // ENABLE_TCP_SSL try { // setup HTTP client for authentication payload - client = new HTTPClient(address, port); - if (!client->open()) { - delete client; - return ERRNO_SOCK_OPEN; +#if defined(ENABLE_TCP_SSL) + if (m_enableSSL) { + sslClient = new SecureHTTPClient(address, port); + if (!sslClient->open()) { + delete sslClient; + return ERRNO_SOCK_OPEN; + } + sslClient->setHandler(m_dispatcher); + } else { +#endif // ENABLE_TCP_SSL + client = new HTTPClient(address, port); + if (!client->open()) { + delete client; + return ERRNO_SOCK_OPEN; + } + client->setHandler(m_dispatcher); +#if defined(ENABLE_TCP_SSL) } - client->setHandler(m_dispatcher); +#endif // ENABLE_TCP_SSL // generate password SHA hash size_t size = password.size(); @@ -227,12 +250,29 @@ int RESTClient::send(const std::string& address, uint32_t port, const std::strin HTTPPayload httpPayload = HTTPPayload::requestPayload(HTTP_PUT, "/auth"); httpPayload.payload(request); - client->request(httpPayload); +#if defined(ENABLE_TCP_SSL) + if (m_enableSSL) { + sslClient->request(httpPayload); + } else { +#endif // ENABLE_TCP_SSL + client->request(httpPayload); +#if defined(ENABLE_TCP_SSL) + } +#endif // ENABLE_TCP_SSL // wait for response and parse if (wait()) { - client->close(); - delete client; +#if defined(ENABLE_TCP_SSL) + if (m_enableSSL) { + sslClient->close(); + delete sslClient; + } else { +#endif // ENABLE_TCP_SSL + client->close(); + delete client; +#if defined(ENABLE_TCP_SSL) + } +#endif // ENABLE_TCP_SSL return ERRNO_API_CALL_TIMEOUT; } @@ -247,30 +287,80 @@ int RESTClient::send(const std::string& address, uint32_t port, const std::strin token = rsp["token"].get(); } else { - client->close(); - delete client; +#if defined(ENABLE_TCP_SSL) + if (m_enableSSL) { + sslClient->close(); + delete sslClient; + } else { +#endif // ENABLE_TCP_SSL + client->close(); + delete client; +#if defined(ENABLE_TCP_SSL) + } +#endif // ENABLE_TCP_SSL return ERRNO_BAD_AUTH_RESPONSE; } - client->close(); - delete client; +#if defined(ENABLE_TCP_SSL) + if (m_enableSSL) { + sslClient->close(); + delete sslClient; + } else { +#endif // ENABLE_TCP_SSL + client->close(); + delete client; +#if defined(ENABLE_TCP_SSL) + } +#endif // ENABLE_TCP_SSL // reset the HTTP client and setup for actual payload request - client = new HTTPClient(address, port); - if (!client->open()) - return ERRNO_SOCK_OPEN; - client->setHandler(m_dispatcher); +#if defined(ENABLE_TCP_SSL) + if (m_enableSSL) { + sslClient = new SecureHTTPClient(address, port); + if (!sslClient->open()) { + delete sslClient; + return ERRNO_SOCK_OPEN; + } + sslClient->setHandler(m_dispatcher); + } else { +#endif // ENABLE_TCP_SSL + client = new HTTPClient(address, port); + if (!client->open()) { + delete client; + return ERRNO_SOCK_OPEN; + } + client->setHandler(m_dispatcher); +#if defined(ENABLE_TCP_SSL) + } +#endif // ENABLE_TCP_SSL // send actual API request httpPayload = HTTPPayload::requestPayload(method, endpoint); httpPayload.headers.add("X-DVM-Auth-Token", token); httpPayload.payload(payload); - client->request(httpPayload); +#if defined(ENABLE_TCP_SSL) + if (m_enableSSL) { + sslClient->request(httpPayload); + } else { +#endif // ENABLE_TCP_SSL + client->request(httpPayload); +#if defined(ENABLE_TCP_SSL) + } +#endif // ENABLE_TCP_SSL // wait for response and parse if (wait()) { - client->close(); - delete client; +#if defined(ENABLE_TCP_SSL) + if (m_enableSSL) { + sslClient->close(); + delete sslClient; + } else { +#endif // ENABLE_TCP_SSL + client->close(); + delete client; +#if defined(ENABLE_TCP_SSL) + } +#endif // ENABLE_TCP_SSL return ERRNO_API_CALL_TIMEOUT; } @@ -289,13 +379,32 @@ int RESTClient::send(const std::string& address, uint32_t port, const std::strin } } - client->close(); - delete client; +#if defined(ENABLE_TCP_SSL) + if (m_enableSSL) { + sslClient->close(); + delete sslClient; + } else { +#endif // ENABLE_TCP_SSL + client->close(); + delete client; +#if defined(ENABLE_TCP_SSL) + } +#endif // ENABLE_TCP_SSL } catch (std::exception&) { - if (client != nullptr) { - delete client; +#if defined(ENABLE_TCP_SSL) + if (m_enableSSL) { + if (sslClient != nullptr) { + delete sslClient; + } + } else { +#endif // ENABLE_TCP_SSL + if (client != nullptr) { + delete client; + } +#if defined(ENABLE_TCP_SSL) } +#endif // ENABLE_TCP_SSL return ERRNO_INTERNAL_ERROR; } diff --git a/src/remote/RESTClient.h b/src/remote/RESTClient.h index 246a3d18..28b20496 100644 --- a/src/remote/RESTClient.h +++ b/src/remote/RESTClient.h @@ -7,7 +7,7 @@ * @package DVM / Remote Command Client * @license GPLv2 License (https://opensource.org/licenses/GPL-2.0) * -* Copyright (C) 2023 Bryan Biedenkapp, N2PLL +* Copyright (C) 2023,2024 Bryan Biedenkapp, N2PLL * */ #if !defined(__REST_CLIENT_H__) @@ -28,7 +28,7 @@ class HOST_SW_API RESTClient { public: /// Initializes a new instance of the RESTClient class. - RESTClient(const std::string& address, uint32_t port, const std::string& password, bool debug); + RESTClient(const std::string& address, uint32_t port, const std::string& password, bool enableSSL, bool debug); /// Finalizes a instance of the RESTClient class. ~RESTClient(); @@ -39,10 +39,10 @@ public: /// Sends remote control command to the specified modem. static int send(const std::string& address, uint32_t port, const std::string& password, const std::string method, - const std::string endpoint, json::object payload, bool debug = false); + const std::string endpoint, json::object payload, bool enableSSL, bool debug = false); /// Sends remote control command to the specified modem. static int send(const std::string& address, uint32_t port, const std::string& password, const std::string method, - const std::string endpoint, json::object payload, json::object& response, bool debug = false); + const std::string endpoint, json::object payload, json::object& response, bool enableSSL, bool debug = false); private: typedef network::rest::http::HTTPPayload HTTPPayload; @@ -61,6 +61,7 @@ private: static bool m_responseAvailable; static HTTPPayload m_response; + static bool m_enableSSL; static bool m_debug; }; diff --git a/src/remote/RESTClientMain.cpp b/src/remote/RESTClientMain.cpp index 93521851..1bac4df1 100644 --- a/src/remote/RESTClientMain.cpp +++ b/src/remote/RESTClientMain.cpp @@ -7,7 +7,7 @@ * @package DVM / Remote Command Client * @license GPLv2 License (https://opensource.org/licenses/GPL-2.0) * -* Copyright (C) 2023 Bryan Biedenkapp, N2PLL +* Copyright (C) 2023,2024 Bryan Biedenkapp, N2PLL * */ #include "remote/RESTClient.h" @@ -122,6 +122,7 @@ static std::string g_progExe = std::string(__EXE_NAME__); static std::string g_remoteAddress = std::string("127.0.0.1"); static uint32_t g_remotePort = REST_API_DEFAULT_PORT; static std::string g_remotePassword = std::string(); +static bool g_enableSSL = false; static bool g_debug = false; // --------------------------------------------------------------------------- @@ -155,7 +156,7 @@ void usage(const char* message, const char* arg) } ::fprintf(stdout, - "usage: %s [-dvh]" + "usage: %s [-dvhs]" "[-a
]" "[-p ]" "[-P ]" @@ -169,6 +170,8 @@ void usage(const char* message, const char* arg) " -p remote modem command port\n" " -P remote modem authentication password\n" "\n" + " -s use HTTPS/SSL\n" + "\n" " -- stop handling options\n", g_progExe.c_str()); @@ -299,6 +302,10 @@ int checkArgs(int argc, char* argv[]) p += 2; } + else if (IS("-s")) { + ++p; + g_enableSSL = true; + } else if (IS("-d")) { ++p; g_debug = true; @@ -398,7 +405,7 @@ int main(int argc, char** argv) return 1; } - RESTClient* client = new RESTClient(g_remoteAddress, g_remotePort, g_remotePassword, g_debug); + RESTClient* client = new RESTClient(g_remoteAddress, g_remotePort, g_remotePassword, g_enableSSL, g_debug); int retCode = EXIT_SUCCESS; std::vector args = std::vector();