From 529d5c1e830b84c019cafe704fedc41348f480f3 Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Sun, 19 Mar 2023 13:30:35 -0400 Subject: [PATCH] implement the majority of REST API support (this commit breaks RCON and any RCON-based tools [like dvmcmd]) (NOTE: not *all* RCON commands are implemented as REST API yet); --- Defines.h | 14 +- Log.h | 2 +- dmr/Control.cpp | 37 +- dmr/Control.h | 6 +- host/Host.cpp | 91 +- host/Host.h | 8 +- lookups/AffiliationLookup.h | 12 +- modem/Modem.h | 6 +- network/Network.cpp | 16 +- network/Network.h | 8 +- network/RESTAPI.cpp | 1536 ++++++++++++++++++++ network/RESTAPI.h | 230 +++ network/RemoteControl.cpp | 1692 ---------------------- network/RemoteControl.h | 214 --- network/rest/RequestDispatcher.h | 19 +- network/rest/http/Connection.h | 19 +- network/rest/http/HTTPHeader.h | 71 - network/rest/http/HTTPHeaders.h | 132 ++ network/rest/http/HTTPReply.cpp | 14 +- network/rest/http/HTTPReply.h | 4 +- network/rest/http/HTTPRequest.h | 11 +- network/rest/http/HTTPRequestHandler.cpp | 8 +- network/rest/http/HTTPRequestLexer.cpp | 24 +- network/rest/http/HTTPRequestLexer.h | 12 + nxdn/Control.h | 4 +- p25/Control.h | 1 - p25/packet/Trunk.h | 1 - remote/RemoteCommand.cpp | 2 +- remote/RemoteCommandMain.cpp | 2 +- 29 files changed, 2089 insertions(+), 2107 deletions(-) create mode 100644 network/RESTAPI.cpp create mode 100644 network/RESTAPI.h delete mode 100644 network/RemoteControl.cpp delete mode 100644 network/RemoteControl.h delete mode 100644 network/rest/http/HTTPHeader.h create mode 100644 network/rest/http/HTTPHeaders.h diff --git a/Defines.h b/Defines.h index 60cc7b37..16c2143a 100644 --- a/Defines.h +++ b/Defines.h @@ -36,6 +36,7 @@ #include #include #include +#include #include #if !defined(ENABLE_DMR) && !defined(ENABLE_P25) && !defined(ENABLE_NXDN) @@ -134,7 +135,7 @@ typedef unsigned long long ulong64_t; const uint32_t REMOTE_MODEM_PORT = 3334; const uint32_t TRAFFIC_DEFAULT_PORT = 62031; -const uint32_t RCON_DEFAULT_PORT = 9990; +const uint32_t REST_API_DEFAULT_PORT = 9990; const uint8_t BIT_MASK_TABLE[] = { 0x80U, 0x40U, 0x20U, 0x10U, 0x08U, 0x04U, 0x02U, 0x01U }; @@ -224,6 +225,17 @@ inline std::string __IP_FROM_ULONG(const ulong64_t& value) { return ss.str(); } +/// +/// Helper to lower-case an input string. +/// +/// +/// +inline std::string strtolower(const std::string value) { + std::string v = value; + std::transform(v.begin(), v.end(), v.begin(), ::tolower); + return v; +} + // --------------------------------------------------------------------------- // Macros // --------------------------------------------------------------------------- diff --git a/Log.h b/Log.h index 26b8fbcc..3094f12f 100644 --- a/Log.h +++ b/Log.h @@ -40,7 +40,7 @@ // --------------------------------------------------------------------------- #define LOG_HOST "HOST" -#define LOG_RCON "RCON" +#define LOG_REST "RESTAPI" #define LOG_MODEM "MODEM" #define LOG_RF "RF" #define LOG_NET "NET" diff --git a/dmr/Control.cpp b/dmr/Control.cpp index 9ae44af4..66d6c8a1 100644 --- a/dmr/Control.cpp +++ b/dmr/Control.cpp @@ -206,7 +206,7 @@ void Control::setCCRunning(bool ccRunning) m_slot2->setCCRunning(ccRunning); break; default: - LogError(LOG_NET, "DMR, invalid slot, TSCC disabled, slotNo = %u", m_tsccSlotNo); + LogError(LOG_DMR, "DMR, invalid slot, TSCC disabled, slotNo = %u", m_tsccSlotNo); break; } } @@ -226,7 +226,7 @@ void Control::setCCHalted(bool ccHalted) m_slot2->setCCHalted(ccHalted); break; default: - LogError(LOG_NET, "DMR, invalid slot, TSCC disabled, slotNo = %u", m_tsccSlotNo); + LogError(LOG_DMR, "DMR, invalid slot, TSCC disabled, slotNo = %u", m_tsccSlotNo); break; } } @@ -285,7 +285,7 @@ bool Control::processFrame(uint32_t slotNo, uint8_t *data, uint32_t len) case 2U: return m_slot2->processFrame(data, len); default: - LogError(LOG_NET, "DMR, invalid slot, slotNo = %u", slotNo); + LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", slotNo); return false; } } @@ -305,7 +305,7 @@ uint32_t Control::getFrame(uint32_t slotNo, uint8_t* data) case 2U: return m_slot2->getFrame(data); default: - LogError(LOG_NET, "DMR, invalid slot, slotNo = %u", slotNo); + LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", slotNo); return 0U; } } @@ -328,7 +328,7 @@ void Control::clock() m_slot2->processNetwork(data); break; default: - LogError(LOG_NET, "DMR, invalid slot, slotNo = %u", slotNo); + LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", slotNo); break; } } @@ -353,9 +353,28 @@ void Control::permittedTG(uint32_t dstId, uint8_t slot) m_slot2->permittedTG(dstId); break; default: - LogError(LOG_NET, "DMR, invalid slot, slotNo = %u", slot); + LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", slot); + break; + } +} + +/// +/// Gets instance of the AffiliationLookup class. +/// +dmr::lookups::DMRAffiliationLookup Control::affiliations() +{ + switch (m_tsccSlotNo) { + case 1U: + return m_slot1->m_affiliations; + case 2U: + return m_slot2->m_affiliations; + break; + default: + LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", m_tsccSlotNo); break; } + + return 0; // ?? } /// @@ -372,7 +391,7 @@ Slot* Control::getTSCCSlot() const return m_slot2; break; default: - LogError(LOG_NET, "DMR, invalid slot, TSCC disabled, slotNo = %u", m_tsccSlotNo); + LogError(LOG_DMR, "DMR, invalid slot, TSCC disabled, slotNo = %u", m_tsccSlotNo); return nullptr; } } @@ -394,7 +413,7 @@ void Control::writeRF_Ext_Func(uint32_t slotNo, uint32_t func, uint32_t arg, uin m_slot2->control()->writeRF_Ext_Func(func, arg, dstId); break; default: - LogError(LOG_NET, "DMR, invalid slot, slotNo = %u", slotNo); + LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", slotNo); break; } } @@ -415,7 +434,7 @@ void Control::writeRF_Call_Alrt(uint32_t slotNo, uint32_t srcId, uint32_t dstId) m_slot2->control()->writeRF_Call_Alrt(srcId, dstId); break; default: - LogError(LOG_NET, "DMR, invalid slot, slotNo = %u", slotNo); + LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", slotNo); break; } } diff --git a/dmr/Control.h b/dmr/Control.h index 07d72448..d9d79c10 100644 --- a/dmr/Control.h +++ b/dmr/Control.h @@ -33,15 +33,14 @@ #include "Defines.h" #include "dmr/data/Data.h" +#include "dmr/lookups/DMRAffiliationLookup.h" #include "dmr/Slot.h" #include "modem/Modem.h" #include "network/BaseNetwork.h" -#include "network/RemoteControl.h" #include "lookups/RSSIInterpolator.h" #include "lookups/IdenTableLookup.h" #include "lookups/RadioIdLookup.h" #include "lookups/TalkgroupIdLookup.h" -#include "lookups/AffiliationLookup.h" #include "yaml/Yaml.h" namespace dmr @@ -95,6 +94,9 @@ namespace dmr /// Permits a TGID on a non-authoritative host. void permittedTG(uint32_t dstId, uint8_t slot); + /// Gets instance of the DMRAffiliationLookup class. + lookups::DMRAffiliationLookup affiliations(); + /// Helper to return the slot carrying the TSCC. Slot* getTSCCSlot() const; diff --git a/host/Host.cpp b/host/Host.cpp index 8c2f1399..67ea36e7 100644 --- a/host/Host.cpp +++ b/host/Host.cpp @@ -143,7 +143,7 @@ Host::Host(const std::string& confFile) : m_controlPermitTG(false), m_activeTickDelay(5U), m_idleTickDelay(5U), - m_remoteControl(nullptr) + m_RESTAPI(nullptr) { UDPSocket::startup(); } @@ -722,8 +722,8 @@ int Host::run() setState(STATE_IDLE); } - if (m_remoteControl != nullptr) { - m_remoteControl->setProtocols(dmr.get(), p25.get(), nxdn.get()); + if (m_RESTAPI != nullptr) { + m_RESTAPI->setProtocols(dmr.get(), p25.get(), nxdn.get()); } ::LogInfoEx(LOG_HOST, "Host is performing late initialization and warmup"); @@ -1335,14 +1335,6 @@ int Host::run() nxdn->clock(ms); #endif // defined(ENABLE_NXDN) - // ------------------------------------------------------ - // -- Remote Control Processing -- - // ------------------------------------------------------ - - if (m_remoteControl != nullptr) { - m_remoteControl->process(this, dmr.get(), p25.get(), nxdn.get()); - } - // ------------------------------------------------------ // -- Timer Clocking -- // ------------------------------------------------------ @@ -1848,13 +1840,13 @@ bool Host::readParams() chNo = 4095U; } - std::string rconAddress = channel["rconAddress"].as("127.0.0.1"); - uint16_t rconPort = (uint16_t)channel["rconPort"].as(RCON_DEFAULT_PORT); - std::string rconPassword = channel["rconPassword"].as(); + std::string restApiAddress = channel["rconAddress"].as("127.0.0.1"); + uint16_t restApiPort = (uint16_t)channel["rconPort"].as(REST_API_DEFAULT_PORT); + std::string restApiPassword = channel["rconPassword"].as(); - ::LogInfoEx(LOG_HOST, "Voice Channel Id %u Channel No $%04X RCON Adddress %s:%u", m_channelId, chNo, rconAddress.c_str(), rconPort); + ::LogInfoEx(LOG_HOST, "Voice Channel Id %u Channel No $%04X REST API Adddress %s:%u", m_channelId, chNo, restApiAddress.c_str(), restApiPort); - VoiceChData data = VoiceChData(chNo, rconAddress, rconPort, rconPassword); + VoiceChData data = VoiceChData(chNo, restApiAddress, restApiPort, restApiPassword); m_voiceChData[chNo] = data; m_voiceChNo.push_back(chNo); } @@ -2260,20 +2252,20 @@ bool Host::createNetwork() { yaml::Node networkConf = m_conf["network"]; bool netEnable = networkConf["enable"].as(false); - bool rconEnable = networkConf["rconEnable"].as(false); + bool restApiEnable = networkConf["rconEnable"].as(false); - // dump out if both networking and RCON are disabled - if (!netEnable && !rconEnable) { + // dump out if both networking and REST API are disabled + if (!netEnable && !restApiEnable) { return true; } std::string address = networkConf["address"].as(); uint16_t port = (uint16_t)networkConf["port"].as(TRAFFIC_DEFAULT_PORT); uint16_t local = (uint16_t)networkConf["local"].as(0U); - std::string rconAddress = networkConf["rconAddress"].as("127.0.0.1"); - uint16_t rconPort = (uint16_t)networkConf["rconPort"].as(RCON_DEFAULT_PORT); - std::string rconPassword = networkConf["rconPassword"].as(); - bool rconDebug = networkConf["rconDebug"].as(false); + std::string restApiAddress = networkConf["rconAddress"].as("127.0.0.1"); + uint16_t restApiPort = (uint16_t)networkConf["rconPort"].as(REST_API_DEFAULT_PORT); + std::string restApiPassword = networkConf["rconPassword"].as(); + bool restApiDebug = networkConf["rconDebug"].as(false); uint32_t id = networkConf["id"].as(0U); uint32_t jitter = networkConf["talkgroupHang"].as(360U); std::string password = networkConf["password"].as(); @@ -2284,11 +2276,16 @@ bool Host::createNetwork() bool updateLookup = networkConf["updateLookups"].as(false); bool debug = networkConf["debug"].as(false); - if (rconPassword.length() > 64) { - std::string password = rconPassword; - rconPassword = password.substr(0, 64); + if (restApiPassword.length() > 64) { + std::string password = restApiPassword; + restApiPassword = password.substr(0, 64); + + ::LogWarning(LOG_HOST, "REST API password is too long; truncating to the first 64 characters."); + } - ::LogWarning(LOG_HOST, "RCON password is too long; truncating to the first 64 characters."); + if (restApiPassword.empty() && restApiEnable) { + ::LogWarning(LOG_HOST, "REST API password not provided; REST API disabled."); + restApiEnable = false; } IdenTable entry = m_idenTable->find(m_channelId); @@ -2314,13 +2311,13 @@ bool Host::createNetwork() LogInfo(" Debug: yes"); } } - LogInfo(" RCON Enabled: %s", rconEnable ? "yes" : "no"); - if (rconEnable) { - LogInfo(" RCON Address: %s", rconAddress.c_str()); - LogInfo(" RCON Port: %u", rconPort); + LogInfo(" REST API Enabled: %s", restApiEnable ? "yes" : "no"); + if (restApiEnable) { + LogInfo(" REST API Address: %s", restApiAddress.c_str()); + LogInfo(" REST API Port: %u", restApiPort); - if (rconDebug) { - LogInfo(" RCON Debug: yes"); + if (restApiDebug) { + LogInfo(" REST API Debug: yes"); } } @@ -2331,8 +2328,8 @@ bool Host::createNetwork() m_network->setLookups(m_ridLookup, m_tidLookup); m_network->setMetadata(m_identity, m_rxFrequency, m_txFrequency, entry.txOffsetMhz(), entry.chBandwidthKhz(), m_channelId, m_channelNo, m_power, m_latitude, m_longitude, m_height, m_location); - if (rconEnable) { - m_network->setRconData(rconPassword, rconPort); + if (restApiEnable) { + m_network->setRESTAPIData(restApiPassword, restApiPort); } bool ret = m_network->open(); @@ -2348,19 +2345,19 @@ bool Host::createNetwork() } // initialize network remote command - if (rconEnable) { - m_remoteControl = new RemoteControl(rconAddress, rconPort, rconPassword, this, rconDebug); - m_remoteControl->setLookups(m_ridLookup, m_tidLookup); - bool ret = m_remoteControl->open(); + if (restApiEnable) { + m_RESTAPI = new RESTAPI(restApiAddress, restApiPort, restApiPassword, this, restApiDebug); + m_RESTAPI->setLookups(m_ridLookup, m_tidLookup); + bool ret = m_RESTAPI->open(); if (!ret) { - delete m_remoteControl; - m_remoteControl = nullptr; - LogError(LOG_HOST, "failed to initialize remote command networking! remote command control will be unavailable!"); - // remote command control failing isn't fatal -- we'll allow this to return normally + delete m_RESTAPI; + m_RESTAPI = nullptr; + LogError(LOG_HOST, "failed to initialize REST API networking! REST API will be unavailable!"); + // REST API failing isn't fatal -- we'll allow this to return normally } } else { - m_remoteControl = nullptr; + m_RESTAPI = nullptr; } return true; @@ -2564,9 +2561,9 @@ void Host::setState(uint8_t state) delete m_network; } - if (m_remoteControl != nullptr) { - m_remoteControl->close(); - delete m_remoteControl; + if (m_RESTAPI != nullptr) { + m_RESTAPI->close(); + delete m_RESTAPI; } } else { diff --git a/host/Host.h b/host/Host.h index 2fad6f54..b3b1bc29 100644 --- a/host/Host.h +++ b/host/Host.h @@ -33,7 +33,7 @@ #include "Defines.h" #include "network/Network.h" -#include "network/RemoteControl.h" +#include "network/RESTAPI.h" #include "modem/Modem.h" #include "Timer.h" #include "lookups/AffiliationLookup.h" @@ -49,7 +49,7 @@ // Class Prototypes // --------------------------------------------------------------------------- -class HOST_SW_API RemoteControl; +class HOST_SW_API RESTAPI; // --------------------------------------------------------------------------- // Class Declaration @@ -152,8 +152,8 @@ private: uint8_t m_activeTickDelay; uint8_t m_idleTickDelay; - friend class RemoteControl; - RemoteControl* m_remoteControl; + friend class RESTAPI; + RESTAPI* m_RESTAPI; /// Reads basic configuration parameters from the INI. bool readParams(); diff --git a/lookups/AffiliationLookup.h b/lookups/AffiliationLookup.h index ecf0abbd..c8161ddb 100644 --- a/lookups/AffiliationLookup.h +++ b/lookups/AffiliationLookup.h @@ -54,9 +54,9 @@ namespace lookups } /// Initializes a new instance of the VoiceChData class. /// Voice Channel Number. - /// RCON Address. - /// RCON Port. - /// RCON Password. + /// REST API Address. + /// REST API Port. + /// REST API Password. VoiceChData(uint32_t chNo, std::string address, uint16_t port, std::string password) : m_chNo(chNo), m_address(address), @@ -87,11 +87,11 @@ namespace lookups public: /// Voice Channel Number. __READONLY_PROPERTY_PLAIN(uint32_t, chNo, chNo); - /// RCON Address. + /// REST API Address. __READONLY_PROPERTY_PLAIN(std::string, address, address); - /// RCON Port. + /// REST API Port. __READONLY_PROPERTY_PLAIN(uint16_t, port, port); - /// RCON Password. + /// REST API Password. __READONLY_PROPERTY_PLAIN(std::string, password, password); }; diff --git a/modem/Modem.h b/modem/Modem.h index fa39cfd0..1b774764 100644 --- a/modem/Modem.h +++ b/modem/Modem.h @@ -34,7 +34,7 @@ #include "Defines.h" #include "modem/port/IModemPort.h" -#include "network/RemoteControl.h" +#include "network/RESTAPI.h" #include "RingBuffer.h" #include "Timer.h" @@ -56,7 +56,7 @@ // --------------------------------------------------------------------------- class HOST_SW_API HostCal; -class HOST_SW_API RemoteControl; +class HOST_SW_API RESTAPI; namespace modem { @@ -349,7 +349,7 @@ namespace modem private: friend class ::HostCal; - friend class ::RemoteControl; + friend class ::RESTAPI; port::IModemPort* m_port; diff --git a/network/Network.cpp b/network/Network.cpp index e1ea2a5b..1b2157a0 100644 --- a/network/Network.cpp +++ b/network/Network.cpp @@ -86,8 +86,8 @@ Network::Network(const std::string& address, uint16_t port, uint16_t local, uint m_longitude(0.0F), m_height(0), m_location(), - m_rconPassword(), - m_rconPort(0) + m_restApiPassword(), + m_restApiPort(0) { assert(!address.empty()); assert(port > 0U); @@ -148,14 +148,14 @@ void Network::setMetadata(const std::string& identity, uint32_t rxFrequency, uin } /// -/// Sets RCON configuration settings from the modem. +/// Sets REST API configuration settings from the modem. /// /// /// -void Network::setRconData(const std::string& password, uint16_t port) +void Network::setRESTAPIData(const std::string& password, uint16_t port) { - m_rconPassword = password; - m_rconPort = port; + m_restApiPassword = password; + m_restApiPort = port; } /// @@ -539,8 +539,8 @@ bool Network::writeConfig() // RCON json::object rcon = json::object(); - rcon["password"].set(m_rconPassword); // RCON Password - rcon["port"].set(m_rconPort); // RCON Port + rcon["password"].set(m_restApiPassword); // REST API Password + rcon["port"].set(m_restApiPort); // REST API Port config["rcon"].set(rcon); config["software"].set(std::string(software)); // Software ID diff --git a/network/Network.h b/network/Network.h index 6608ce4c..26d9b80e 100644 --- a/network/Network.h +++ b/network/Network.h @@ -59,8 +59,8 @@ namespace network /// Sets metadata configuration settings from the modem. void setMetadata(const std::string& callsign, uint32_t rxFrequency, uint32_t txFrequency, float txOffsetMhz, float chBandwidthKhz, uint8_t channelId, uint32_t channelNo, uint32_t power, float latitude, float longitude, int height, const std::string& location); - /// Sets RCON configuration settings from the modem. - void setRconData(const std::string& password, uint16_t port); + /// Sets REST API configuration settings from the modem. + void setRESTAPIData(const std::string& password, uint16_t port); /// Gets the current status of the network. uint8_t getStatus(); @@ -109,8 +109,8 @@ namespace network int m_height; std::string m_location; - std::string m_rconPassword; - uint16_t m_rconPort; + std::string m_restApiPassword; + uint16_t m_restApiPort; /// Writes login request to the network. bool writeLogin(); diff --git a/network/RESTAPI.cpp b/network/RESTAPI.cpp new file mode 100644 index 00000000..51e62a45 --- /dev/null +++ b/network/RESTAPI.cpp @@ -0,0 +1,1536 @@ +/** +* Digital Voice Modem - Host Software +* GPLv2 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Host Software +* +*/ +// +// Based on code from the MMDVMHost project. (https://github.com/g4klx/MMDVMHost) +// Licensed under the GPLv2 License (https://opensource.org/licenses/GPL-2.0) +// +/* +* Copyright (C) 2019 by Jonathan Naylor G4KLX +* Copyright (C) 2019-2023 by Bryan Biedenkapp N2PLL +* +* 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., 675 Mass Ave, Cambridge, MA 02139, USA. +*/ +#include "Defines.h" +#include "edac/SHA256.h" +#include "dmr/Control.h" +#include "p25/Control.h" +#include "nxdn/Control.h" +#include "modem/Modem.h" +#include "host/Host.h" +#include "network/json/json.h" +#include "RESTAPI.h" +#include "HostMain.h" +#include "Log.h" +#include "Thread.h" +#include "Utils.h" + +using namespace network; +using namespace network::rest; +using namespace network::rest::http; +using namespace modem; + +#include +#include +#include +#include + +#include +#include + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +#define REST_API_BIND(funcAddr, classInstance) std::bind(&funcAddr, classInstance, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3) + +// --------------------------------------------------------------------------- +// Global Functions +// --------------------------------------------------------------------------- + +/// +/// +/// +/// +/// +/// +void errorReply(HTTPReply& reply, std::string message, HTTPReply::StatusType status = HTTPReply::BAD_REQUEST) +{ + HTTPReply rep; + rep.status = status; + + json::object response = json::object(); + + int s = (int)rep.status; + response["status"].set(s); + response["message"].set(message); + + reply.reply(response); +} + +/// +/// +/// +/// +/// +/// +/// +bool parseRequestBody(const HTTPRequest& request, HTTPReply& reply, json::object& obj) +{ + std::string contentType = request.headers.find("Content-Type"); + if (contentType != "application/json") { + reply = HTTPReply::stockReply(HTTPReply::BAD_REQUEST, "application/json"); + return false; + } + + // parse JSON body + json::value v; + std::string err = json::parse(v, request.content); + if (!err.empty()) { + errorReply(reply, err); + return false; + } + + // ensure parsed JSON is an object + if (!v.is()) { + errorReply(reply, "Request was not a valid JSON object."); + return false; + } + + obj = v.get(); + return true; +} + +// --------------------------------------------------------------------------- +// Public Class Members +// --------------------------------------------------------------------------- + +/// +/// Initializes a new instance of the RESTAPI class. +/// +/// 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) : + m_dispatcher(debug), + m_restServer(address, port), + m_random(), + m_p25MFId(p25::P25_MFG_STANDARD), + m_password(password), + m_passwordHash(nullptr), + m_debug(debug), + m_host(host), + m_dmr(nullptr), + m_p25(nullptr), + m_nxdn(nullptr), + m_ridLookup(nullptr), + m_tidLookup(nullptr), + m_authTokens() +{ + assert(!address.empty()); + assert(port > 0U); + assert(!password.empty()); + + size_t size = password.size(); + + uint8_t* in = new uint8_t[size]; + for (size_t i = 0U; i < size; i++) + in[i] = password.at(i); + + m_passwordHash = new uint8_t[32U]; + ::memset(m_passwordHash, 0x00U, 32U); + + edac::SHA256 sha256; + sha256.buffer(in, (uint32_t)(size), m_passwordHash); + + if (m_debug) { + Utils::dump("REST Password Hash", m_passwordHash, 32U); + } + + std::random_device rd; + std::mt19937 mt(rd()); + m_random = mt; +} + +/// +/// Finalizes a instance of the RESTAPI class. +/// +RESTAPI::~RESTAPI() +{ + /* stub */ +} + +/// +/// Sets the instances of the Radio ID and Talkgroup ID lookup tables. +/// +/// Radio ID Lookup Table Instance +/// Talkgroup ID Lookup Table Instance +void RESTAPI::setLookups(lookups::RadioIdLookup* ridLookup, lookups::TalkgroupIdLookup* tidLookup) +{ + m_ridLookup = ridLookup; + m_tidLookup = tidLookup; +} + +/// +/// Sets the instances of the digital radio protocols. +/// +/// Instance of the DMR Control class. +/// Instance of the P25 Control class. +/// Instance of the NXDN Control class. +void RESTAPI::setProtocols(dmr::Control* dmr, p25::Control* p25, nxdn::Control* nxdn) +{ + m_dmr = dmr; + m_p25 = p25; + m_nxdn = nxdn; +} + +/// +/// Opens connection to the network. +/// +/// +bool RESTAPI::open() +{ + initializeEndpoints(); + m_restServer.setHandler(m_dispatcher); + + return run(); +} + +/// +/// Closes connection to the network. +/// +void RESTAPI::close() +{ + m_restServer.stop(); + wait(); +} + +// --------------------------------------------------------------------------- +// Private Class Members +// --------------------------------------------------------------------------- + +/// +/// +/// +void RESTAPI::entry() +{ + m_restServer.run(); +} + +/// +/// Helper to initialize REST API endpoints. +/// +void RESTAPI::initializeEndpoints() +{ + m_dispatcher.match(PUT_AUTHENTICATE).put(REST_API_BIND(RESTAPI::restAPI_PutAuth, this)); + + m_dispatcher.match(GET_VERSION).get(REST_API_BIND(RESTAPI::restAPI_GetVersion, this)); + m_dispatcher.match(GET_STATUS).get(REST_API_BIND(RESTAPI::restAPI_GetStatus, this)); + m_dispatcher.match(GET_VOICE_CH).get(REST_API_BIND(RESTAPI::restAPI_GetVoiceCh, this)); + + m_dispatcher.match(PUT_MDM_MODE).put(REST_API_BIND(RESTAPI::restAPI_PutModemMode, this)); + m_dispatcher.match(PUT_MDM_KILL).get(REST_API_BIND(RESTAPI::restAPI_PutModemKill, this)); + + m_dispatcher.match(PUT_PERMIT_TG).get(REST_API_BIND(RESTAPI::restAPI_PutPermitTG, this)); + m_dispatcher.match(PUT_GRANT_TG).get(REST_API_BIND(RESTAPI::restAPI_PutGrantTG, this)); + m_dispatcher.match(GET_RELEASE_GRNTS).get(REST_API_BIND(RESTAPI::restAPI_GetReleaseGrants, this)); + m_dispatcher.match(GET_RELEASE_AFFS).get(REST_API_BIND(RESTAPI::restAPI_GetReleaseAffs, this)); + + m_dispatcher.match(GET_RID_WHITELIST, true).get(REST_API_BIND(RESTAPI::restAPI_GetRIDWhitelist, this)); + m_dispatcher.match(GET_RID_BLACKLIST, true).get(REST_API_BIND(RESTAPI::restAPI_GetRIDBlacklist, this)); + + /* + ** Digital Mobile Radio + */ + + m_dispatcher.match(GET_DMR_BEACON).get(REST_API_BIND(RESTAPI::restAPI_GetDMRBeacon, this)); + m_dispatcher.match(GET_DMR_DEBUG, true).get(REST_API_BIND(RESTAPI::restAPI_GetDMRDebug, this)); + m_dispatcher.match(GET_DMR_DUMP_CSBK, true).get(REST_API_BIND(RESTAPI::restAPI_GetDMRDumpCSBK, this)); + m_dispatcher.match(PUT_DMR_RID).get(REST_API_BIND(RESTAPI::restAPI_PutDMRRID, this)); + m_dispatcher.match(GET_DMR_CC_DEDICATED, true).get(REST_API_BIND(RESTAPI::restAPI_GetDMRCCEnable, this)); + m_dispatcher.match(GET_DMR_CC_BCAST, true).get(REST_API_BIND(RESTAPI::restAPI_GetDMRCCBroadcast, this)); + + /* + ** Project 25 + */ + + m_dispatcher.match(GET_P25_CC).get(REST_API_BIND(RESTAPI::restAPI_GetP25CC, this)); + m_dispatcher.match(GET_P25_DEBUG, true).get(REST_API_BIND(RESTAPI::restAPI_GetP25Debug, this)); + m_dispatcher.match(GET_P25_DUMP_TSBK, true).get(REST_API_BIND(RESTAPI::restAPI_GetP25DumpTSBK, this)); + m_dispatcher.match(PUT_P25_RID).get(REST_API_BIND(RESTAPI::restAPI_PutP25RID, this)); + m_dispatcher.match(GET_P25_CC_DEDICATED, true).get(REST_API_BIND(RESTAPI::restAPI_GetP25CCEnable, this)); + m_dispatcher.match(GET_P25_CC_BCAST, true).get(REST_API_BIND(RESTAPI::restAPI_GetP25CCBroadcast, this)); + + /* + ** Next Generation Digital Narrowband + */ + + m_dispatcher.match(GET_NXDN_DEBUG, true).get(REST_API_BIND(RESTAPI::restAPI_GetNXDNDebug, this)); + m_dispatcher.match(GET_NXDN_DUMP_RCCH, true).get(REST_API_BIND(RESTAPI::restAPI_GetNXDNDumpRCCH, this)); +} + +/// +/// +/// +/// +void RESTAPI::invalidateHostToken(const std::string host) +{ + auto token = std::find_if(m_authTokens.begin(), m_authTokens.end(), [&](const AuthTokenValueType& tok) { return tok.first == host; }); + if (token != m_authTokens.end()) { + m_authTokens.erase(host); + } +} + +/// +/// +/// +/// +bool RESTAPI::validateAuth(const HTTPRequest& request, HTTPReply& reply) +{ + std::string host = request.headers.find("Host"); + std::string headerToken = request.headers.find("X-DVM-Auth-Token"); + if (headerToken == "") { + errorReply(reply, "invalid authentication token", HTTPReply::UNAUTHORIZED); + return false; + } + + auto token = std::find_if(m_authTokens.begin(), m_authTokens.end(), [&](const AuthTokenValueType& tok) { return tok.first == host; }); + if (token != m_authTokens.end()) { + uint32_t storedToken = token->second; + uint32_t passedToken = (uint32_t)::strtoul(headerToken.c_str(), NULL, 10); + if (storedToken == passedToken) { + return true; + } else { + m_authTokens.erase(host); // devalidate host + errorReply(reply, "invalid authentication token", HTTPReply::UNAUTHORIZED); + return false; + } + } + else { + errorReply(reply, "invalid authentication token", HTTPReply::UNAUTHORIZED); + return false; + } + + return false; +} + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_PutAuth(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + std::string host = request.headers.find("Host"); + json::object response = json::object(); + json::object req = json::object(); + if (!parseRequestBody(request, reply, req)) { + return; + } + + // validate auth is a string within the JSON blob + if (!req["auth"].is()) { + invalidateHostToken(host); + errorReply(reply, "password was not a valid string"); + return; + } + + std::string auth = req["auth"].get(); + if (auth.empty()) { + invalidateHostToken(host); + errorReply(reply, "auth cannot be empty"); + return; + } + + if (auth.size() > 64) { + invalidateHostToken(host); + errorReply(reply, "auth cannot be longer than 64 characters"); + return; + } + + if (!(auth.find_first_not_of("0123456789abcdefABCDEF", 2) == std::string::npos)) { + invalidateHostToken(host); + errorReply(reply, "auth contains invalid characters"); + return; + } + + if (m_debug) { + ::LogDebug(LOG_REST, "/auth auth = %s", auth.c_str()); + } + + const char* authPtr = auth.c_str(); + uint8_t* passwordHash = new uint8_t[32U]; + ::memset(passwordHash, 0x00U, 32U); + + for (uint8_t i = 0; i < 32U; i++) { + char t[4] = {authPtr[0], authPtr[1], 0}; + passwordHash[i] = (uint8_t)::strtoul(t, NULL, 16); + authPtr += 2 * sizeof(char); + } + + if (m_debug) { + Utils::dump("Password Hash", passwordHash, 32U); + } + + // compare hashes + if (::memcmp(m_passwordHash, passwordHash, 32U) != 0) { + invalidateHostToken(host); + errorReply(reply, "invalid password"); + return; + } + + delete[] passwordHash; + + invalidateHostToken(host); + std::uniform_int_distribution dist(DVM_RAND_MIN, DVM_REST_RAND_MAX); + uint64_t salt = dist(m_random); + + m_authTokens[host] = salt; + response["token"].set(std::to_string(salt)); + reply.reply(response); +} + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_GetVersion(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + json::object response = json::object(); + response["version"].set(std::string((__PROG_NAME__ " " __VER__ " (" DESCR_DMR DESCR_P25 DESCR_NXDN "CW Id, Network) (built " __BUILD__ ")"))); + + reply.reply(response); +} + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_GetStatus(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + json::object response = json::object(); + + yaml::Node systemConf = m_host->m_conf["system"]; + { + yaml::Node modemConfig = m_host->m_conf["system"]["modem"]; + std::string portType = modemConfig["protocol"]["type"].as(); + response["portType"].set(portType); + + yaml::Node uartConfig = modemConfig["protocol"]["uart"]; + std::string modemPort = uartConfig["port"].as(); + response["modemPort"].set(modemPort); + uint32_t portSpeed = uartConfig["speed"].as(115200U); + response["portSpeed"].set(portSpeed); + + response["hostState"].set(m_host->m_state); + bool dmrEnabled = m_dmr != nullptr; + response["dmrEnabled"].set(dmrEnabled); + bool p25Enabled = m_p25 != nullptr; + response["p25Enabled"].set(p25Enabled); + bool nxdnEnabled = m_nxdn != nullptr; + response["nxdnEnabled"].set(nxdnEnabled); + + uint8_t protoVer = m_host->m_modem->getVersion(); + response["protoVer"].set(protoVer); + + response["dmrCC"].set(m_host->m_dmrCtrlChannel); + response["p25CC"].set(m_host->m_p25CtrlChannel); + response["nxdnCC"].set(m_host->m_nxdnCtrlChannel); + } + + { + json::object modemInfo = json::object(); + if (!m_host->m_modem->isHotspot()) { + modemInfo["pttInvert"].set(m_host->m_modem->m_pttInvert); + modemInfo["rxInvert"].set(m_host->m_modem->m_rxInvert); + modemInfo["txInvert"].set(m_host->m_modem->m_txInvert); + modemInfo["dcBlocker"].set(m_host->m_modem->m_dcBlocker); + } + + modemInfo["rxLevel"].set(m_host->m_modem->m_rxLevel); + modemInfo["cwTxLevel"].set(m_host->m_modem->m_cwIdTXLevel); + modemInfo["dmrTxLevel"].set(m_host->m_modem->m_dmrTXLevel); + modemInfo["p25TxLevel"].set(m_host->m_modem->m_p25TXLevel); + modemInfo["nxdnTxLevel"].set(m_host->m_modem->m_nxdnTXLevel); + + modemInfo["rxDCOffset"].set(m_host->m_modem->m_rxDCOffset); + modemInfo["txDCOffset"].set(m_host->m_modem->m_txDCOffset); + + if (!m_host->m_modem->isHotspot()) { + modemInfo["dmrSymLevel3Adj"].set(m_host->m_modem->m_dmrSymLevel3Adj); + modemInfo["dmrSymLevel1Adj"].set(m_host->m_modem->m_dmrSymLevel1Adj); + modemInfo["p25SymLevel3Adj"].set(m_host->m_modem->m_p25SymLevel3Adj); + modemInfo["p25SymLevel1Adj"].set(m_host->m_modem->m_p25SymLevel1Adj); + + // are we on a protocol version 3 firmware? + if (m_host->m_modem->getVersion() >= 3U) { + modemInfo["nxdnSymLevel3Adj"].set(m_host->m_modem->m_nxdnSymLevel3Adj); + modemInfo["nxdnSymLevel1Adj"].set(m_host->m_modem->m_nxdnSymLevel1Adj); + } + } + + if (m_host->m_modem->isHotspot()) { + modemInfo["dmrDiscBW"].set(m_host->m_modem->m_dmrDiscBWAdj); + modemInfo["dmrPostBW"].set(m_host->m_modem->m_dmrPostBWAdj); + modemInfo["p25DiscBW"].set(m_host->m_modem->m_p25DiscBWAdj); + modemInfo["p25PostBW"].set(m_host->m_modem->m_p25PostBWAdj); + + // are we on a protocol version 3 firmware? + if (m_host->m_modem->getVersion() >= 3U) { + modemInfo["nxdnDiscBW"].set(m_host->m_modem->m_nxdnDiscBWAdj); + modemInfo["nxdnPostBW"].set(m_host->m_modem->m_nxdnPostBWAdj); + + modemInfo["afcEnabled"].set(m_host->m_modem->m_afcEnable); + modemInfo["afcKI"].set(m_host->m_modem->m_afcKI); + modemInfo["afcKP"].set(m_host->m_modem->m_afcKP); + modemInfo["afcRange"].set(m_host->m_modem->m_afcRange); + } + + switch (m_host->m_modem->m_adfGainMode) { + case ADF_GAIN_AUTO_LIN: + modemInfo["gainMode"].set("ADF7021 Gain Mode: Auto High Linearity"); + break; + case ADF_GAIN_LOW: + modemInfo["gainMode"].set("ADF7021 Gain Mode: Low"); + break; + case ADF_GAIN_HIGH: + modemInfo["gainMode"].set("ADF7021 Gain Mode: High"); + break; + case ADF_GAIN_AUTO: + default: + modemInfo["gainMode"].set("ADF7021 Gain Mode: Auto"); + break; + } + } + + modemInfo["fdmaPreambles"].set(m_host->m_modem->m_fdmaPreamble); + modemInfo["dmrRxDelay"].set(m_host->m_modem->m_dmrRxDelay); + modemInfo["p25CorrCount"].set(m_host->m_modem->m_p25CorrCount); + + modemInfo["rxFrequency"].set(m_host->m_modem->m_rxFrequency); + modemInfo["txFrequency"].set(m_host->m_modem->m_txFrequency); + modemInfo["rxTuning"].set(m_host->m_modem->m_rxTuning); + modemInfo["txTuning"].set(m_host->m_modem->m_txTuning); + uint32_t rxFreqEffective = m_host->m_modem->m_rxFrequency + m_host->m_modem->m_rxTuning; + modemInfo["rxFrequencyEffective"].set(rxFreqEffective); + uint32_t txFreqEffective = m_host->m_modem->m_txFrequency + m_host->m_modem->m_txTuning; + modemInfo["txFrequencyEffective"].set(txFreqEffective); + response["modem"].set(modemInfo); + } + + reply.reply(response); +} + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_GetVoiceCh(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + json::object response = json::object(); + + json::array channels = json::array(); + if (m_host->m_voiceChData.size() > 0) { + for (auto entry : m_host->m_voiceChData) { + uint32_t chNo = entry.first; + lookups::VoiceChData data = entry.second; + + json::object channel = json::object(); + channel["chNo"].set(chNo); + std::string address = data.address(); + channel["address"].set(address); + uint16_t port = data.port(); + channel["port"].set(port); + + channels.push_back(json::value(channel)); + } + } + + response["channels"].set(channels); + reply.reply(response); +} + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_PutModemMode(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + json::object response = json::object(); + json::object req = json::object(); + if (!parseRequestBody(request, reply, req)) { + return; + } + + // validate mode is a string within the JSON blob + if (!req["mode"].is()) { + errorReply(reply, "password was not a valid string"); + return; + } + + std::string mode = req["mode"].get(); + + if (mode == MODE_OPT_IDLE) { + m_host->m_fixedMode = false; + m_host->setState(STATE_IDLE); + + response["message"].set(std::string("Dynamic mode")); + uint8_t hostMode = m_host->m_state; + response["mode"].set(hostMode); + + reply.reply(response); + } + else if (mode == MODE_OPT_LCKOUT) { + m_host->m_fixedMode = false; + m_host->setState(HOST_STATE_LOCKOUT); + + response["message"].set(std::string("Lockout mode")); + uint8_t hostMode = m_host->m_state; + response["mode"].set(hostMode); + + reply.reply(response); + } +#if defined(ENABLE_DMR) + else if (mode == MODE_OPT_FDMR) { + if (m_dmr != nullptr) { + m_host->m_fixedMode = true; + m_host->setState(STATE_DMR); + + response["message"].set(std::string("Fixed mode")); + uint8_t hostMode = m_host->m_state; + response["mode"].set(hostMode); + + reply.reply(response); + } + else { + errorReply(reply, "DMR mode is not enabled", HTTPReply::SERVICE_UNAVAILABLE); + } + } +#endif // defined(ENABLE_DMR) +#if defined(ENABLE_P25) + else if (mode == MODE_OPT_FP25) { + if (m_p25 != nullptr) { + m_host->m_fixedMode = true; + m_host->setState(STATE_P25); + + response["message"].set(std::string("Fixed mode")); + uint8_t hostMode = m_host->m_state; + response["mode"].set(hostMode); + + reply.reply(response); + } + else { + errorReply(reply, "P25 mode is not enabled", HTTPReply::SERVICE_UNAVAILABLE); + } + } +#endif // defined(ENABLE_P25) +#if defined(ENABLE_NXDN) + else if (mode == MODE_OPT_FNXDN) { + if (m_nxdn != nullptr) { + m_host->m_fixedMode = true; + m_host->setState(STATE_NXDN); + + response["message"].set(std::string("Fixed mode")); + uint8_t hostMode = m_host->m_state; + response["mode"].set(hostMode); + + reply.reply(response); + } + else { + errorReply(reply, "NXDN mode is not enabled", HTTPReply::SERVICE_UNAVAILABLE); + } + } +#endif // defined(ENABLE_NXDN) + else { + errorReply(reply, "invalid mode"); + } +} + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_PutModemKill(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + json::object req = json::object(); + if (!parseRequestBody(request, reply, req)) { + return; + } + + errorReply(reply, "OK", HTTPReply::OK); + + if (!req["force"].is()) { + g_killed = true; + } else { + bool force = req["force"].get(); + if (force) { + g_killed = true; + m_host->setState(HOST_STATE_QUIT); + } else { + g_killed = true; + } + } +} + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_PutPermitTG(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + json::object req = json::object(); + if (!parseRequestBody(request, reply, req)) { + return; + } + + if (!m_host->m_authoritative) { + errorReply(reply, "Host is authoritative, cannot permit TG"); + return; + } + + // validate state is a string within the JSON blob + if (!req["state"].is()) { + errorReply(reply, "state was not a valid integer"); + return; + } + + DVM_STATE state = (DVM_STATE)req["state"].get(); + + // validate destination ID is a integer within the JSON blob + if (!req["dstId"].is()) { + errorReply(reply, "destination ID was not a valid integer"); + return; + } + + uint32_t dstId = req["dstId"].get(); + + if (dstId == 0U) { + errorReply(reply, "destination ID is an illegal TGID"); + return; + } + + switch (state) { + case STATE_DMR: +#if defined(ENABLE_DMR) + { + // validate slot is a integer within the JSON blob + if (!req["slot"].is()) { + errorReply(reply, "slot was not a valid integer"); + return; + } + + uint8_t slot = (uint8_t)req["slot"].get(); + + if (slot == 0U || slot > 2U) { + errorReply(reply, "illegal DMR slot"); + return; + } + + if (m_dmr != nullptr) { + m_dmr->permittedTG(dstId, slot); + } + else { + errorReply(reply, "DMR mode is not enabled", HTTPReply::SERVICE_UNAVAILABLE); + } + } +#else + { + errorReply(reply, "DMR operations are unavailable", HTTPReply::SERVICE_UNAVAILABLE); + } +#endif // defined(ENABLE_DMR) + break; + case STATE_P25: +#if defined(ENABLE_P25) + { + if (m_p25 != nullptr) { + m_p25->permittedTG(dstId); + } + else { + errorReply(reply, "P25 mode is not enabled", HTTPReply::SERVICE_UNAVAILABLE); + } + } +#else + { + errorReply(reply, "P25 operations are unavailable", HTTPReply::SERVICE_UNAVAILABLE); + } +#endif // defined(ENABLE_P25) + break; + case STATE_NXDN: +#if defined(ENABLE_NXDN) + { + if (m_nxdn != nullptr) { + m_nxdn->permittedTG(dstId); + } + else { + errorReply(reply, "NXDN mode is not enabled", HTTPReply::SERVICE_UNAVAILABLE); + } + } +#else + { + errorReply(reply, "NXDN operations are unavailable", HTTPReply::SERVICE_UNAVAILABLE); + } +#endif // defined(ENABLE_NXDN) + break; + default: + errorReply(reply, "invalid mode"); + break; + } +} + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_PutGrantTG(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + json::object req = json::object(); + if (!parseRequestBody(request, reply, req)) { + return; + } + + if (m_host->m_authoritative && (m_host->m_dmrCtrlChannel || m_host->m_p25CtrlChannel || m_host->m_nxdnCtrlChannel)) { + errorReply(reply, "Host is authoritative, cannot grant TG"); + return; + } + + // validate state is a string within the JSON blob + if (!req["state"].is()) { + errorReply(reply, "state was not a valid integer"); + return; + } + + DVM_STATE state = (DVM_STATE)req["state"].get(); + + // validate destination ID is a integer within the JSON blob + if (!req["dstId"].is()) { + errorReply(reply, "destination ID was not a valid integer"); + return; + } + + uint32_t dstId = req["dstId"].get(); + + if (dstId == 0U) { + errorReply(reply, "destination ID is an illegal TGID"); + return; + } + + // validate unit-to-unit is a integer within the JSON blob + if (!req["unitToUnit"].is()) { + errorReply(reply, "unit-to-unit was not a valid integer"); + return; + } + + uint8_t unitToUnit = (uint8_t)req["unitToUnit"].get(); + + if (unitToUnit > 1U) { + errorReply(reply, "unit-to-unit must be a 0 or 1"); + return; + } + + switch (state) { + case STATE_DMR: +#if defined(ENABLE_DMR) + { + // validate slot is a integer within the JSON blob + if (!req["slot"].is()) { + errorReply(reply, "slot was not a valid integer"); + return; + } + + uint8_t slot = (uint8_t)req["slot"].get(); + + if (slot == 0U || slot > 2U) { + errorReply(reply, "illegal DMR slot"); + return; + } + + if (m_dmr != nullptr) { + // TODO TODO + //m_dmr->grantTG(dstId, slot, unitToUnit == 1U); + } + else { + errorReply(reply, "DMR mode is not enabled", HTTPReply::SERVICE_UNAVAILABLE); + } + } +#else + { + errorReply(reply, "DMR operations are unavailable", HTTPReply::SERVICE_UNAVAILABLE); + } +#endif // defined(ENABLE_DMR) + break; + case STATE_P25: +#if defined(ENABLE_P25) + { + if (m_p25 != nullptr) { + // TODO TODO + //m_p25->grantTG(dstId, unitToUnit == 1U); + } + else { + errorReply(reply, "P25 mode is not enabled", HTTPReply::SERVICE_UNAVAILABLE); + } + } +#else + { + errorReply(reply, "P25 operations are unavailable", HTTPReply::SERVICE_UNAVAILABLE); + } +#endif // defined(ENABLE_P25) + break; + case STATE_NXDN: +#if defined(ENABLE_NXDN) + { + if (m_nxdn != nullptr) { + // TODO TODO + //nxdn->grantTG(dstId, unitToUnit == 1U); + } + else { + errorReply(reply, "NXDN mode is not enabled", HTTPReply::SERVICE_UNAVAILABLE); + } + } +#else + { + errorReply(reply, "NXDN operations are unavailable", HTTPReply::SERVICE_UNAVAILABLE); + } +#endif // defined(ENABLE_NXDN) + break; + default: + errorReply(reply, "invalid mode"); + } +} + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_GetReleaseGrants(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + errorReply(reply, "OK", HTTPReply::OK); +#if defined(ENABLE_DMR) + if (m_dmr != nullptr) { + m_dmr->affiliations().releaseGrant(0, true); + } +#endif // defined(ENABLE_DMR) +#if defined(ENABLE_P25) + if (m_p25 != nullptr) { + m_p25->affiliations().releaseGrant(0, true); + } +#endif // defined(ENABLE_P25) +#if defined(ENABLE_NXDN) + if (m_nxdn != nullptr) { + m_nxdn->affiliations().releaseGrant(0, true); + } +#endif // defined(ENABLE_NXDN) +} + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_GetReleaseAffs(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + errorReply(reply, "OK", HTTPReply::OK); +#if defined(ENABLE_DMR) + if (m_dmr != nullptr) { + m_dmr->affiliations().clearGroupAff(0, true); + } +#endif // defined(ENABLE_DMR) +#if defined(ENABLE_P25) + if (m_p25 != nullptr) { + m_p25->affiliations().clearGroupAff(0, true); + } +#endif // defined(ENABLE_P25) +#if defined(ENABLE_NXDN) + if (m_nxdn != nullptr) { + m_nxdn->affiliations().clearGroupAff(0, true); + } +#endif // defined(ENABLE_NXDN) +} + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_GetRIDWhitelist(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + if (match.size() < 2) { + errorReply(reply, "invalid API call arguments"); + return; + } + + errorReply(reply, "OK", HTTPReply::OK); + uint32_t srcId = (uint32_t)::strtoul(match.str(1).c_str(), NULL, 10); + + if (srcId != 0U) { + m_ridLookup->toggleEntry(srcId, true); + } + else { + errorReply(reply, "tried to whitelist RID 0"); + } +} + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_GetRIDBlacklist(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + json::object response = json::object(); + if (match.size() < 2) { + errorReply(reply, "invalid API call arguments"); + return; + } + + errorReply(reply, "OK", HTTPReply::OK); + uint32_t srcId = (uint32_t)::strtoul(match.str(1).c_str(), NULL, 10); + + if (srcId != 0U) { + m_ridLookup->toggleEntry(srcId, false); + } + else { + errorReply(reply, "tried to whitelist RID 0"); + } +} + +/* +** Digital Mobile Radio +*/ + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_GetDMRBeacon(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + +#if defined(ENABLE_DMR) + errorReply(reply, "OK", HTTPReply::OK); + if (m_dmr != nullptr) { + if (m_host->m_dmrBeacons) { + g_fireDMRBeacon = true; + } + else { + errorReply(reply, "DMR beacons are not enabled", HTTPReply::SERVICE_UNAVAILABLE); + return; + } + } + else { + errorReply(reply, "DMR mode is not enabled", HTTPReply::SERVICE_UNAVAILABLE); + return; + } +#else + errorReply(reply, "DMR operations are unavailable", HTTPReply::SERVICE_UNAVAILABLE); +#endif // defined(ENABLE_DMR) +} + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_GetDMRDebug(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + json::object response = json::object(); +#if defined(ENABLE_DMR) + errorReply(reply, "OK", HTTPReply::OK); + if (m_dmr != nullptr) { + if (match.size() <= 1) { + bool debug = m_dmr->getDebug(); + bool verbose = m_dmr->getVerbose(); + + response["debug"].set(debug); + response["verbose"].set(verbose); + + reply.reply(response); + return; + } + else { + if (match.size() == 3) { + uint8_t debug = (uint8_t)::strtoul(match.str(1).c_str(), NULL, 10); + uint8_t verbose = (uint8_t)::strtoul(match.str(2).c_str(), NULL, 10); + m_dmr->setDebugVerbose((debug == 1U) ? true : false, (verbose == 1U) ? true : false); + } + } + } + else { + errorReply(reply, "DMR mode is not enabled", HTTPReply::SERVICE_UNAVAILABLE); + return; + } +#else + errorReply(reply, "DMR operations are unavailable", HTTPReply::SERVICE_UNAVAILABLE); +#endif // defined(ENABLE_DMR) +} + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_GetDMRDumpCSBK(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + json::object response = json::object(); +#if defined(ENABLE_DMR) + errorReply(reply, "OK", HTTPReply::OK); + if (m_dmr != nullptr) { + if (match.size() <= 1) { + bool csbkDump = m_dmr->getCSBKVerbose(); + + response["verbose"].set(csbkDump); + + reply.reply(response); + return; + } + else { + if (match.size() == 2) { + uint8_t enable = (uint8_t)::strtoul(match.str(1).c_str(), NULL, 10); + m_dmr->setCSBKVerbose((enable == 1U) ? true : false); + } + } + } + else { + errorReply(reply, "DMR mode is not enabled", HTTPReply::SERVICE_UNAVAILABLE); + return; + } +#else + errorReply(reply, "DMR operations are unavailable", HTTPReply::SERVICE_UNAVAILABLE); +#endif // defined(ENABLE_DMR) +} + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_PutDMRRID(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + json::object response = json::object(); + json::object req = json::object(); + if (!parseRequestBody(request, reply, req)) { + return; + } + + reply.reply(response); +} + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_GetDMRCCEnable(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + json::object response = json::object(); + if (match.size() < 2) { + errorReply(reply, "invalid API call arguments"); + return; + } + + uint32_t v = (uint32_t)::strtoul(match.str(1).c_str(), NULL, 10); + + reply.reply(response); +} + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_GetDMRCCBroadcast(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + json::object response = json::object(); + if (match.size() < 2) { + errorReply(reply, "invalid API call arguments"); + return; + } + + uint32_t v = (uint32_t)::strtoul(match.str(1).c_str(), NULL, 10); + + reply.reply(response); +} + +/* +** Project 25 +*/ + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_GetP25CC(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + +#if defined(ENABLE_P25) + errorReply(reply, "OK", HTTPReply::OK); + if (m_dmr != nullptr) { + if (m_host->m_p25CCData) { + g_fireP25Control = true; + } + else { + errorReply(reply, "P25 control data is not enabled", HTTPReply::SERVICE_UNAVAILABLE); + return; + } + } + else { + errorReply(reply, "P25 mode is not enabled", HTTPReply::SERVICE_UNAVAILABLE); + return; + } +#else + errorReply(reply, "P25 operations are unavailable", HTTPReply::SERVICE_UNAVAILABLE); +#endif // defined(ENABLE_P25) +} + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_GetP25Debug(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + json::object response = json::object(); +#if defined(ENABLE_P25) + errorReply(reply, "OK", HTTPReply::OK); + if (m_dmr != nullptr) { + if (match.size() <= 1) { + bool debug = m_p25->getDebug(); + bool verbose = m_p25->getVerbose(); + + response["debug"].set(debug); + response["verbose"].set(verbose); + + reply.reply(response); + return; + } + else { + if (match.size() == 3) { + uint8_t debug = (uint8_t)::strtoul(match.str(1).c_str(), NULL, 10); + uint8_t verbose = (uint8_t)::strtoul(match.str(2).c_str(), NULL, 10); + m_p25->setDebugVerbose((debug == 1U) ? true : false, (verbose == 1U) ? true : false); + } + } + } + else { + errorReply(reply, "P25 mode is not enabled", HTTPReply::SERVICE_UNAVAILABLE); + return; + } +#else + errorReply(reply, "P25 operations are unavailable", HTTPReply::SERVICE_UNAVAILABLE); +#endif // defined(ENABLE_P25) +} + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_GetP25DumpTSBK(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + json::object response = json::object(); +#if defined(ENABLE_P25) + errorReply(reply, "OK", HTTPReply::OK); + if (m_p25 != nullptr) { + if (match.size() <= 1) { + bool tsbkDump = m_p25->trunk()->getTSBKVerbose(); + + response["verbose"].set(tsbkDump); + + reply.reply(response); + return; + } + else { + if (match.size() == 2) { + uint8_t enable = (uint8_t)::strtoul(match.str(1).c_str(), NULL, 10); + m_p25->trunk()->setTSBKVerbose((enable == 1U) ? true : false); + } + } + } + else { + errorReply(reply, "P25 mode is not enabled", HTTPReply::SERVICE_UNAVAILABLE); + return; + } +#else + errorReply(reply, "P25 operations are unavailable", HTTPReply::SERVICE_UNAVAILABLE); +#endif // defined(ENABLE_P25) +} + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_PutP25RID(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + json::object response = json::object(); + json::object req = json::object(); + if (!parseRequestBody(request, reply, req)) { + return; + } + + reply.reply(response); +} + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_GetP25CCEnable(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + json::object response = json::object(); + if (match.size() < 2) { + errorReply(reply, "invalid API call arguments"); + return; + } + + uint32_t v = (uint32_t)::strtoul(match.str(1).c_str(), NULL, 10); + + reply.reply(response); +} + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_GetP25CCBroadcast(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + json::object response = json::object(); + if (match.size() < 2) { + errorReply(reply, "invalid API call arguments"); + return; + } + + uint32_t v = (uint32_t)::strtoul(match.str(1).c_str(), NULL, 10); + + reply.reply(response); +} + +/* +** Next Generation Digital Narrowband +*/ + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_GetNXDNDebug(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + json::object response = json::object(); +#if defined(ENABLE_NXDN) + errorReply(reply, "OK", HTTPReply::OK); + if (m_dmr != nullptr) { + if (match.size() <= 1) { + bool debug = m_nxdn->getDebug(); + bool verbose = m_nxdn->getVerbose(); + + response["debug"].set(debug); + response["verbose"].set(verbose); + + reply.reply(response); + return; + } + else { + if (match.size() == 3) { + uint8_t debug = (uint8_t)::strtoul(match.str(1).c_str(), NULL, 10); + uint8_t verbose = (uint8_t)::strtoul(match.str(2).c_str(), NULL, 10); + m_nxdn->setDebugVerbose((debug == 1U) ? true : false, (verbose == 1U) ? true : false); + } + } + } + else { + errorReply(reply, "NXDN mode is not enabled", HTTPReply::SERVICE_UNAVAILABLE); + return; + } +#else + errorReply(reply, "NXDN operations are unavailable", HTTPReply::SERVICE_UNAVAILABLE); +#endif // defined(ENABLE_NXDN) +} + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_GetNXDNDumpRCCH(const HTTPRequest& request, HTTPReply& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + json::object response = json::object(); +#if defined(ENABLE_NXDN) + errorReply(reply, "OK", HTTPReply::OK); + if (m_p25 != nullptr) { + if (match.size() <= 1) { + bool rcchDump = m_nxdn->getRCCHVerbose(); + + response["verbose"].set(rcchDump); + + reply.reply(response); + return; + } + else { + if (match.size() == 2) { + uint8_t enable = (uint8_t)::strtoul(match.str(1).c_str(), NULL, 10); + m_nxdn->setRCCHVerbose((enable == 1U) ? true : false); + } + } + } + else { + errorReply(reply, "NXDN mode is not enabled", HTTPReply::SERVICE_UNAVAILABLE); + return; + } +#else + errorReply(reply, "NXDN operations are unavailable", HTTPReply::SERVICE_UNAVAILABLE); +#endif // defined(ENABLE_NXDN) +} diff --git a/network/RESTAPI.h b/network/RESTAPI.h new file mode 100644 index 00000000..ab45018f --- /dev/null +++ b/network/RESTAPI.h @@ -0,0 +1,230 @@ +/** +* Digital Voice Modem - Host Software +* GPLv2 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Host Software +* +*/ +/* +* Copyright (C) 2023 by Bryan Biedenkapp N2PLL +* +* 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., 675 Mass Ave, Cambridge, MA 02139, USA. +*/ +#if !defined(__REST_API_H__) +#define __REST_API_H__ + +#include "Defines.h" +#include "network/UDPSocket.h" +#include "network/rest/RequestDispatcher.h" +#include "network/rest/http/HTTPServer.h" +#include "lookups/RadioIdLookup.h" +#include "lookups/TalkgroupIdLookup.h" +#include "Thread.h" + +#include +#include +#include + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +#define DVM_REST_RAND_MAX 0xfffffffffffffffe + +#define PUT_AUTHENTICATE "/auth" + +#define GET_VERSION "/version" +#define GET_STATUS "/status" +#define GET_VOICE_CH "/voice-ch" + +#define PUT_MDM_MODE "/mdm/mode" +#define MODE_OPT_IDLE "idle" +#define MODE_OPT_LCKOUT "lockout" +#define MODE_OPT_FDMR "dmr" +#define MODE_OPT_FP25 "p25" +#define MODE_OPT_FNXDN "nxdn" + +#define PUT_MDM_KILL "/mdm/kill" + +#define PUT_PERMIT_TG "/permit-tg" +#define PUT_GRANT_TG "/grant-tg" +#define GET_RELEASE_GRNTS "/release-grants" +#define GET_RELEASE_AFFS "/release-affs" + +#define GET_RID_WHITELIST "/rid-whitelist/(\\d+)" +#define GET_RID_BLACKLIST "/rid-blacklist/(\\d+)" + +#define GET_DMR_BEACON "/dmr/beacon" +#define GET_DMR_DEBUG "/dmr/debug/(\\d+)/(\\d+)" +#define GET_DMR_DUMP_CSBK "/dmr/dump-csbk/(\\d+)" +#define PUT_DMR_RID "/dmr/rid" +#define GET_DMR_CC_DEDICATED "/dmr/cc-enable/(\\d+)" +#define GET_DMR_CC_BCAST "/dmr/cc-broadcast/(\\d+)" + +#define GET_P25_CC "/p25/cc" +//#define GET_P25_CC_FALLBACK "/p25/cc-fallback/(\\d+)" +#define GET_P25_DEBUG "/p25/debug/(\\d+)/(\\d+)" +#define GET_P25_DUMP_TSBK "/p25/dump-tsbk/(\\d+)" +#define PUT_P25_RID "/p25/rid" +#define GET_P25_CC_DEDICATED "/p25/cc-enable/(\\d+)" +#define GET_P25_CC_BCAST "/p25/cc-broadcast/(\\d+)" + +#define GET_NXDN_DEBUG "/nxdn/debug/(\\d+)/(\\d+)" +#define GET_NXDN_DUMP_RCCH "/nxdn/dump-rcch/(\\d+)" + +// --------------------------------------------------------------------------- +// Class Prototypes +// --------------------------------------------------------------------------- + +class HOST_SW_API Host; +namespace dmr { class HOST_SW_API Control; } +namespace p25 { class HOST_SW_API Control; } +namespace nxdn { class HOST_SW_API Control; } + +// --------------------------------------------------------------------------- +// Class Declaration +// Implements the REST API server logic. +// --------------------------------------------------------------------------- + +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); + /// Finalizes a instance of the RESTAPI class. + ~RESTAPI(); + + /// Sets the instances of the Radio ID and Talkgroup ID lookup tables. + void setLookups(::lookups::RadioIdLookup* ridLookup, ::lookups::TalkgroupIdLookup* tidLookup); + /// Sets the instances of the digital radio protocols. + void setProtocols(dmr::Control* dmr, p25::Control* p25, nxdn::Control* nxdn); + + /// Opens connection to the network. + bool open(); + + /// Closes connection to the network. + void close(); + +private: + typedef network::rest::RequestDispatcher RESTDispatcherType; + typedef network::rest::http::HTTPRequest HTTPRequest; + typedef network::rest::http::HTTPReply HTTPReply; + RESTDispatcherType m_dispatcher; + network::rest::http::HTTPServer m_restServer; + + std::mt19937 m_random; + + uint8_t m_p25MFId; + + std::string m_password; + uint8_t* m_passwordHash; + bool m_debug; + + Host* m_host; + dmr::Control* m_dmr; + p25::Control* m_p25; + nxdn::Control* m_nxdn; + + ::lookups::RadioIdLookup* m_ridLookup; + ::lookups::TalkgroupIdLookup* m_tidLookup; + + typedef std::unordered_map::value_type AuthTokenValueType; + std::unordered_map m_authTokens; + + /// + virtual void entry(); + + /// Helper to initialize REST API endpoints. + void initializeEndpoints(); + + /// + void invalidateHostToken(const std::string host); + /// + bool validateAuth(const HTTPRequest& request, HTTPReply& reply); + + /// + void restAPI_PutAuth(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); + + /// + void restAPI_GetVersion(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); + /// + void restAPI_GetStatus(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); + /// + void restAPI_GetVoiceCh(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); + + /// + void restAPI_PutModemMode(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); + /// + void restAPI_PutModemKill(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); + + /// + void restAPI_PutPermitTG(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); + /// + void restAPI_PutGrantTG(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); + /// + void restAPI_GetReleaseGrants(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); + /// + void restAPI_GetReleaseAffs(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); + + /// + void restAPI_GetRIDWhitelist(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); + /// + void restAPI_GetRIDBlacklist(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); + + /* + ** Digital Mobile Radio + */ + + /// + void restAPI_GetDMRBeacon(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); + /// + void restAPI_GetDMRDebug(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); + /// + void restAPI_GetDMRDumpCSBK(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); + /// + void restAPI_PutDMRRID(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); + /// + void restAPI_GetDMRCCEnable(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); + /// + void restAPI_GetDMRCCBroadcast(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); + + /* + ** Project 25 + */ + + /// + void restAPI_GetP25CC(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); + /// + void restAPI_GetP25Debug(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); + /// + void restAPI_GetP25DumpTSBK(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); + /// + void restAPI_PutP25RID(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); + /// + void restAPI_GetP25CCEnable(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); + /// + void restAPI_GetP25CCBroadcast(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); + + /* + ** Next Generation Digital Narrowband + */ + + /// + void restAPI_GetNXDNDebug(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); + /// + void restAPI_GetNXDNDumpRCCH(const HTTPRequest& request, HTTPReply& reply, const network::rest::RequestMatch& match); +}; + +#endif // __REST_API_H__ diff --git a/network/RemoteControl.cpp b/network/RemoteControl.cpp deleted file mode 100644 index ca2765d4..00000000 --- a/network/RemoteControl.cpp +++ /dev/null @@ -1,1692 +0,0 @@ -/** -* Digital Voice Modem - Host Software -* GPLv2 Open Source. Use is subject to license terms. -* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. -* -* @package DVM / Host Software -* -*/ -// -// Based on code from the MMDVMHost project. (https://github.com/g4klx/MMDVMHost) -// Licensed under the GPLv2 License (https://opensource.org/licenses/GPL-2.0) -// -/* -* Copyright (C) 2019 by Jonathan Naylor G4KLX -* Copyright (C) 2019-2023 by Bryan Biedenkapp N2PLL -* -* 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., 675 Mass Ave, Cambridge, MA 02139, USA. -*/ -#include "Defines.h" -#include "edac/SHA256.h" -#include "dmr/Control.h" -#include "p25/Control.h" -#include "nxdn/Control.h" -#include "modem/Modem.h" -#include "host/Host.h" -#include "network/UDPSocket.h" -#include "network/json/json.h" -#include "RemoteControl.h" -#include "HostMain.h" -#include "Log.h" -#include "Thread.h" -#include "Utils.h" - -using namespace network; -using namespace network::rest; -using namespace network::rest::http; -using namespace modem; - -#include -#include -#include -#include - -#include -#include - -// --------------------------------------------------------------------------- -// Constants -// --------------------------------------------------------------------------- - -#define RCON_CMD_OK "ACK" -#define RCON_CMD_NACK "NACK: " - -#define BAD_CMD_STR "NACK: Bad or invalid remote command" -#define NO_DATA_CMD_STR "NACK: No data" -#define INVALID_AUTH_STR "NACK: Invalid authentication" -#define INVALID_OPT_STR "NACK: Invalid command arguments, " - -const uint8_t RCON_FRAME_START = 0xFEU; -const uint8_t START_OF_TEXT = 0x02U; -const uint8_t END_OF_TEXT = 0x03U; -const uint8_t END_OF_BLOCK = 0x17U; -const uint8_t REC_SEPARATOR = 0x1EU; - -const uint32_t RC_BUFFER_LENGTH = 250U; - -// --------------------------------------------------------------------------- -// Global Functions -// --------------------------------------------------------------------------- - -template -std::string string_format(const std::string& format, FormatArgs ... args) -{ - int size_s = std::snprintf(nullptr, 0, format.c_str(), args ...) + 1; // extra space for '\0' - if (size_s <= 0) - throw std::runtime_error("Error during string formatting."); - - auto size = static_cast(size_s); - std::unique_ptr buf(new char[ size ]); - std::snprintf(buf.get(), size, format.c_str(), args ...); - - return std::string(buf.get(), buf.get() + size - 1); -} - -// --------------------------------------------------------------------------- -// Public Class Members -// --------------------------------------------------------------------------- - -/// -/// Initializes a new instance of the RemoteControl class. -/// -/// Network Hostname/IP address to connect to. -/// Network port number. -/// Authentication password. -/// Instance of the Host class. -/// -RemoteControl::RemoteControl(const std::string& address, uint16_t port, const std::string& password, Host* host, bool debug) : - m_dispatcher(), - m_restServer(address, (uint16_t)(port + 1U)/*port*/), - m_socket(address, port), - m_p25MFId(p25::P25_MFG_STANDARD), - m_password(password), - m_passwordHash(nullptr), - m_debug(debug), - m_host(host), - m_dmr(nullptr), - m_p25(nullptr), - m_nxdn(nullptr), - m_ridLookup(nullptr), - m_tidLookup(nullptr) -{ - assert(!address.empty()); - assert(port > 0U); - - if (!password.empty()) { - size_t size = password.size(); - - uint8_t* in = new uint8_t[size]; - for (size_t i = 0U; i < size; i++) - in[i] = password.at(i); - - m_passwordHash = new uint8_t[32U]; - ::memset(m_passwordHash, 0x00U, 32U); - - edac::SHA256 sha256; - sha256.buffer(in, (uint32_t)(size), m_passwordHash); - } -} - -/// -/// Finalizes a instance of the RemoteControl class. -/// -RemoteControl::~RemoteControl() -{ - /* stub */ -} - -/// -/// Sets the instances of the Radio ID and Talkgroup ID lookup tables. -/// -/// Radio ID Lookup Table Instance -/// Talkgroup ID Lookup Table Instance -void RemoteControl::setLookups(lookups::RadioIdLookup* ridLookup, lookups::TalkgroupIdLookup* tidLookup) -{ - m_ridLookup = ridLookup; - m_tidLookup = tidLookup; -} - -/// -/// Sets the instances of the digital radio protocols. -/// -/// Instance of the DMR Control class. -/// Instance of the P25 Control class. -/// Instance of the NXDN Control class. -void RemoteControl::setProtocols(dmr::Control* dmr, p25::Control* p25, nxdn::Control* nxdn) -{ - m_dmr = dmr; - m_p25 = p25; - m_nxdn = nxdn; -} - -/// -/// Process remote network command data. -/// -/// Instance of the Host class. -/// Instance of the DMR Control class. -/// Instance of the P25 Control class. -/// Instance of the NXDN Control class. -void RemoteControl::process(Host* host, dmr::Control* dmr, p25::Control* p25, nxdn::Control* nxdn) -{ - std::vector args = std::vector(); - args.clear(); - - uint8_t buffer[RC_BUFFER_LENGTH]; - std::string reply = RCON_CMD_OK; - - sockaddr_storage address; - uint32_t addrLen; - uint32_t len = m_socket.read((uint8_t*)buffer, RC_BUFFER_LENGTH, address, addrLen); - if (len > 0U) { - buffer[len] = '\0'; - - if (m_debug) - Utils::dump(1U, "RCON Received", (uint8_t*)buffer, len); - - // make sure this is an RCON command - if (buffer[0U] != RCON_FRAME_START) { - LogWarning(LOG_RCON, BAD_CMD_STR); - return; - } - - if (buffer[1U] != START_OF_TEXT) { - LogWarning(LOG_RCON, BAD_CMD_STR); - return; - } - - // ensure we have at least 34 bytes - if (len < 34U) { - LogWarning(LOG_RCON, BAD_CMD_STR); - reply = BAD_CMD_STR; - writeResponse(reply, address, addrLen); - return; - } - - if (m_passwordHash != nullptr) { - uint8_t hash[32U]; - ::memset(hash, 0x00U, 32U); - if (::memcmp(m_passwordHash, buffer + 2U, 32U) != 0) { - LogError(LOG_RCON, INVALID_AUTH_STR " from %s", UDPSocket::address(address).c_str()); - reply = INVALID_AUTH_STR; - writeResponse(reply, address, addrLen); - return; - } - } - - // make sure we have arguments after the hash - if ((buffer[34U] != REC_SEPARATOR) || (len - 35U) <= 0U) { - LogWarning(LOG_RCON, BAD_CMD_STR); - reply = BAD_CMD_STR; - writeResponse(reply, address, addrLen); - return; - } - - uint32_t size = len - 36U; - char argBuffer[RC_BUFFER_LENGTH]; - ::memset(argBuffer, 0x00U, RC_BUFFER_LENGTH); - ::memcpy(argBuffer, buffer + 35U, size); - - // parse the original command into a vector of strings. - char* b = argBuffer; - char* p = NULL; - while ((p = ::strtok(b, " ")) != NULL) { - b = NULL; - args.push_back(std::string(p)); - } - - if (args.size() < 1 || args.at(0U) == "") { - args.clear(); - LogWarning(LOG_RCON, BAD_CMD_STR); - reply = BAD_CMD_STR; - } - else { - std::string rcom = args.at(0); - uint32_t argCnt = args.size() - 1; - - LogInfoEx(LOG_RCON, "cmd = %s, argCnt = %u from %s", rcom.c_str(), argCnt, UDPSocket::address(address).c_str()); - - // process command - if (rcom == RCD_GET_VERSION) { - reply = (__PROG_NAME__ " " __VER__ " (" DESCR_DMR DESCR_P25 DESCR_NXDN "CW Id, Network) (built " __BUILD__ ")"); - } - else if (rcom == RCD_GET_HELP) { - reply = displayHelp(); - } - else if (rcom == RCD_GET_STATUS) { - reply = rcdGetStatus(host, dmr, p25, nxdn); - } - else if (rcom == RCD_GET_VOICE_CH) { - reply = rcdGetVoiceCh(host, dmr, p25, nxdn); - } - else if (rcom == RCD_MODE && argCnt >= 1U) { - reply = rcdMode(args, host, dmr, p25, nxdn); - } - else if (rcom == RCD_KILL) { - g_killed = true; - } - else if (rcom == RCD_FORCE_KILL) { - g_killed = true; - host->setState(HOST_STATE_QUIT); // ensures immediate cessation of service - } - else if (rcom == RCD_PERMIT_TG && argCnt >= 1U) { - reply = rcdPermitTG(args, host, dmr, p25, nxdn); - } - else if (rcom == RCD_GRANT_TG && argCnt >= 1U) { - reply = rcdPermitTG(args, host, dmr, p25, nxdn); - } - else if (rcom == RCD_RID_WLIST && argCnt >= 1U) { - uint32_t srcId = getArgUInt32(args, 0U); - if (srcId != 0U) { - m_ridLookup->toggleEntry(srcId, true); - } - else { - reply = INVALID_OPT_STR "tried to whitelist RID 0!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else if (rcom == RCD_RID_BLIST && argCnt >= 1U) { - uint32_t srcId = getArgUInt32(args, 0U); - if (srcId != 0U) { - m_ridLookup->toggleEntry(srcId, false); - } - else { - reply = INVALID_OPT_STR "tried to blacklist RID 0!"; - LogError(LOG_RCON, reply.c_str()); - } - } -#if defined(ENABLE_DMR) - else if (rcom == RCD_DMR_BEACON) { - if (dmr != nullptr) { - if (host->m_dmrBeacons) { - g_fireDMRBeacon = true; - } - else { - reply = RCON_CMD_NACK "DMR beacons is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - reply = RCON_CMD_NACK "DMR mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } -#endif // defined(ENABLE_DMR) -#if defined(ENABLE_P25) - else if (rcom == RCD_P25_CC) { - if (p25 != nullptr) { - if (host->m_p25CCData) { - g_fireP25Control = true; - } - else { - reply = RCON_CMD_NACK "P25 control data is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - reply = RCON_CMD_NACK "P25 mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else if (rcom == RCD_P25_CC_FALLBACK) { - uint8_t fallback = getArgUInt8(args, 0U); - if (p25 != nullptr) { - if (host->m_p25CCData) { - p25->trunk()->setConvFallback((fallback == 1U) ? true : false); - } - else { - reply = RCON_CMD_NACK "P25 control data is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - reply = RCON_CMD_NACK "P25 mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } -#endif // defined(ENABLE_P25) -#if defined(ENABLE_DMR) - else if (rcom == RCD_DMR_RID_PAGE && argCnt >= 2U) { - if (dmr != nullptr) { - uint32_t slotNo = getArgUInt32(args, 0U); - uint32_t dstId = getArgUInt32(args, 1U); - if (slotNo > 0U && slotNo < 3U) { - if (dstId != 0U) { - dmr->writeRF_Call_Alrt(slotNo, p25::P25_WUID_FNE, dstId); - } - else { - reply = INVALID_OPT_STR "tried to DMR call alert RID 0!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - reply = INVALID_OPT_STR "invalid DMR slot number for call alert!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - reply = RCON_CMD_NACK "DMR mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else if (rcom == RCD_DMR_RID_CHECK && argCnt >= 2U) { - if (dmr != nullptr) { - uint32_t slotNo = getArgUInt32(args, 0U); - uint32_t dstId = getArgUInt32(args, 1U); - if (slotNo > 0U && slotNo < 3U) { - if (dstId != 0U) { - dmr->writeRF_Ext_Func(slotNo, dmr::DMR_EXT_FNCT_CHECK, p25::P25_WUID_FNE, dstId); - } - else { - reply = INVALID_OPT_STR "tried to DMR radio check RID 0!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - reply = INVALID_OPT_STR "invalid DMR slot number for radio check!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - reply = RCON_CMD_NACK "DMR mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else if (rcom == RCD_DMR_RID_INHIBIT && argCnt >= 2U) { - if (dmr != nullptr) { - uint32_t slotNo = getArgUInt32(args, 0U); - uint32_t dstId = getArgUInt32(args, 1U); - if (slotNo > 0U && slotNo < 3U) { - if (dstId != 0U) { - dmr->writeRF_Ext_Func(slotNo, dmr::DMR_EXT_FNCT_INHIBIT, p25::P25_WUID_FNE, dstId); - } - else { - reply = INVALID_OPT_STR "tried to DMR radio inhibit RID 0!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - reply = INVALID_OPT_STR "invalid DMR slot number for radio inhibit!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - reply = RCON_CMD_NACK "DMR mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else if (rcom == RCD_DMR_RID_UNINHIBIT && argCnt >= 2U) { - if (dmr != nullptr) { - uint32_t slotNo = getArgUInt32(args, 0U); - uint32_t dstId = getArgUInt32(args, 1U); - if (slotNo > 0U && slotNo < 3U) { - if (dstId != 0U) { - dmr->writeRF_Ext_Func(slotNo, dmr::DMR_EXT_FNCT_UNINHIBIT, p25::P25_WUID_FNE, dstId); - } - else { - reply = INVALID_OPT_STR "tried to DMR radio uninhibit RID 0!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - reply = INVALID_OPT_STR "invalid DMR slot number for radio uninhibit!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - reply = RCON_CMD_NACK "DMR mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } -#endif // defined(ENABLE_DMR) -#if defined(ENABLE_P25) - else if (rcom == RCD_P25_SET_MFID && argCnt >= 1U) { - if (p25 != nullptr) { - uint8_t mfId = getArgUInt8(args, 0U); - if (mfId != 0U) { - LogMessage(LOG_RCON, "Remote P25, mfgId = $%02X", mfId); - m_p25MFId = mfId; - } - else { - LogMessage(LOG_RCON, "Remote P25, mfgId reset, mfgId = $%02X", mfId); - m_p25MFId = p25::P25_MFG_STANDARD; - } - - p25->trunk()->setLastMFId(m_p25MFId); - } - else { - reply = RCON_CMD_NACK "P25 mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else if (rcom == RCD_P25_RID_PAGE && argCnt >= 1U) { - if (p25 != nullptr) { - uint32_t dstId = getArgUInt32(args, 0U); - if (dstId != 0U) { - // FIXME - //p25->trunk()->setMFId(m_p25MFId); - p25->trunk()->writeRF_TSDU_Call_Alrt(p25::P25_WUID_FNE, dstId); - } - else { - reply = INVALID_OPT_STR "tried to P25 call alert RID 0!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - reply = RCON_CMD_NACK "P25 mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else if (rcom == RCD_P25_RID_CHECK && argCnt >= 1U) { - if (p25 != nullptr) { - uint32_t dstId = getArgUInt32(args, 0U); - if (dstId != 0U) { - // FIXME - //p25->trunk()->setMFId(m_p25MFId); - p25->trunk()->writeRF_TSDU_Ext_Func(p25::P25_EXT_FNCT_CHECK, p25::P25_WUID_FNE, dstId); - } - else { - reply = INVALID_OPT_STR "tried to P25 radio check RID 0!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - reply = RCON_CMD_NACK "P25 mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else if (rcom == RCD_P25_RID_INHIBIT && argCnt >= 1U) { - if (p25 != nullptr) { - uint32_t dstId = getArgUInt32(args, 0U); - if (dstId != 0U) { - // FIXME - //p25->trunk()->setMFId(m_p25MFId); - p25->trunk()->writeRF_TSDU_Ext_Func(p25::P25_EXT_FNCT_INHIBIT, p25::P25_WUID_FNE, dstId); - } - else { - reply = INVALID_OPT_STR "tried to P25 inhibit RID 0!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - reply = RCON_CMD_NACK "P25 mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else if (rcom == RCD_P25_RID_UNINHIBIT && argCnt >= 1U) { - if (p25 != nullptr) { - uint32_t dstId = getArgUInt32(args, 0U); - if (dstId != 0U) { - // FIXME - //p25->trunk()->setMFId(m_p25MFId); - p25->trunk()->writeRF_TSDU_Ext_Func(p25::P25_EXT_FNCT_UNINHIBIT, p25::P25_WUID_FNE, dstId); - } - else { - reply = INVALID_OPT_STR "tried to P25 uninhibit RID 0!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - reply = RCON_CMD_NACK "P25 mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else if (rcom == RCD_P25_RID_GAQ && argCnt >= 1U) { - if (p25 != nullptr) { - uint32_t dstId = getArgUInt32(args, 0U); - if (dstId != 0U) { - // FIXME - //p25->trunk()->setMFId(m_p25MFId); - p25->trunk()->writeRF_TSDU_Grp_Aff_Q(dstId); - } - else { - reply = INVALID_OPT_STR "tried to P25 grp aff. query RID 0!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - reply = RCON_CMD_NACK "P25 mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else if (rcom == RCD_P25_RID_UREG && argCnt >= 1U) { - if (p25 != nullptr) { - uint32_t dstId = getArgUInt32(args, 0U); - if (dstId != 0U) { - // FIXME - //p25->trunk()->setMFId(m_p25MFId); - p25->trunk()->writeRF_TSDU_U_Reg_Cmd(dstId); - } - else { - reply = INVALID_OPT_STR "tried to P25 unit reg. command RID 0!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - reply = RCON_CMD_NACK "P25 mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else if (rcom == RCD_P25_RELEASE_GRANTS) { - if (p25 != nullptr) { - p25->affiliations().releaseGrant(0, true); - } - else { - reply = RCON_CMD_NACK "P25 mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else if (rcom == RCD_P25_RELEASE_AFFS) { - if (p25 != nullptr) { - uint32_t grp = getArgUInt32(args, 0U); - - if (grp == 0) { - p25->affiliations().clearGroupAff(0, true); - } - else { - p25->affiliations().clearGroupAff(grp, false); - } - } - else { - reply = RCON_CMD_NACK "P25 mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } -#endif // defined(ENABLE_P25) -#if defined(ENABLE_DMR) - else if (rcom == RCD_DMR_CC_DEDICATED) { - if (dmr != nullptr) { - if (host->m_dmrTSCCData) { - if (p25 != nullptr) { - reply = RCON_CMD_NACK "Can't enable DMR control channel while P25 is enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - else { - host->m_dmrCtrlChannel = !host->m_dmrCtrlChannel; - reply = string_format("DMR CC is %s", host->m_p25CtrlChannel ? "enabled" : "disabled"); - LogInfoEx(LOG_RCON, reply.c_str()); - } - } - else { - reply = RCON_CMD_NACK "DMR control data is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - reply = RCON_CMD_NACK "DMR mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else if (rcom == RCD_DMR_CC_BCAST) { - if (dmr != nullptr) { - host->m_dmrTSCCData = !host->m_dmrTSCCData; - reply = string_format("DMR CC broadcast is %s", host->m_dmrTSCCData ? "enabled" : "disabled"); - LogInfoEx(LOG_RCON, reply.c_str()); - } - else { - reply = RCON_CMD_NACK "DMR mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } -#endif // defined(ENABLE_DMR) -#if defined(ENABLE_P25) - else if (rcom == RCD_P25_CC_DEDICATED) { - if (p25 != nullptr) { - if (host->m_p25CCData) { - if (dmr != nullptr) { - reply = RCON_CMD_NACK "Can't enable P25 control channel while DMR is enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - else { - host->m_p25CtrlChannel = !host->m_p25CtrlChannel; - host->m_p25CtrlBroadcast = true; - g_fireP25Control = true; - p25->setCCHalted(false); - - reply = string_format("P25 CC is %s", host->m_p25CtrlChannel ? "enabled" : "disabled"); - LogInfoEx(LOG_RCON, reply.c_str()); - } - } - else { - reply = RCON_CMD_NACK "P25 control data is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - reply = RCON_CMD_NACK "P25 mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else if (rcom == RCD_P25_CC_BCAST) { - if (p25 != nullptr) { - if (host->m_p25CCData) { - host->m_p25CtrlBroadcast = !host->m_p25CtrlBroadcast; - - if (!host->m_p25CtrlBroadcast) { - g_fireP25Control = false; - p25->setCCHalted(true); - } - else { - g_fireP25Control = true; - p25->setCCHalted(false); - } - - reply = string_format("P25 CC broadcast is %s", host->m_p25CtrlBroadcast ? "enabled" : "disabled"); - LogInfoEx(LOG_RCON, reply.c_str()); - } - else { - reply = RCON_CMD_NACK "P25 control data is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - reply = RCON_CMD_NACK "P25 mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } -#endif // defined(ENABLE_P25) -#if defined(ENABLE_DMR) - else if (rcom == RCD_DMR_DEBUG) { - if (argCnt < 2U) { - if (dmr != nullptr) { - bool debug = dmr->getDebug(); - bool verbose = dmr->getVerbose(); - reply = string_format("dmr->debug = %u, dmr->verbose = %u", debug, verbose); - } - else { - reply = RCON_CMD_NACK "DMR mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - uint8_t debug = getArgUInt8(args, 0U); - uint8_t verbose = getArgUInt8(args, 1U); - if (dmr != nullptr) { - dmr->setDebugVerbose((debug == 1U) ? true : false, (verbose == 1U) ? true : false); - } - else { - reply = RCON_CMD_NACK "DMR mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - } - else if (rcom == RCD_DMR_DUMP_CSBK) { - if (argCnt < 1U) { - if (dmr != nullptr) { - bool csbkDump = dmr->getCSBKVerbose(); - reply = string_format("dmr->dumpCsbkData = %u", csbkDump); - } - else { - reply = RCON_CMD_NACK "DMR mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - uint8_t verbose = getArgUInt8(args, 0U); - if (dmr != nullptr) { - dmr->setCSBKVerbose((verbose == 1U) ? true : false); - } - else { - reply = RCON_CMD_NACK "DMR mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - } -#endif // defined(ENABLE_DMR) -#if defined(ENABLE_P25) - else if (rcom == RCD_P25_DEBUG) { - if (argCnt < 2U) { - if (p25 != nullptr) { - bool debug = p25->getDebug(); - bool verbose = p25->getVerbose(); - reply = string_format("p25->debug = %u, p25->verbose = %u", debug, verbose); - } - else { - reply = RCON_CMD_NACK "P25 mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - uint8_t debug = getArgUInt8(args, 0U); - uint8_t verbose = getArgUInt8(args, 1U); - if (p25 != nullptr) { - p25->setDebugVerbose((debug == 1U) ? true : false, (verbose == 1U) ? true : false); - } - else { - reply = RCON_CMD_NACK "P25 mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - } - else if (rcom == RCD_P25_DUMP_TSBK) { - if (argCnt < 1U) { - if (p25 != nullptr) { - bool tsbkDump = p25->trunk()->getTSBKVerbose(); - reply = string_format("p25->dumpTsbkData = %u", tsbkDump); - } - else { - reply = RCON_CMD_NACK "P25 mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - uint8_t verbose = getArgUInt8(args, 0U); - if (p25 != nullptr) { - p25->trunk()->setTSBKVerbose((verbose == 1U) ? true : false); - } - else { - reply = RCON_CMD_NACK "P25 mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - } -#endif // defined(ENABLE_P25) -#if defined(ENABLE_NXDN) - else if (rcom == RCD_NXDN_DEBUG) { - if (argCnt < 2U) { - if (nxdn != nullptr) { - bool debug = nxdn->getDebug(); - bool verbose = nxdn->getVerbose(); - reply = string_format("nxdn->debug = %u, nxdn->verbose = %u", debug, verbose); - } - else { - reply = RCON_CMD_NACK "NXDN mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - uint8_t debug = getArgUInt8(args, 0U); - uint8_t verbose = getArgUInt8(args, 1U); - if (nxdn != nullptr) { - nxdn->setDebugVerbose((debug == 1U) ? true : false, (verbose == 1U) ? true : false); - } - else { - reply = RCON_CMD_NACK "NXDN mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - } - else if (rcom == RCD_NXDN_DUMP_RCCH) { - if (argCnt < 1U) { - if (nxdn != nullptr) { - bool rcchDump = nxdn->getRCCHVerbose(); - reply = string_format("nxdn->dumpRcchData = %u", rcchDump); - } - else { - reply = RCON_CMD_NACK "NXDN mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - uint8_t verbose = getArgUInt8(args, 0U); - if (nxdn != nullptr) { - nxdn->setRCCHVerbose((verbose == 1U) ? true : false); - } - else { - reply = RCON_CMD_NACK "NXDN mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } - } -#endif // defined(ENABLE_NXDN) -#if defined(ENABLE_DMR) - else if (rcom == RCD_DMRD_MDM_INJ && argCnt >= 1U) { - reply = rcdDMRModemInj(args, host, dmr); - } -#endif // defined(ENABLE_DMR) -#if defined(ENABLE_P25) - else if (rcom == RCD_P25D_MDM_INJ && argCnt >= 1U) { - reply = rcdP25ModemInj(args, host, p25); - } -#endif // defined(ENABLE_P25) -#if defined(ENABLE_NXDN) - else if (rcom == RCD_NXDD_MDM_INJ && argCnt >= 1U) { - reply = rcdNXDNModemInj(args, host, nxdn); - } -#endif // defined(ENABLE_NXDN) - else if (rcom == RCD_PING) { - reply = CMD_ACK; - } - else { - args.clear(); - reply = BAD_CMD_STR; - LogError(LOG_RCON, BAD_CMD_STR " (\"%s\")", rcom.c_str()); - } - } - - // write response - writeResponse(reply, address, addrLen); - } -} - -/// -/// Opens connection to the network. -/// -/// -bool RemoteControl::open() -{ - initializeEndpoints(); - m_restServer.setHandler(m_dispatcher); - - run(); - return m_socket.open(); -} - -/// -/// Closes connection to the network. -/// -void RemoteControl::close() -{ - m_restServer.stop(); - wait(); - - m_socket.close(); -} - -// --------------------------------------------------------------------------- -// Private Class Members -// --------------------------------------------------------------------------- - -/// -/// Helper to initialize REST API endpoints. -/// -void RemoteControl::initializeEndpoints() -{ - m_dispatcher.match("/version") - .get([](HTTPReply& reply, const RequestMatch& match) { - json::object response = json::object(); - response["version"].set(std::string((__PROG_NAME__ " " __VER__ " (" DESCR_DMR DESCR_P25 DESCR_NXDN "CW Id, Network) (built " __BUILD__ ")"))); - - reply.reply(response); - }); -} - -/// -/// -/// -void RemoteControl::entry() -{ - m_restServer.run(); -} - -/// -/// Helper to write response to client. -/// -/// -/// -/// -/// -void RemoteControl::writeResponse(std::string reply, sockaddr_storage address, uint32_t addrLen) -{ - uint8_t buffer[RC_BUFFER_LENGTH]; - ::memset(buffer, 0x00U, RC_BUFFER_LENGTH); - - buffer[0U] = RCON_FRAME_START; - buffer[1U] = START_OF_TEXT; - - LogInfoEx(LOG_RCON, "reply len = %u, blocks = %u to %s", reply.length(), (reply.length() / (RC_BUFFER_LENGTH - 3U)) + 1U, UDPSocket::address(address).c_str()); - - if (reply.length() > (RC_BUFFER_LENGTH - 3U)) { - uint32_t len = reply.length(), offs = 0U; - for (uint32_t i = 0U; i < reply.length() / (RC_BUFFER_LENGTH - 3U); i++) { - if (m_debug) - LogDebug(LOG_RCON, "RemoteControl::writeResponse() block = %u, block len = %u, offs = %u", i, len, offs); - - std::string str = reply.substr(offs, RC_BUFFER_LENGTH - 3U); - - ::memset(buffer + 2U, 0x00U, str.length()); - ::memcpy(buffer + 2U, str.c_str(), str.length() + 1U); - - if (len - RC_BUFFER_LENGTH == 0U) { - buffer[str.length() + 2U] = END_OF_TEXT; - } - else { - buffer[str.length() + 2U] = END_OF_BLOCK; - } - - if (m_debug) - Utils::dump(1U, "RCON (Multiblock) Sent", (uint8_t*)buffer, RC_BUFFER_LENGTH); - - m_socket.write(buffer, RC_BUFFER_LENGTH, address, addrLen); - - Thread::sleep(50); - - offs += RC_BUFFER_LENGTH - 3U; - len -= RC_BUFFER_LENGTH - 3U; - } - - if (m_debug) - LogDebug(LOG_RCON, "RemoteControl::writeResponse() remaining block len = %u, offs = %u", len, offs); - - if (len > 0U) { - std::string str = reply.substr(offs, std::string::npos); - - ::memset(buffer + 2U, 0x00U, RC_BUFFER_LENGTH - 3U); - ::memcpy(buffer + 2U, str.c_str(), str.length() + 1U); - - buffer[str.length() + 2U] = END_OF_TEXT; - - if (m_debug) - Utils::dump(1U, "RCON (Multiblock) Sent", (uint8_t*)buffer, str.length() + 4U); - - m_socket.write(buffer, str.length() + 4U, address, addrLen); - } - } - else { - ::memcpy(buffer + 2U, reply.c_str(), reply.length() + 1U); - buffer[reply.length() + 2U] = END_OF_TEXT; - - if (m_debug) { - LogDebug(LOG_RCON, "RemoteControl::writeResponse() single block len = %u", reply.length() + 4U); - Utils::dump(1U, "RCON Sent", (uint8_t*)buffer, reply.length() + 4U); - } - - m_socket.write(buffer, reply.length() + 3U, address, addrLen); - } -} - -/// -/// Helper to print the remote control help. -/// -/// -std::string RemoteControl::displayHelp() -{ - std::string reply = ""; - - reply += "RCON Help\r\nGeneral Commands:\r\n"; - reply += " version Display current version of host\r\n"; - reply += " help Displays RCON help\r\n"; - reply += " status Display current settings and operation mode\r\n"; - reply += " voice-ch Retrieves the list of configured voice channels\r\n"; - reply += "\r\n"; - reply += " mdm-mode Set current mode of host (idle, lockout, dmr, p25, nxdn)\r\n"; - reply += " mdm-kill Causes the host to quit\r\n"; - reply += " mdm-force-kill Causes the host to quit immediately\r\n"; - reply += "\r\n"; - reply += " permit-tg Causes the host to permit the specified destination ID if non-authoritative\r\n"; - reply += " grant-tg Causes the host to grant the specified destination ID if non-authoritative\r\n"; - reply += "\r\n"; - reply += " rid-whitelist Whitelists the specified RID in the host ACL tables\r\n"; - reply += " rid-blacklist Blacklists the specified RID in the host ACL tables\r\n"; - reply += "\r\n"; -#if defined(ENABLE_DMR) - reply += " dmr-beacon Transmits a DMR beacon burst\r\n"; -#endif // defined(ENABLE_DMR) -#if defined(ENABLE_P25) - reply += " p25-cc Transmits a non-continous P25 CC burst\r\n"; - reply += " p25-cc-fallback <0/1> Sets the P25 CC into conventional fallback mode\r\n"; -#endif // defined(ENABLE_P25) - reply += "\r\n"; -#if defined(ENABLE_DMR) - reply += " dmr-debug \r\n"; - reply += " dmr-dump-csbk <0/1>\r\n"; -#endif // defined(ENABLE_DMR) -#if defined(ENABLE_P25) - reply += " p25-debug \r\n"; - reply += " p25-dump-tsbk <0/1>\r\n"; -#endif // defined(ENABLE_P25) -#if defined(ENABLE_NXDN) - reply += " nxdn-debug \r\n"; - reply += " nxdn-dump-rcch <0/1>\r\n"; -#endif // defined(ENABLE_NXDN) -#if defined(ENABLE_DMR) - reply += "\r\nDMR Commands:\r\n"; - reply += " dmr-rid-page Pages/Calls the specified RID\r\n"; - reply += " dmr-rid-check Radio Checks the specified RID\r\n"; - reply += " dmr-rid-inhibit Inhibits the specified RID\r\n"; - reply += " dmr-rid-uninhibit Uninhibits the specified RID\r\n"; - reply += "\r\n"; - reply += " dmr-cc-dedicated <0/1> Enables or disables dedicated control channel\r\n"; - reply += " dmr-cc-bcast <0/1> Enables or disables broadcast of the control channel\r\n"; -#endif // defined(ENABLE_DMR) -#if defined(ENABLE_P25) - reply += "\r\nP25 Commands:\r\n"; - reply += " p25-set-mfid Sets the P25 MFId for the next sent P25 command\r\n"; - reply += " p25-rid-page Pages/Calls the specified RID\r\n"; - reply += " p25-rid-check Radio Checks the specified RID\r\n"; - reply += " p25-rid-inhibit Inhibits the specified RID\r\n"; - reply += " p25-rid-uninhibit Uninhibits the specified RID\r\n"; - reply += " p25-rid-gaq Group affiliation queries the specified RID\r\n"; - reply += " p25-rid-ureg Demand unit registration for the specified RID\r\n"; - reply += "\r\n"; - reply += " p25-rel-grnts Forcibly releases all channel grants for P25\r\n"; - reply += " p25-rel-affs Forcibly releases all group affiliations for P25\r\n"; - reply += "\r\n"; - reply += " p25-cc-dedicated <0/1> Enables or disables dedicated control channel\r\n"; - reply += " p25-cc-bcast <0/1> Enables or disables broadcast of the control channel\r\n"; -#endif // defined(ENABLE_P25) - return reply; -} - -/// -/// -/// -/// Instance of the Host class. -/// Instance of the DMR Control class. -/// Instance of the P25 Control class. -/// Instance of the NXDN Control class. -/// -std::string RemoteControl::rcdGetStatus(Host* host, dmr::Control* dmr, p25::Control* p25, nxdn::Control* nxdn) -{ - std::string reply = ""; - yaml::Node systemConf = host->m_conf["system"]; - { - yaml::Node modemConfig = host->m_conf["system"]["modem"]; - std::string type = modemConfig["protocol"]["type"].as(); - - yaml::Node uartConfig = modemConfig["protocol"]["uart"]; - std::string modemPort = uartConfig["port"].as(); - uint32_t portSpeed = uartConfig["speed"].as(115200U); - - reply += string_format("Host State: %u, DMR: %u, P25: %u, NXDN: %u, Port Type: %s, Modem Port: %s, Port Speed: %u, Proto Ver: %u", host->m_state, - dmr != nullptr, p25 != nullptr, nxdn != nullptr, type.c_str(), modemPort.c_str(), portSpeed, host->m_modem->getVersion()); - reply += string_format("\r\nDMR CC: %u, P25 CC: %u, NXDN CC: %u", host->m_dmrCtrlChannel, host->m_p25CtrlChannel, host->m_nxdnCtrlChannel); - } - - { - if (!host->m_modem->isHotspot()) { - reply += string_format("\r\nPTT Invert: %s, RX Invert: %s, TX Invert: %s, DC Blocker: %s", - host->m_modem->m_pttInvert ? "yes" : "no", host->m_modem->m_rxInvert ? "yes" : "no", host->m_modem->m_txInvert ? "yes" : "no", host->m_modem->m_dcBlocker ? "yes" : "no"); - } - reply += string_format("\r\nRX Level: %.1f%%, CW TX Level: %.1f%%, DMR TX Level: %.1f%%, P25 TX Level: %.1f%%, NXDN TX Level: %.1f%%, TX DC Offset: %d, RX DC Offset: %d", - host->m_modem->m_rxLevel, host->m_modem->m_cwIdTXLevel, host->m_modem->m_dmrTXLevel, host->m_modem->m_p25TXLevel, host->m_modem->m_nxdnTXLevel, host->m_modem->m_txDCOffset, host->m_modem->m_rxDCOffset); - if (!host->m_modem->isHotspot()) { - reply += string_format("\r\nDMR Symbol +/- 3 Level Adj.: %d, DMR Symbol +/- 1 Level Adj.: %d, P25 Symbol +/- 3 Level Adj.: %d, P25 Symbol +/- 1 Level Adj.: %d", - host->m_modem->m_dmrSymLevel3Adj, host->m_modem->m_dmrSymLevel1Adj, host->m_modem->m_p25SymLevel3Adj, host->m_modem->m_p25SymLevel1Adj); - - // are we on a protocol version 3 firmware? - if (host->m_modem->getVersion() >= 3U) { - reply += string_format("\r\nNXDN Symbol +/- 3 Level Adj.: %d, NXDN Symbol +/- 1 Level Adj.: %d", - host->m_modem->m_nxdnSymLevel3Adj, host->m_modem->m_nxdnSymLevel1Adj); - } - } - if (host->m_modem->isHotspot()) { - reply += string_format("\r\nDMR Disc. BW: %d, P25 Disc. BW: %d, DMR Post Demod BW: %d, P25 Post Demod BW: %d", - host->m_modem->m_dmrDiscBWAdj, host->m_modem->m_p25DiscBWAdj, host->m_modem->m_dmrPostBWAdj, host->m_modem->m_p25PostBWAdj); - - // are we on a protocol version 3 firmware? - if (host->m_modem->getVersion() >= 3U) { - reply += string_format("\r\nNXDN Disc. BW: %d, NXDN Post Demod BW: %d", - host->m_modem->m_nxdnDiscBWAdj, host->m_modem->m_nxdnPostBWAdj); - - reply += string_format("\r\nAFC Enabled: %u, AFC KI: %u, AFC KP: %u, AFC Range: %u", - host->m_modem->m_afcEnable, host->m_modem->m_afcKI, host->m_modem->m_afcKP, host->m_modem->m_afcRange); - } - - switch (host->m_modem->m_adfGainMode) { - case ADF_GAIN_AUTO_LIN: - reply += "\r\nADF7021 Gain Mode: Auto High Linearity"; - break; - case ADF_GAIN_LOW: - reply += "\r\nADF7021 Gain Mode: Low"; - break; - case ADF_GAIN_HIGH: - reply += "\r\nADF7021 Gain Mode: High"; - break; - case ADF_GAIN_AUTO: - default: - reply += "\r\nADF7021 Gain Mode: Auto"; - break; - } - } - reply += string_format("\r\nFDMA Preambles: %u (%.1fms), DMR Rx Delay: %u (%.1fms), P25 Corr. Count: %u (%.1fms)", host->m_modem->m_fdmaPreamble, float(host->m_modem->m_fdmaPreamble) * 0.2222F, - host->m_modem->m_dmrRxDelay, float(host->m_modem->m_dmrRxDelay) * 0.0416666F, host->m_modem->m_p25CorrCount, float(host->m_modem->m_p25CorrCount) * 0.667F); - reply += string_format("\r\nRx Freq: %uHz, Tx Freq: %uHz, Rx Offset: %dHz, Tx Offset: %dHz", host->m_modem->m_rxFrequency, host->m_modem->m_txFrequency, host->m_modem->m_rxTuning, host->m_modem->m_txTuning); - reply += string_format("\r\nRx Effective Freq: %uHz, Tx Effective Freq: %uHz", host->m_modem->m_rxFrequency + host->m_modem->m_rxTuning, host->m_modem->m_txFrequency + host->m_modem->m_txTuning); - } - - return reply; -} - -/// -/// -/// -/// Instance of the Host class. -/// Instance of the DMR Control class. -/// Instance of the P25 Control class. -/// Instance of the NXDN Control class. -/// -std::string RemoteControl::rcdGetVoiceCh(Host* host, dmr::Control* dmr, p25::Control* p25, nxdn::Control* nxdn) -{ - std::string reply = ""; - - if (host->m_voiceChData.size() > 0) { - for (auto entry : host->m_voiceChData) { - uint32_t chNo = entry.first; - lookups::VoiceChData data = entry.second; - - reply += string_format("\r\n%u, %s:%u", chNo, data.address().c_str(), data.port()); - } - } - else { - reply = NO_DATA_CMD_STR; - } - - return reply; -} - -/// -/// -/// -/// -/// Instance of the Host class. -/// Instance of the DMR Control class. -/// Instance of the P25 Control class. -/// Instance of the NXDN Control class. -/// -std::string RemoteControl::rcdMode(std::vector args, Host* host, dmr::Control* dmr, p25::Control* p25, nxdn::Control* nxdn) -{ - std::string reply = ""; - std::string mode = getArgString(args, 0U); - if (mode == RCD_MODE_OPT_IDLE) { - host->m_fixedMode = false; - host->setState(STATE_IDLE); - - reply = string_format("Dynamic mode, mode %u", host->m_state); - LogInfoEx(LOG_RCON, reply.c_str()); - } - else if (mode == RCD_MODE_OPT_LCKOUT) { - host->m_fixedMode = false; - host->setState(HOST_STATE_LOCKOUT); - reply = string_format("Lockout mode, mode %u", host->m_state); - LogInfoEx(LOG_RCON, reply.c_str()); - } -#if defined(ENABLE_DMR) - else if (mode == RCD_MODE_OPT_FDMR) { - if (dmr != nullptr) { - host->m_fixedMode = true; - host->setState(STATE_DMR); - reply = string_format("Fixed mode, mode %u", host->m_state); - LogInfoEx(LOG_RCON, reply.c_str()); - } - else { - reply = RCON_CMD_NACK "DMR mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } -#endif // defined(ENABLE_DMR) -#if defined(ENABLE_P25) - else if (mode == RCD_MODE_OPT_FP25) { - if (p25 != nullptr) { - host->m_fixedMode = true; - host->setState(STATE_P25); - reply = string_format("Fixed mode, mode %u", host->m_state); - LogInfoEx(LOG_RCON, reply.c_str()); - } - else { - reply = RCON_CMD_NACK "P25 mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } -#endif // defined(ENABLE_P25) -#if defined(ENABLE_NXDN) - else if (mode == RCD_MODE_OPT_FNXDN) { - if (nxdn != nullptr) { - host->m_fixedMode = true; - host->setState(STATE_NXDN); - reply = string_format("Fixed mode, mode %u", host->m_state); - LogInfoEx(LOG_RCON, reply.c_str()); - } - else { - reply = RCON_CMD_NACK "NXDN mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } -#endif // defined(ENABLE_NXDN) - else { - reply = INVALID_OPT_STR "invalid mode!"; - LogError(LOG_RCON, reply.c_str()); - } - - return reply; -} - -/// -/// -/// -/// -/// Instance of the Host class. -/// Instance of the DMR Control class. -/// Instance of the P25 Control class. -/// Instance of the NXDN Control class. -/// -std::string RemoteControl::rcdPermitTG(std::vector args, Host* host, dmr::Control* dmr, p25::Control* p25, nxdn::Control* nxdn) -{ - std::string reply = ""; - if (!host->m_authoritative) { - reply = RCON_CMD_NACK "Host is authoritative, cannot permit TG!"; - LogError(LOG_RCON, reply.c_str()); - return reply; - } - - DVM_STATE state = (DVM_STATE)getArgInt32(args, 0U); - uint32_t dstId = getArgInt32(args, 1U); - if (dstId == 0U) { - reply = string_format(INVALID_OPT_STR "illegal TGID permitted (%u)", dstId); - LogError(LOG_RCON, reply.c_str()); - return reply; - } - - switch (state) { - case STATE_DMR: -#if defined(ENABLE_DMR) - { - uint8_t slot = getArgInt8(args, 2U); - if (slot == 0U) { - reply = INVALID_OPT_STR "illegal slot"; - LogError(LOG_RCON, reply.c_str()); - return reply; - } - - if (dmr != nullptr) { - dmr->permittedTG(dstId, slot); - } - else { - reply = RCON_CMD_NACK "DMR mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } -#else - { - reply = INVALID_OPT_STR "invalid mode!"; - LogError(LOG_RCON, reply.c_str()); - } -#endif // defined(ENABLE_DMR) - break; - case STATE_P25: -#if defined(ENABLE_P25) - { - if (p25 != nullptr) { - p25->permittedTG(dstId); - } - else { - reply = RCON_CMD_NACK "P25 mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } -#else - { - reply = INVALID_OPT_STR "invalid mode!"; - LogError(LOG_RCON, reply.c_str()); - } -#endif // defined(ENABLE_P25) - break; - case STATE_NXDN: -#if defined(ENABLE_NXDN) - { - if (nxdn != nullptr) { - nxdn->permittedTG(dstId); - } - else { - reply = RCON_CMD_NACK "NXDN mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } -#else - { - reply = INVALID_OPT_STR "invalid mode!"; - LogError(LOG_RCON, reply.c_str()); - } -#endif // defined(ENABLE_NXDN) - break; - default: - reply = INVALID_OPT_STR "invalid mode!"; - LogError(LOG_RCON, reply.c_str()); - break; - } - - return reply; -} - -/// -/// -/// -/// -/// Instance of the Host class. -/// Instance of the DMR Control class. -/// Instance of the P25 Control class. -/// Instance of the NXDN Control class. -/// -std::string RemoteControl::rcdGrantTG(std::vector args, Host* host, dmr::Control* dmr, p25::Control* p25, nxdn::Control* nxdn) -{ - std::string reply = ""; - if (host->m_authoritative && (host->m_dmrCtrlChannel || host->m_p25CtrlChannel || host->m_nxdnCtrlChannel)) { - reply = RCON_CMD_NACK "Host is authoritative, cannot grant TG!"; - LogError(LOG_RCON, reply.c_str()); - return reply; - } - - DVM_STATE state = (DVM_STATE)getArgInt32(args, 0U); - uint32_t dstId = getArgInt32(args, 1U); - uint8_t unitToUnit = getArgInt32(args, 2U); - if (unitToUnit > 1U) { - reply = string_format(INVALID_OPT_STR "illegal TGID granted (%u)", dstId); - LogError(LOG_RCON, reply.c_str()); - return reply; - } - - if (dstId == 0U) { - reply = string_format(INVALID_OPT_STR "illegal TGID granted (%u)", dstId); - LogError(LOG_RCON, reply.c_str()); - return reply; - } - - switch (state) { - case STATE_DMR: -#if defined(ENABLE_DMR) - { - uint8_t slot = getArgInt8(args, 2U); - if (slot == 0U) { - reply = INVALID_OPT_STR "illegal slot"; - LogError(LOG_RCON, reply.c_str()); - return reply; - } - - if (dmr != nullptr) { - // TODO TODO - //dmr->grantTG(dstId, slot, unitToUnit == 1U); - } - else { - reply = RCON_CMD_NACK "DMR mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } -#else - { - reply = INVALID_OPT_STR "invalid mode!"; - LogError(LOG_RCON, reply.c_str()); - } -#endif // defined(ENABLE_DMR) - break; - case STATE_P25: -#if defined(ENABLE_P25) - { - if (p25 != nullptr) { - // TODO TODO - //p25->grantTG(dstId, unitToUnit == 1U); - } - else { - reply = RCON_CMD_NACK "P25 mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } -#else - { - reply = INVALID_OPT_STR "invalid mode!"; - LogError(LOG_RCON, reply.c_str()); - } -#endif // defined(ENABLE_P25) - break; - case STATE_NXDN: -#if defined(ENABLE_NXDN) - { - if (nxdn != nullptr) { - // TODO TODO - //nxdn->grantTG(dstId, unitToUnit == 1U); - } - else { - reply = RCON_CMD_NACK "NXDN mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - } -#else - { - reply = INVALID_OPT_STR "invalid mode!"; - LogError(LOG_RCON, reply.c_str()); - } -#endif // defined(ENABLE_NXDN) - break; - default: - reply = INVALID_OPT_STR "invalid mode!"; - LogError(LOG_RCON, reply.c_str()); - break; - } - - return reply; -} - -/// -/// -/// -/// -/// Instance of the Host class. -/// Instance of the DMR Control class. -/// Instance of the P25 Control class. -/// Instance of the NXDN Control class. -/// -std::string RemoteControl::rcdDMRModemInj(std::vector args, Host* host, dmr::Control* dmr) -{ - std::string reply = ""; - - if (dmr != nullptr) { - uint8_t slot = getArgUInt32(args, 0U); - std::string argString = getArgString(args, 1U); - const char* fileName = argString.c_str(); - if (fileName != nullptr) { - FILE* file = ::fopen(fileName, "r"); - if (file != nullptr) { - uint8_t* buffer = nullptr; - int32_t fileSize = 0; - - // obtain file size - ::fseek(file, 0, SEEK_END); - fileSize = ::ftell(file); - ::rewind(file); - - // allocate a buffer and read file - buffer = new uint8_t[fileSize]; - if (buffer != nullptr) { - int32_t bytes = ::fread(buffer, 1U, fileSize, file); - if (bytes == fileSize) { - uint8_t sync[dmr::DMR_SYNC_LENGTH_BYTES]; - ::memcpy(sync, buffer, dmr::DMR_SYNC_LENGTH_BYTES); - - // count data sync errors - uint8_t dataErrs = 0U; - for (uint8_t i = 0U; i < dmr::DMR_SYNC_LENGTH_BYTES; i++) - dataErrs += Utils::countBits8(sync[i] ^ dmr::DMR_MS_DATA_SYNC_BYTES[i]); - - // count voice sync errors - uint8_t voiceErrs = 0U; - for (uint8_t i = 0U; i < dmr::DMR_SYNC_LENGTH_BYTES; i++) - voiceErrs += Utils::countBits8(sync[i] ^ dmr::DMR_MS_VOICE_SYNC_BYTES[i]); - - if ((dataErrs <= 4U) || (voiceErrs <= 4U)) { - if (slot == 0U) { - host->m_modem->injectDMRData1(buffer, fileSize); - } - else if (slot == 1U) { - host->m_modem->injectDMRData2(buffer, fileSize); - } - else { - reply = RCON_CMD_NACK "invalid DMR slot!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - reply = RCON_CMD_NACK "DMR data has too many errors!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - reply = RCON_CMD_NACK "DMR failed to open DMR data!"; - LogError(LOG_RCON, reply.c_str()); - } - - delete[] buffer; - } - - ::fclose(file); - } - } - } - else { - reply = RCON_CMD_NACK "DMR mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - - return reply; -} - -/// -/// -/// -/// -/// Instance of the Host class. -/// Instance of the P25 Control class. -/// -std::string RemoteControl::rcdP25ModemInj(std::vector args, Host* host, p25::Control* p25) -{ - std::string reply = ""; - - if (p25 != nullptr) { - std::string argString = getArgString(args, 0U); - const char* fileName = argString.c_str(); - if (fileName != nullptr) { - FILE* file = ::fopen(fileName, "r"); - if (file != nullptr) { - uint8_t* buffer = nullptr; - int32_t fileSize = 0; - - // obtain file size - ::fseek(file, 0, SEEK_END); - fileSize = ::ftell(file); - ::rewind(file); - - // allocate a buffer and read file - buffer = new uint8_t[fileSize]; - if (buffer != nullptr) { - int32_t bytes = ::fread(buffer, 1U, fileSize, file); - if (bytes == fileSize) { - uint8_t sync[p25::P25_SYNC_LENGTH_BYTES]; - ::memcpy(sync, buffer, p25::P25_SYNC_LENGTH_BYTES); - - uint8_t errs = 0U; - for (uint8_t i = 0U; i < p25::P25_SYNC_LENGTH_BYTES; i++) - errs += Utils::countBits8(sync[i] ^ p25::P25_SYNC_BYTES[i]); - - if (errs <= 4U) { - bool valid = p25->nid().decode(buffer); - if (valid) { - host->m_modem->injectP25Data(buffer, fileSize); - } - else { - reply = RCON_CMD_NACK "P25 data did not contain a valid NID!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - reply = RCON_CMD_NACK "P25 data has too many errors!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - reply = RCON_CMD_NACK "P25 failed to open P25 data!"; - LogError(LOG_RCON, reply.c_str()); - } - - delete[] buffer; - } - - ::fclose(file); - } - } - } - else { - reply = RCON_CMD_NACK "P25 mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - - return reply; -} - -/// -/// -/// -/// -/// Instance of the Host class. -/// Instance of the NXDN Control class. -/// -std::string RemoteControl::rcdNXDNModemInj(std::vector args, Host* host, nxdn::Control* nxdn) -{ - std::string reply = ""; - - if (nxdn != nullptr) { - std::string argString = getArgString(args, 0U); - const char* fileName = argString.c_str(); - if (fileName != nullptr) { - FILE* file = ::fopen(fileName, "r"); - if (file != nullptr) { - uint8_t* buffer = nullptr; - int32_t fileSize = 0; - - // obtain file size - ::fseek(file, 0, SEEK_END); - fileSize = ::ftell(file); - ::rewind(file); - - // allocate a buffer and read file - buffer = new uint8_t[fileSize]; - if (buffer != nullptr) { - int32_t bytes = ::fread(buffer, 1U, fileSize, file); - if (bytes == fileSize) { - uint8_t sync[nxdn::NXDN_FSW_BYTES_LENGTH]; - ::memcpy(sync, buffer, nxdn::NXDN_FSW_BYTES_LENGTH); - - uint8_t errs = 0U; - for (uint8_t i = 0U; i < nxdn::NXDN_FSW_BYTES_LENGTH; i++) - errs += Utils::countBits8(sync[i] ^ nxdn::NXDN_FSW_BYTES[i]); - - if (errs <= 4U) { - host->m_modem->injectNXDNData(buffer, fileSize); - } - else { - reply = RCON_CMD_NACK "NXDN data has too many errors!"; - LogError(LOG_RCON, reply.c_str()); - } - } - else { - reply = RCON_CMD_NACK "NXDN failed to open NXDN data!"; - LogError(LOG_RCON, reply.c_str()); - } - - delete[] buffer; - } - - ::fclose(file); - } - } - } - else { - reply = RCON_CMD_NACK "NXDN mode is not enabled!"; - LogError(LOG_RCON, reply.c_str()); - } - - return reply; -} - -/// -/// -/// -/// -/// -/// -std::string RemoteControl::getArgString(std::vector args, uint32_t n) const -{ - n += 1; - if (n >= args.size()) - return ""; - - return args.at(n); -} diff --git a/network/RemoteControl.h b/network/RemoteControl.h deleted file mode 100644 index ea037b14..00000000 --- a/network/RemoteControl.h +++ /dev/null @@ -1,214 +0,0 @@ -/** -* Digital Voice Modem - Host Software -* GPLv2 Open Source. Use is subject to license terms. -* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. -* -* @package DVM / Host Software -* -*/ -// -// Based on code from the MMDVMHost project. (https://github.com/g4klx/MMDVMHost) -// Licensed under the GPLv2 License (https://opensource.org/licenses/GPL-2.0) -// -/* -* Copyright (C) 2019 by Jonathan Naylor G4KLX -* Copyright (C) 2019-2023 by Bryan Biedenkapp N2PLL -* -* 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., 675 Mass Ave, Cambridge, MA 02139, USA. -*/ -#if !defined(__REMOTE_CONTROL_H__) -#define __REMOTE_CONTROL_H__ - -#include "Defines.h" -#include "network/UDPSocket.h" -#include "network/rest/RequestDispatcher.h" -#include "network/rest/http/HTTPServer.h" -#include "lookups/RadioIdLookup.h" -#include "lookups/TalkgroupIdLookup.h" -#include "Thread.h" - -#include -#include - -// --------------------------------------------------------------------------- -// Constants -// --------------------------------------------------------------------------- - -#define RCD_GET_VERSION "version" -#define RCD_GET_HELP "help" -#define RCD_GET_STATUS "status" -#define RCD_GET_VOICE_CH "voice-ch" - -#define RCD_MODE "mdm-mode" -#define RCD_MODE_OPT_IDLE "idle" -#define RCD_MODE_OPT_LCKOUT "lockout" -#define RCD_MODE_OPT_FDMR "dmr" -#define RCD_MODE_OPT_FP25 "p25" -#define RCD_MODE_OPT_FNXDN "nxdn" - -#define RCD_KILL "mdm-kill" -#define RCD_FORCE_KILL "mdm-force-kill" - -#define RCD_PERMIT_TG "permit-tg" -#define RCD_GRANT_TG "grant-tg" - -#define RCD_RID_WLIST "rid-whitelist" -#define RCD_RID_BLIST "rid-blacklist" - -#define RCD_DMR_BEACON "dmr-beacon" -#define RCD_P25_CC "p25-cc" -#define RCD_P25_CC_FALLBACK "p25-cc-fallback" - -#define RCD_DMR_RID_PAGE "dmr-rid-page" -#define RCD_DMR_RID_CHECK "dmr-rid-check" -#define RCD_DMR_RID_INHIBIT "dmr-rid-inhibit" -#define RCD_DMR_RID_UNINHIBIT "dmr-rid-uninhibit" - -#define RCD_P25_SET_MFID "p25-set-mfid" -#define RCD_P25_RID_PAGE "p25-rid-page" -#define RCD_P25_RID_CHECK "p25-rid-check" -#define RCD_P25_RID_INHIBIT "p25-rid-inhibit" -#define RCD_P25_RID_UNINHIBIT "p25-rid-uninhibit" -#define RCD_P25_RID_GAQ "p25-rid-gaq" -#define RCD_P25_RID_UREG "p25-rid-ureg" - -#define RCD_P25_RELEASE_GRANTS "p25-rel-grnts" -#define RCD_P25_RELEASE_AFFS "p25-rel-affs" - -#define RCD_DMR_CC_DEDICATED "dmr-cc-dedicated" -#define RCD_DMR_CC_BCAST "dmr-cc-bcast" - -#define RCD_P25_CC_DEDICATED "p25-cc-dedicated" -#define RCD_P25_CC_BCAST "p25-cc-bcast" - -#define RCD_DMR_DEBUG "dmr-debug" -#define RCD_DMR_DUMP_CSBK "dmr-dump-csbk" -#define RCD_P25_DEBUG "p25-debug" -#define RCD_P25_DUMP_TSBK "p25-dump-tsbk" -#define RCD_NXDN_DEBUG "nxdn-debug" -#define RCD_NXDN_DUMP_RCCH "nxdn-dump-rcch" - -#define RCD_DMRD_MDM_INJ "debug-dmrd-mdm-inj" -#define RCD_P25D_MDM_INJ "debug-p25d-mdm-inj" -#define RCD_NXDD_MDM_INJ "debug-nxdd-mdm-inj" - -#define RCD_PING "ping" - -// --------------------------------------------------------------------------- -// Class Prototypes -// --------------------------------------------------------------------------- - -class HOST_SW_API Host; -namespace dmr { class HOST_SW_API Control; } -namespace p25 { class HOST_SW_API Control; } -namespace nxdn { class HOST_SW_API Control; } - -// --------------------------------------------------------------------------- -// Class Declaration -// Implements the remote control networking logic. -// --------------------------------------------------------------------------- - -class HOST_SW_API RemoteControl : private Thread { -public: - /// Initializes a new instance of the RemoteControl class. - RemoteControl(const std::string& address, uint16_t port, const std::string& password, Host* host, bool debug); - /// Finalizes a instance of the RemoteControl class. - ~RemoteControl(); - - /// Sets the instances of the Radio ID and Talkgroup ID lookup tables. - void setLookups(::lookups::RadioIdLookup* ridLookup, ::lookups::TalkgroupIdLookup* tidLookup); - /// Sets the instances of the digital radio protocols. - void setProtocols(dmr::Control* dmr, p25::Control* p25, nxdn::Control* nxdn); - - /// Process remote network command data. - void process(Host* host, dmr::Control* dmr, p25::Control* p25, nxdn::Control* nxdn); - - /// Opens connection to the network. - bool open(); - - /// Closes connection to the network. - void close(); - -private: - typedef network::rest::RequestDispatcher RESTDispatcherType; - RESTDispatcherType m_dispatcher; - network::rest::http::HTTPServer m_restServer; - - network::UDPSocket m_socket; - uint8_t m_p25MFId; - - std::string m_password; - uint8_t* m_passwordHash; - bool m_debug; - - Host* m_host; - dmr::Control* m_dmr; - p25::Control* m_p25; - nxdn::Control* m_nxdn; - - ::lookups::RadioIdLookup* m_ridLookup; - ::lookups::TalkgroupIdLookup* m_tidLookup; - - /// Helper to initialize REST API endpoints. - void initializeEndpoints(); - - /// - virtual void entry(); - - /// Helper to send response to client. - void writeResponse(std::string reply, sockaddr_storage address, uint32_t addrLen); - - /// Helper to print the remote control help. - std::string displayHelp(); - - /// - std::string rcdGetStatus(Host* host, dmr::Control* dmr, p25::Control* p25, nxdn::Control* nxdn); - /// - std::string rcdGetVoiceCh(Host* host, dmr::Control* dmr, p25::Control* p25, nxdn::Control* nxdn); - /// - std::string rcdMode(std::vector args, Host* host, dmr::Control* dmr, p25::Control* p25, nxdn::Control* nxdn); - - /// - std::string rcdPermitTG(std::vector args, Host* host, dmr::Control* dmr, p25::Control* p25, nxdn::Control* nxdn); - /// - std::string rcdGrantTG(std::vector args, Host* host, dmr::Control* dmr, p25::Control* p25, nxdn::Control* nxdn); - - /// - std::string rcdDMRModemInj(std::vector args, Host* host, dmr::Control* dmr); - /// - std::string rcdP25ModemInj(std::vector args, Host* host, p25::Control* p25); - /// - std::string rcdNXDNModemInj(std::vector args, Host* host, nxdn::Control* nxdn); - - /// - std::string getArgString(std::vector args, uint32_t n) const; - - /// - __forceinline uint64_t getArgUInt64(std::vector args, uint32_t n) const { return (uint64_t)::atol(getArgString(args, n).c_str()); } - /// - __forceinline uint32_t getArgUInt32(std::vector args, uint32_t n) const { return (uint32_t)::atoi(getArgString(args, n).c_str()); } - /// - __forceinline int32_t getArgInt32(std::vector args, uint32_t n) const { return ::atoi(getArgString(args, n).c_str()); } - /// - __forceinline uint16_t getArgUInt16(std::vector args, uint32_t n) const { return (uint16_t)::atoi(getArgString(args, n).c_str()); } - /// - __forceinline int16_t getArgInt16(std::vector args, uint32_t n) const { return (int16_t)::atoi(getArgString(args, n).c_str()); } - /// - __forceinline uint8_t getArgUInt8(std::vector args, uint32_t n) const { return (uint8_t)::atoi(getArgString(args, n).c_str()); } - /// - __forceinline int8_t getArgInt8(std::vector args, uint32_t n) const { return (int8_t)::atoi(getArgString(args, n).c_str()); } -}; - -#endif // __REMOTE_CONTROL_H__ diff --git a/network/rest/RequestDispatcher.h b/network/rest/RequestDispatcher.h index 26b36835..5d76f20c 100644 --- a/network/rest/RequestDispatcher.h +++ b/network/rest/RequestDispatcher.h @@ -49,9 +49,9 @@ namespace network struct RequestMatch : std::smatch { /// Initializes a new instance of the RequestMatch structure. - RequestMatch(const std::smatch& m, const std::string& d) : std::smatch(m), data(d) { /* stub */ } + RequestMatch(const std::smatch& m, const std::string& c) : std::smatch(m), content(c) { /* stub */ } - std::string data; + std::string content; }; // --------------------------------------------------------------------------- @@ -98,7 +98,7 @@ namespace network /// void handleRequest(const Request& request, Reply& reply, const std::smatch &what) { // dispatching to matching based on handler - RequestMatch match(what, request.data); + RequestMatch match(what, request.content); auto& handler = m_handlers[request.method]; if (handler) { handler(request, reply, match); @@ -128,20 +128,21 @@ namespace network RequestDispatcher(const std::string& basePath, bool debug) : m_basePath(basePath), m_debug(debug) { /* stub */ } /// - MatcherType& match(const std::string& expression) + MatcherType& match(const std::string& expression, bool regex = false) { MatcherTypePtr& p = m_matchers[expression]; if (!p) { if (m_debug) { - ::LogDebug(LOG_RCON, "creating REST RequestDispatcher, expression = %s", expression.c_str()); + ::LogDebug(LOG_REST, "creating RequestDispatcher, expression = %s", expression.c_str()); } p = std::make_shared(expression); } else { if (m_debug) { - ::LogDebug(LOG_RCON, "fetching REST RequestDispatcher, expression = %s", expression.c_str()); + ::LogDebug(LOG_REST, "fetching RequestDispatcher, expression = %s", expression.c_str()); } } + p->setRegEx(regex); return *p; } @@ -153,7 +154,7 @@ namespace network if (!matcher.second->regex()) { if (request.uri.find(matcher.first) != std::string::npos) { if (m_debug) { - ::LogDebug(LOG_RCON, "non-regex endpoint, uri = %s, expression = %s", request.uri.c_str(), matcher.first.c_str()); + ::LogDebug(LOG_REST, "non-regex endpoint, uri = %s, expression = %s", request.uri.c_str(), matcher.first.c_str()); } //what = matcher.first; @@ -163,7 +164,7 @@ namespace network } else { if (std::regex_match(request.uri, what, std::regex(matcher.first))) { if (m_debug) { - ::LogDebug(LOG_RCON, "regex endpoint, uri = %s, expression = %s", request.uri.c_str(), matcher.first.c_str()); + ::LogDebug(LOG_REST, "regex endpoint, uri = %s, expression = %s", request.uri.c_str(), matcher.first.c_str()); } matcher.second->handleRequest(request, reply, what); @@ -172,7 +173,7 @@ namespace network } } - ::LogError(LOG_RCON, "unknown endpoint, uri = %s", request.uri.c_str()); + ::LogError(LOG_REST, "unknown endpoint, uri = %s", request.uri.c_str()); reply = http::HTTPReply::stockReply(http::HTTPReply::BAD_REQUEST, "application/json"); } diff --git a/network/rest/http/Connection.h b/network/rest/http/Connection.h index 035f5093..e0b95af5 100644 --- a/network/rest/http/Connection.h +++ b/network/rest/http/Connection.h @@ -38,9 +38,9 @@ #define __REST_HTTP__CONNECTION_H__ #include "Defines.h" -#include "network/rest/http/HTTPReply.h" #include "network/rest/http/HTTPRequest.h" #include "network/rest/http/HTTPRequestLexer.h" +#include "network/rest/http/HTTPReply.h" #include #include @@ -104,13 +104,14 @@ namespace network m_socket.async_read_some(asio::buffer(m_buffer), [=](asio::error_code ec, std::size_t bytes_transferred) { if (!ec) { HTTPRequestLexer::ResultType result; - char* data; + char* content; + + std::tie(result, content) = m_lexer.parse(m_request, m_buffer.data(), m_buffer.data() + bytes_transferred); - std::tie(result, data) = m_lexer.parse(m_request, m_buffer.data(), m_buffer.data() + bytes_transferred); - auto header = std::find_if(m_request.headers.begin(), m_request.headers.end(), [](const HTTPHeader& h) { return h.name == "content-length"; }); - if (header != m_request.headers.end()) { - size_t length = (size_t)::strtoul(header->value.c_str(), NULL, 10); - m_request.data = std::string(data, length); + 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); } if (result == HTTPRequestLexer::GOOD) { @@ -137,13 +138,13 @@ namespace network if (!m_persistent) { auto self(this->shared_from_this()); } else { - m_reply.headers.emplace_back("Connection:", "keep-alive"); + m_reply.headers.add("Connection", "keep-alive"); } asio::async_write(m_socket, m_reply.toBuffers(), [=](asio::error_code ec, std::size_t) { if (m_persistent) { m_lexer.reset(); - m_reply.headers.resize(0); + m_reply.headers = HTTPHeaders(); m_reply.status = HTTPReply::OK; m_reply.content = ""; m_request = HTTPRequest(); diff --git a/network/rest/http/HTTPHeader.h b/network/rest/http/HTTPHeader.h deleted file mode 100644 index 02997624..00000000 --- a/network/rest/http/HTTPHeader.h +++ /dev/null @@ -1,71 +0,0 @@ -/** -* Digital Voice Modem - Host Software -* GPLv2 Open Source. Use is subject to license terms. -* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. -* -* @package DVM / Host Software -* -*/ -// -// Based on code from the CRUD project. (https://github.com/venediktov/CRUD) -// Licensed under the BPL-1.0 License (https://opensource.org/license/bsl1-0-html) -// -/* -* Copyright (c) 2003-2013 Christopher M. Kohlhoff -* Copyright (C) 2023 by Bryan Biedenkapp N2PLL -* -* Permission is hereby granted, free of charge, to any person or organization -* obtaining a copy of the software and accompanying documentation covered by -* this license (the “Software”) to use, reproduce, display, distribute, execute, -* and transmit the Software, and to prepare derivative works of the Software, and -* to permit third-parties to whom the Software is furnished to do so, all subject -* to the following: -* -* The copyright notices in the Software and this entire statement, including the -* above license grant, this restriction and the following disclaimer, must be included -* in all copies of the Software, in whole or in part, and all derivative works of the -* Software, unless such copies or derivative works are solely in the form of -* machine-executable object code generated by a source language processor. -* -* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR -* PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE -* DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN -* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE -* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ -#if !defined(__REST_HTTP__HTTP_HEADER_H__) -#define __REST_HTTP__HTTP_HEADER_H__ - -#include "Defines.h" - -#include - -namespace network -{ - namespace rest - { - namespace http - { - - // --------------------------------------------------------------------------- - // Structure Declaration - // This class implements a model for an HTTP header. - // --------------------------------------------------------------------------- - - struct HTTPHeader - { - std::string name; - std::string value; - - /// Initializes a new instance of the HTTPHeader struct. - HTTPHeader() { /* stub */ } - /// Initializes a new instance of the HTTPHeader struct. - HTTPHeader(const std::string& name, const std::string& value) : name(name), value(value) { /* stub */ } - }; - - } // namespace http - } // namespace rest -} // namespace network - -#endif // __REST_HTTP__HTTP_HEADER_H__ diff --git a/network/rest/http/HTTPHeaders.h b/network/rest/http/HTTPHeaders.h new file mode 100644 index 00000000..8ca10d46 --- /dev/null +++ b/network/rest/http/HTTPHeaders.h @@ -0,0 +1,132 @@ +/** +* Digital Voice Modem - Host Software +* GPLv2 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Host Software +* +*/ +// +// Based on code from the CRUD project. (https://github.com/venediktov/CRUD) +// Licensed under the BPL-1.0 License (https://opensource.org/license/bsl1-0-html) +// +/* +* Copyright (c) 2003-2013 Christopher M. Kohlhoff +* Copyright (C) 2023 by Bryan Biedenkapp N2PLL +* +* Permission is hereby granted, free of charge, to any person or organization +* obtaining a copy of the software and accompanying documentation covered by +* this license (the “Software”) to use, reproduce, display, distribute, execute, +* and transmit the Software, and to prepare derivative works of the Software, and +* to permit third-parties to whom the Software is furnished to do so, all subject +* to the following: +* +* The copyright notices in the Software and this entire statement, including the +* above license grant, this restriction and the following disclaimer, must be included +* in all copies of the Software, in whole or in part, and all derivative works of the +* Software, unless such copies or derivative works are solely in the form of +* machine-executable object code generated by a source language processor. +* +* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +* PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE +* DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN +* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +#if !defined(__REST_HTTP__HTTP_HEADERS_H__) +#define __REST_HTTP__HTTP_HEADERS_H__ + +#include "Defines.h" +#include "Log.h" + +#include +#include + +namespace network +{ + namespace rest + { + namespace http + { + + // --------------------------------------------------------------------------- + // Class Prototypes + // --------------------------------------------------------------------------- + + struct HTTPReply; + + // --------------------------------------------------------------------------- + // Structure Declaration + // + // --------------------------------------------------------------------------- + + struct HTTPHeaders + { + struct Header + { + std::string name; + std::string value; + + Header() : name{}, value{} { /* stub */} + Header(std::string n, std::string v) : name{n}, value{v} { /* stub */ } + }; + + /// Gets the list of HTTP headers. + std::vector
headers() const { return m_headers; } + /// Returns true if the headers are empty. + bool empty() const { return m_headers.empty(); } + /// Returns the number of headers. + std::size_t size() const { return m_headers.size(); } + /// Clears the list of HTTP headers. + void clearHeaders() { m_headers = std::vector
(); } + /// Helper to add a HTTP header. + void add(const std::string& name, const std::string& value) + { + //::LogDebug(LOG_REST, "HTTPHeaders::add(), header = %s, value = %s", name.c_str(), value.c_str()); + for (auto& header : m_headers) { + if (::strtolower(header.name) == ::strtolower(name)) { + header.value = value; + return; + } + } + + m_headers.push_back(Header(name, value)); + //for (auto header : m_headers) + // ::LogDebug(LOG_REST, "HTTPHeaders::add() m_headers.header = %s, m_headers.value = %s", header.name.c_str(), header.value.c_str()); + } + /// Helper to add a HTTP header. + void remove(const std::string headerName) + { + auto header = std::find_if(m_headers.begin(), m_headers.end(), [&](const Header& h) { + return ::strtolower(h.name) == ::strtolower(headerName); + }); + + if (header != m_headers.end()) { + m_headers.erase(header); + } + } + /// Helper to find the named HTTP header. + std::string find(const std::string headerName) const + { + auto header = std::find_if(m_headers.begin(), m_headers.end(), [&](const Header& h) { + return ::strtolower(h.name) == ::strtolower(headerName); + }); + + if (header != m_headers.end()) { + return header->value; + } + else { + return ""; + } + } + + private: + friend struct HTTPReply; + std::vector
m_headers; + }; + } // namespace http + } // namespace rest +} // namespace network + +#endif // __REST_HTTP__HTTP_HEADERS_H__ diff --git a/network/rest/http/HTTPReply.cpp b/network/rest/http/HTTPReply.cpp index cd1acb5a..3922a97b 100644 --- a/network/rest/http/HTTPReply.cpp +++ b/network/rest/http/HTTPReply.cpp @@ -36,6 +36,7 @@ */ #include "Defines.h" #include "network/rest/http/HTTPReply.h" +#include "Log.h" using namespace network::rest::http; @@ -299,7 +300,9 @@ std::vector HTTPReply::toBuffers() buffers.push_back(status_strings::toBuffer(status)); for (std::size_t i = 0; i < headers.size(); ++i) { - HTTPHeader& h = headers[i]; + HTTPHeaders::Header& h = headers.m_headers[i]; + //::LogDebug(LOG_REST, "HTTPReply::toBuffers() header = %s, value = %s", h.name.c_str(), h.value.c_str()); + buffers.push_back(asio::buffer(h.name)); buffers.push_back(asio::buffer(misc_strings::name_value_separator)); buffers.push_back(asio::buffer(h.value)); @@ -368,7 +371,10 @@ HTTPReply HTTPReply::stockReply(HTTPReply::StatusType status, const std::string /// void HTTPReply::ensureDefaultHeaders(std::string contentType) { - headers.push_back(HTTPHeader("Content-Length", std::to_string(content.size()))); - headers.push_back(HTTPHeader("Content-Type", contentType)); - headers.push_back(HTTPHeader("Server", std::string((__EXE_NAME__ "/" __VER__)))); + headers.add("Content-Type", contentType); + headers.add("Content-Length", std::to_string(content.size())); + headers.add("Server", std::string((__EXE_NAME__ "/" __VER__))); + + //for (auto header : headers.headers()) + // ::LogDebug(LOG_REST, "HTTPReply::ensureDefaultHeaders() header = %s, value = %s", header.name.c_str(), header.value.c_str()); } diff --git a/network/rest/http/HTTPReply.h b/network/rest/http/HTTPReply.h index 66a53420..889f7ae0 100644 --- a/network/rest/http/HTTPReply.h +++ b/network/rest/http/HTTPReply.h @@ -39,7 +39,7 @@ #include "Defines.h" #include "network/json/json.h" -#include "network/rest/http/HTTPHeader.h" +#include "network/rest/http/HTTPHeaders.h" #include #include @@ -82,7 +82,7 @@ namespace network SERVICE_UNAVAILABLE = 503 } status; - std::vector headers; + HTTPHeaders headers; std::string content; /// Convert the reply into a vector of buffers. The buffers do not own the diff --git a/network/rest/http/HTTPRequest.h b/network/rest/http/HTTPRequest.h index 3b9c10c4..8bf8284a 100644 --- a/network/rest/http/HTTPRequest.h +++ b/network/rest/http/HTTPRequest.h @@ -38,7 +38,8 @@ #define __REST_HTTP__HTTP_REQUEST_H__ #include "Defines.h" -#include "network/rest/http/HTTPHeader.h" +#include "network/rest/http/HTTPHeaders.h" +#include "Log.h" #include #include @@ -49,7 +50,6 @@ namespace network { namespace http { - // --------------------------------------------------------------------------- // Structure Declaration // This struct implements a model of a request received from a HTTP client. @@ -60,11 +60,12 @@ namespace network std::string method; std::string uri; + HTTPHeaders headers; + std::string content; + int httpVersionMajor; int httpVersionMinor; - - std::vector headers; - std::string data; + }; } // namespace http } // namespace rest diff --git a/network/rest/http/HTTPRequestHandler.cpp b/network/rest/http/HTTPRequestHandler.cpp index 17019680..6cffc390 100644 --- a/network/rest/http/HTTPRequestHandler.cpp +++ b/network/rest/http/HTTPRequestHandler.cpp @@ -106,11 +106,9 @@ void HTTPRequestHandler::handleRequest(const HTTPRequest& request, HTTPReply& re while (is.read(buf, sizeof(buf)).gcount() > 0) reply.content.append(buf, is.gcount()); - reply.headers.resize(2); - reply.headers[0].name = "Content-Length"; - reply.headers[0].value = std::to_string(reply.content.size()); - reply.headers[1].name = "Content-Type"; - reply.headers[1].value = "application/octet-stream"; + reply.headers.clearHeaders(); + reply.headers.add("Content-Length", std::to_string(reply.content.size())); + reply.headers.add("Content-Type", "application/octet-stream"); } /// diff --git a/network/rest/http/HTTPRequestLexer.cpp b/network/rest/http/HTTPRequestLexer.cpp index 3aeb6280..70cd5adb 100644 --- a/network/rest/http/HTTPRequestLexer.cpp +++ b/network/rest/http/HTTPRequestLexer.cpp @@ -37,6 +37,7 @@ #include "Defines.h" #include "network/rest/http/HTTPRequestLexer.h" #include "network/rest/http/HTTPRequest.h" +#include "Log.h" using namespace network::rest::http; @@ -51,6 +52,7 @@ using namespace network::rest::http; /// HTTPRequestLexer::HTTPRequestLexer() : + m_headers(), m_state(METHOD_START) { /* stub */ @@ -60,6 +62,7 @@ HTTPRequestLexer::HTTPRequestLexer() : void HTTPRequestLexer::reset() { m_state = METHOD_START; + m_headers = std::vector(); } // --------------------------------------------------------------------------- @@ -234,8 +237,8 @@ HTTPRequestLexer::ResultType HTTPRequestLexer::consume(HTTPRequest& req, char in return BAD; } else { - req.headers.push_back(HTTPHeader()); - req.headers.back().name.push_back(std::tolower(input)); + m_headers.push_back(LexedHeader()); + m_headers.back().name.push_back(std::tolower(input)); m_state = HEADER_NAME; return INDETERMINATE; } @@ -253,7 +256,7 @@ HTTPRequestLexer::ResultType HTTPRequestLexer::consume(HTTPRequest& req, char in } else { m_state = HEADER_VALUE; - req.headers.back().value.push_back(input); + m_headers.back().value.push_back(input); return INDETERMINATE; } @@ -267,7 +270,7 @@ HTTPRequestLexer::ResultType HTTPRequestLexer::consume(HTTPRequest& req, char in } else { - req.headers.back().name.push_back(std::tolower(input)); + m_headers.back().name.push_back(std::tolower(input)); return INDETERMINATE; } @@ -290,7 +293,7 @@ HTTPRequestLexer::ResultType HTTPRequestLexer::consume(HTTPRequest& req, char in return BAD; } else { - req.headers.back().value.push_back(input); + m_headers.back().value.push_back(input); return INDETERMINATE; } @@ -304,7 +307,16 @@ HTTPRequestLexer::ResultType HTTPRequestLexer::consume(HTTPRequest& req, char in } case EXPECTING_NEWLINE_3: - return (input == '\n') ? GOOD : BAD; + if (input == '\n') { + for (auto header : m_headers) { + //::LogDebug(LOG_REST, "HTTPRequestLexer::consume(), header = %s, value = %s", header.name.c_str(), header.value.c_str()); + req.headers.add(header.name, header.value); + } + + return GOOD; + } else { + return BAD; + } default: return BAD; diff --git a/network/rest/http/HTTPRequestLexer.h b/network/rest/http/HTTPRequestLexer.h index 105165bc..769b607b 100644 --- a/network/rest/http/HTTPRequestLexer.h +++ b/network/rest/http/HTTPRequestLexer.h @@ -38,6 +38,7 @@ #define __REST_HTTP__HTTP_REQUEST_PARSER_H__ #include +#include namespace network { @@ -96,6 +97,17 @@ namespace network /// Check if a byte is an digit. static bool isDigit(int c); + struct LexedHeader + { + std::string name; + std::string value; + + LexedHeader() { /* stub */ } + LexedHeader(const std::string& name, const std::string& value) : name(name), value(value) {} + }; + + std::vector m_headers; + enum state { METHOD_START, diff --git a/nxdn/Control.h b/nxdn/Control.h index c98fbf93..53bf62a8 100644 --- a/nxdn/Control.h +++ b/nxdn/Control.h @@ -40,7 +40,6 @@ #include "nxdn/packet/Data.h" #include "nxdn/SiteData.h" #include "network/BaseNetwork.h" -#include "network/RemoteControl.h" #include "lookups/RSSIInterpolator.h" #include "lookups/IdenTableLookup.h" #include "lookups/RadioIdLookup.h" @@ -107,6 +106,9 @@ namespace nxdn /// Permits a TGID on a non-authoritative host. void permittedTG(uint32_t dstId); + /// Gets instance of the AffiliationLookup class. + lookups::AffiliationLookup affiliations() { return m_affiliations; } + /// Flag indicating whether the processor or is busy or not. bool isBusy() const; diff --git a/p25/Control.h b/p25/Control.h index 466e980d..ca1b0851 100644 --- a/p25/Control.h +++ b/p25/Control.h @@ -38,7 +38,6 @@ #include "p25/packet/Voice.h" #include "p25/packet/Trunk.h" #include "network/BaseNetwork.h" -#include "network/RemoteControl.h" #include "lookups/RSSIInterpolator.h" #include "lookups/IdenTableLookup.h" #include "lookups/RadioIdLookup.h" diff --git a/p25/packet/Trunk.h b/p25/packet/Trunk.h index 08a7b882..cefb0eec 100644 --- a/p25/packet/Trunk.h +++ b/p25/packet/Trunk.h @@ -34,7 +34,6 @@ #include "p25/lc/TDULC.h" #include "p25/Control.h" #include "network/BaseNetwork.h" -#include "network/RemoteControl.h" #include "Timer.h" #include diff --git a/remote/RemoteCommand.cpp b/remote/RemoteCommand.cpp index 5f1c5ea0..f23dc8ac 100644 --- a/remote/RemoteCommand.cpp +++ b/remote/RemoteCommand.cpp @@ -186,7 +186,7 @@ int RemoteCommand::send(const std::string& address, uint32_t port, const std::st break; if (debug) - ::LogDebug(LOG_RCON, "RemoteCommand::send() block len = %u, offs = %u", len - 3, offs); + ::LogDebug(LOG_HOST, "RemoteCommand::send() block len = %u, offs = %u", len - 3, offs); buffer[len] = '\0'; diff --git a/remote/RemoteCommandMain.cpp b/remote/RemoteCommandMain.cpp index a213c109..1215ab9e 100644 --- a/remote/RemoteCommandMain.cpp +++ b/remote/RemoteCommandMain.cpp @@ -65,7 +65,7 @@ using namespace network; 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 = RCON_DEFAULT_PORT; +static uint32_t g_remotePort = REST_API_DEFAULT_PORT; static std::string g_remotePassword = std::string(); static bool g_debug = false;