From 35298fe94b2a7752df295a4b57c2da04fb441454 Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Mon, 20 Mar 2023 23:55:24 -0400 Subject: [PATCH] implement the majority of working REST API support (this commit deprecates RCON completely, RCON will no longer function with any build beyond this); --- CMakeLists.txt | 5 +- dmr/packet/ControlSignaling.cpp | 28 +- modem/Modem.h | 8 + network/RESTAPI.cpp | 247 ++++++-- network/RESTAPI.h | 54 +- network/RESTDefines.h | 96 ++++ network/json/json.h | 81 ++- network/rest/RequestDispatcher.h | 27 + network/rest/http/Connection.h | 47 +- network/rest/http/HTTPClient.h | 21 +- network/rest/http/HTTPPayload.cpp | 26 +- network/rest/http/HTTPPayload.h | 10 +- nxdn/packet/Trunk.cpp | 12 +- p25/packet/Trunk.cpp | 22 +- remote/RESTClient.cpp | 275 +++++++++ remote/{RemoteCommand.h => RESTClient.h} | 42 +- remote/RESTClientMain.cpp | 684 +++++++++++++++++++++++ remote/RemoteCommand.cpp | 219 -------- remote/RemoteCommandMain.cpp | 228 -------- 19 files changed, 1496 insertions(+), 636 deletions(-) create mode 100644 network/RESTDefines.h create mode 100644 remote/RESTClient.cpp rename remote/{RemoteCommand.h => RESTClient.h} (57%) create mode 100644 remote/RESTClientMain.cpp delete mode 100644 remote/RemoteCommand.cpp delete mode 100644 remote/RemoteCommandMain.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d4abf8d7..32af06e5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -106,8 +106,8 @@ file(GLOB dvmhost_SRC "network/rest/*.cpp" "network/rest/http/*.h" "network/rest/http/*.cpp" - "remote/RemoteCommand.cpp" - "remote/RemoteCommand.h" + "remote/RESTClient.cpp" + "remote/RESTClient.h" "yaml/*.h" "yaml/*.cpp" "*.h" @@ -118,6 +118,7 @@ file(GLOB dvmhost_SRC file(GLOB dvmcmd_SRC "network/UDPSocket.h" "network/UDPSocket.cpp" + "network/RESTDefines.h" "network/json/*.h" "network/rest/*.h" "network/rest/*.cpp" diff --git a/dmr/packet/ControlSignaling.cpp b/dmr/packet/ControlSignaling.cpp index c17ae6dd..03d4dacc 100644 --- a/dmr/packet/ControlSignaling.cpp +++ b/dmr/packet/ControlSignaling.cpp @@ -38,7 +38,7 @@ #include "dmr/Sync.h" #include "edac/BPTC19696.h" #include "edac/CRC.h" -#include "remote/RemoteCommand.h" +#include "remote/RESTClient.h" #include "Log.h" #include "Utils.h" @@ -837,11 +837,14 @@ bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_ if (m_tscc->m_authoritative && m_tscc->m_controlPermitTG) { ::lookups::VoiceChData voiceChData = m_tscc->m_affiliations->getRFChData(chNo); if (voiceChData.isValidCh() && !voiceChData.address().empty() && voiceChData.port() > 0) { - std::stringstream ss; - ss << "permit-tg " << modem::DVM_STATE::STATE_DMR << " " << dstId << " " << slot; - - RemoteCommand::send(voiceChData.address(), voiceChData.port(), voiceChData.password(), - ss.str(), m_tscc->m_debug); + json::object req = json::object(); + int state = modem::DVM_STATE::STATE_DMR; + req["state"].set(state); + req["dstId"].set(dstId); + req["slot"].set(slot); + + RESTClient::send(voiceChData.address(), voiceChData.port(), voiceChData.password(), + HTTP_PUT, PUT_PERMIT_TG, req, m_tscc->m_debug); } } @@ -872,11 +875,14 @@ bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_ if (m_tscc->m_authoritative && m_tscc->m_controlPermitTG) { ::lookups::VoiceChData voiceChData = m_tscc->m_affiliations->getRFChData(chNo); if (voiceChData.isValidCh() && !voiceChData.address().empty() && voiceChData.port() > 0) { - std::stringstream ss; - ss << "permit-tg " << modem::DVM_STATE::STATE_DMR << " " << dstId << " " << slot; - - RemoteCommand::send(voiceChData.address(), voiceChData.port(), voiceChData.password(), - ss.str(), m_tscc->m_debug); + json::object req = json::object(); + int state = modem::DVM_STATE::STATE_DMR; + req["state"].set(state); + req["dstId"].set(dstId); + req["slot"].set(slot); + + RESTClient::send(voiceChData.address(), voiceChData.port(), voiceChData.password(), + HTTP_PUT, PUT_PERMIT_TG, req, m_tscc->m_debug); } } diff --git a/modem/Modem.h b/modem/Modem.h index 1b774764..5c7de30b 100644 --- a/modem/Modem.h +++ b/modem/Modem.h @@ -41,10 +41,18 @@ #include #include +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + #define MODEM_VERSION_STR "%.*s, Modem protocol: %u" #define MODEM_UNSUPPORTED_STR "Modem protocol: %u, unsupported! Stopping." #define NULL_MODEM "null" +// --------------------------------------------------------------------------- +// Macros +// --------------------------------------------------------------------------- + #define MODEM_OC_PORT_HANDLER bool(modem::Modem* modem) #define MODEM_OC_PORT_HANDLER_BIND(funcAddr, classInstance) std::bind(&funcAddr, classInstance, std::placeholders::_1) #define MODEM_RESP_HANDLER bool(modem::Modem* modem, uint32_t ms, modem::RESP_TYPE_DVM rspType, bool rspDblLen, const uint8_t* buffer, uint16_t len) diff --git a/network/RESTAPI.cpp b/network/RESTAPI.cpp index fdad64fc..9158e107 100644 --- a/network/RESTAPI.cpp +++ b/network/RESTAPI.cpp @@ -55,7 +55,7 @@ using namespace modem; #include // --------------------------------------------------------------------------- -// Constants +// Macros // --------------------------------------------------------------------------- #define REST_API_BIND(funcAddr, classInstance) std::bind(&funcAddr, classInstance, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3) @@ -64,6 +64,20 @@ using namespace modem; // 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); +} + /// /// /// @@ -275,8 +289,8 @@ void RESTAPI::initializeEndpoints() 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)); + m_dispatcher.match(GET_DMR_CC_DEDICATED).get(REST_API_BIND(RESTAPI::restAPI_GetDMRCCEnable, this)); + m_dispatcher.match(GET_DMR_CC_BCAST).get(REST_API_BIND(RESTAPI::restAPI_GetDMRCCBroadcast, this)); /* ** Project 25 @@ -286,15 +300,17 @@ void RESTAPI::initializeEndpoints() 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)); + m_dispatcher.match(GET_P25_CC_DEDICATED).get(REST_API_BIND(RESTAPI::restAPI_GetP25CCEnable, this)); + m_dispatcher.match(GET_P25_CC_BCAST).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)); + m_dispatcher.match(GET_NXDN_CC).get(REST_API_BIND(RESTAPI::restAPI_GetNXDNCC, this)); + m_dispatcher.match(GET_NXDN_DEBUG).get(REST_API_BIND(RESTAPI::restAPI_GetNXDNDebug, this)); + m_dispatcher.match(GET_NXDN_DUMP_RCCH).get(REST_API_BIND(RESTAPI::restAPI_GetNXDNDumpRCCH, this)); + m_dispatcher.match(GET_NXDN_CC_DEDICATED).get(REST_API_BIND(RESTAPI::restAPI_GetNXDNCCEnable, this)); } /// @@ -725,16 +741,19 @@ void RESTAPI::restAPI_PutModemKill(const HTTPPayload& request, HTTPPayload& repl errorPayload(reply, "OK", HTTPPayload::OK); + // validate mode is a string within the JSON blob if (!req["force"].is()) { + errorPayload(reply, "force was not a valid value"); + return; + } + + bool force = req["force"].get(); + + if (!force) { g_killed = true; } else { - bool force = req["force"].get(); - if (force) { - g_killed = true; - m_host->setState(HOST_STATE_QUIT); - } else { - g_killed = true; - } + g_killed = true; + m_host->setState(HOST_STATE_QUIT); } } @@ -897,17 +916,12 @@ void RESTAPI::restAPI_PutGrantTG(const HTTPPayload& request, HTTPPayload& reply, } // validate unit-to-unit is a integer within the JSON blob - if (!req["unitToUnit"].is()) { - errorPayload(reply, "unit-to-unit was not a valid integer"); + if (!req["unitToUnit"].is()) { + errorPayload(reply, "unit-to-unit was not a valid boolean"); return; } - uint8_t unitToUnit = (uint8_t)req["unitToUnit"].get(); - - if (unitToUnit > 1U) { - errorPayload(reply, "unit-to-unit must be a 0 or 1"); - return; - } + //bool unitToUnit = req["unitToUnit"].get(); switch (state) { case STATE_DMR: @@ -928,7 +942,7 @@ void RESTAPI::restAPI_PutGrantTG(const HTTPPayload& request, HTTPPayload& reply, if (m_dmr != nullptr) { // TODO TODO - //m_dmr->grantTG(dstId, slot, unitToUnit == 1U); + //m_dmr->grantTG(dstId, slot, unitToUnit); } else { errorPayload(reply, "DMR mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); @@ -945,7 +959,7 @@ void RESTAPI::restAPI_PutGrantTG(const HTTPPayload& request, HTTPPayload& reply, { if (m_p25 != nullptr) { // TODO TODO - //m_p25->grantTG(dstId, unitToUnit == 1U); + //m_p25->grantTG(dstId, unitToUnit); } else { errorPayload(reply, "P25 mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); @@ -962,7 +976,7 @@ void RESTAPI::restAPI_PutGrantTG(const HTTPPayload& request, HTTPPayload& reply, { if (m_nxdn != nullptr) { // TODO TODO - //nxdn->grantTG(dstId, unitToUnit == 1U); + //nxdn->grantTG(dstId, unitToUnit); } else { errorPayload(reply, "NXDN mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); @@ -1247,13 +1261,33 @@ void RESTAPI::restAPI_GetDMRCCEnable(const HTTPPayload& request, HTTPPayload& re if (!validateAuth(request, reply)) { return; } +#if defined(ENABLE_DMR) + errorPayload(reply, "OK", HTTPPayload::OK); + if (m_dmr != nullptr) { + if (m_host->m_dmrTSCCData) { + if (m_p25 != nullptr) { + errorPayload(reply, "Can't enable DMR control channel while P25 is enabled!"); + } - if (match.size() < 2) { - errorPayload(reply, "invalid API call arguments"); + if (m_nxdn != nullptr) { + errorPayload(reply, "Can't enable DMR control channel while NXDN is enabled!"); + return; + } + + m_host->m_dmrCtrlChannel = !m_host->m_dmrCtrlChannel; + errorPayload(reply, string_format("DMR CC is %s", m_host->m_p25CtrlChannel ? "enabled" : "disabled"), HTTPPayload::OK); + } + else { + errorPayload(reply, "DMR control data is not enabled!"); + } + } + else { + errorPayload(reply, "DMR mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); return; } - - errorPayload(reply, "OK", HTTPPayload::OK); +#else + errorPayload(reply, "DMR operations are unavailable", HTTPPayload::SERVICE_UNAVAILABLE); +#endif // defined(ENABLE_DMR) } /// @@ -1267,13 +1301,19 @@ void RESTAPI::restAPI_GetDMRCCBroadcast(const HTTPPayload& request, HTTPPayload& if (!validateAuth(request, reply)) { return; } - - if (match.size() < 2) { - errorPayload(reply, "invalid API call arguments"); +#if defined(ENABLE_DMR) + errorPayload(reply, "OK", HTTPPayload::OK); + if (m_dmr != nullptr) { + m_host->m_dmrTSCCData = !m_host->m_dmrTSCCData; + errorPayload(reply, string_format("DMR CC broadcast is %s", m_host->m_dmrTSCCData ? "enabled" : "disabled"), HTTPPayload::OK); + } + else { + errorPayload(reply, "DMR mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); return; } - - errorPayload(reply, "OK", HTTPPayload::OK); +#else + errorPayload(reply, "DMR operations are unavailable", HTTPPayload::SERVICE_UNAVAILABLE); +#endif // defined(ENABLE_DMR) } /* @@ -1428,13 +1468,38 @@ void RESTAPI::restAPI_GetP25CCEnable(const HTTPPayload& request, HTTPPayload& re if (!validateAuth(request, reply)) { return; } +#if defined(ENABLE_P25) + errorPayload(reply, "OK", HTTPPayload::OK); + if (m_p25 != nullptr) { + if (m_host->m_p25CCData) { + if (m_dmr != nullptr) { + errorPayload(reply, "Can't enable P25 control channel while DMR is enabled!"); + return; + } - if (match.size() < 2) { - errorPayload(reply, "invalid API call arguments"); + if (m_nxdn != nullptr) { + errorPayload(reply, "Can't enable P25 control channel while NXDN is enabled!"); + return; + } + + m_host->m_p25CtrlChannel = !m_host->m_p25CtrlChannel; + m_host->m_p25CtrlBroadcast = true; + g_fireP25Control = true; + m_p25->setCCHalted(false); + + errorPayload(reply, string_format("P25 CC is %s", m_host->m_p25CtrlChannel ? "enabled" : "disabled"), HTTPPayload::OK); + } + else { + errorPayload(reply, "P25 control data is not enabled!"); + } + } + else { + errorPayload(reply, "P25 mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); return; } - - errorPayload(reply, "OK", HTTPPayload::OK); +#else + errorPayload(reply, "P25 operations are unavailable", HTTPPayload::SERVICE_UNAVAILABLE); +#endif // defined(ENABLE_P25) } /// @@ -1448,19 +1513,72 @@ void RESTAPI::restAPI_GetP25CCBroadcast(const HTTPPayload& request, HTTPPayload& if (!validateAuth(request, reply)) { return; } +#if defined(ENABLE_P25) + errorPayload(reply, "OK", HTTPPayload::OK); + if (m_p25 != nullptr) { + if (m_host->m_p25CCData) { + m_host->m_p25CtrlBroadcast = !m_host->m_p25CtrlBroadcast; - if (match.size() < 2) { - errorPayload(reply, "invalid API call arguments"); + if (!m_host->m_p25CtrlBroadcast) { + g_fireP25Control = false; + m_p25->setCCHalted(true); + } + else { + g_fireP25Control = true; + m_p25->setCCHalted(false); + } + + errorPayload(reply, string_format("P25 CC broadcast is %s", m_host->m_p25CtrlBroadcast ? "enabled" : "disabled"), HTTPPayload::OK); + } + else { + errorPayload(reply, "P25 control data is not enabled!"); + } + } + else { + errorPayload(reply, "P25 mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); return; } - - errorPayload(reply, "OK", HTTPPayload::OK); +#else + errorPayload(reply, "P25 operations are unavailable", HTTPPayload::SERVICE_UNAVAILABLE); +#endif // defined(ENABLE_P25) } /* ** Next Generation Digital Narrowband */ +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_GetNXDNCC(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + +#if defined(ENABLE_NXDN) + errorPayload(reply, "OK", HTTPPayload::OK); + if (m_nxdn != nullptr) { + if (m_host->m_nxdnCCData) { + g_fireNXDNControl = true; + } + else { + errorPayload(reply, "NXDN control data is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); + return; + } + } + else { + errorPayload(reply, "NXDN mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); + return; + } +#else + errorPayload(reply, "NXDN operations are unavailable", HTTPPayload::SERVICE_UNAVAILABLE); +#endif // defined(ENABLE_NXDN) +} + /// /// /// @@ -1545,3 +1663,48 @@ void RESTAPI::restAPI_GetNXDNDumpRCCH(const HTTPPayload& request, HTTPPayload& r errorPayload(reply, "NXDN operations are unavailable", HTTPPayload::SERVICE_UNAVAILABLE); #endif // defined(ENABLE_NXDN) } + +/// +/// +/// +/// +/// +/// +void RESTAPI::restAPI_GetNXDNCCEnable(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } +#if defined(ENABLE_NXDN) + errorPayload(reply, "OK", HTTPPayload::OK); + if (m_nxdn != nullptr) { + if (m_host->m_nxdnCCData) { + if (m_dmr != nullptr) { + errorPayload(reply, "Can't enable NXDN control channel while DMR is enabled!"); + return; + } + + if (m_p25 != nullptr) { + errorPayload(reply, "Can't enable NXDN control channel while P25 is enabled!"); + return; + } + + m_host->m_nxdnCtrlChannel = !m_host->m_nxdnCtrlChannel; + m_host->m_nxdnCtrlBroadcast = true; + g_fireNXDNControl = true; + m_nxdn->setCCHalted(false); + + errorPayload(reply, string_format("NXDN CC is %s", m_host->m_nxdnCtrlChannel ? "enabled" : "disabled"), HTTPPayload::OK); + } + else { + errorPayload(reply, "NXDN control data is not enabled!"); + } + } + else { + errorPayload(reply, "NXDN mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); + return; + } +#else + errorPayload(reply, "NXDN operations are unavailable", HTTPPayload::SERVICE_UNAVAILABLE); +#endif // defined(ENABLE_NXDN) +} diff --git a/network/RESTAPI.h b/network/RESTAPI.h index cc39f903..62c64d44 100644 --- a/network/RESTAPI.h +++ b/network/RESTAPI.h @@ -28,6 +28,7 @@ #include "Defines.h" #include "network/UDPSocket.h" +#include "network/RESTDefines.h" #include "network/rest/RequestDispatcher.h" #include "network/rest/http/HTTPServer.h" #include "lookups/RadioIdLookup.h" @@ -38,53 +39,6 @@ #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 // --------------------------------------------------------------------------- @@ -220,10 +174,14 @@ private: ** Next Generation Digital Narrowband */ - /// + /// + void restAPI_GetNXDNCC(const HTTPPayload& request, HTTPPayload& reply, const network::rest::RequestMatch& match); + /// void restAPI_GetNXDNDebug(const HTTPPayload& request, HTTPPayload& reply, const network::rest::RequestMatch& match); /// void restAPI_GetNXDNDumpRCCH(const HTTPPayload& request, HTTPPayload& reply, const network::rest::RequestMatch& match); + /// + void restAPI_GetNXDNCCEnable(const HTTPPayload& request, HTTPPayload& reply, const network::rest::RequestMatch& match); }; #endif // __REST_API_H__ diff --git a/network/RESTDefines.h b/network/RESTDefines.h new file mode 100644 index 00000000..a0266d62 --- /dev/null +++ b/network/RESTDefines.h @@ -0,0 +1,96 @@ +/** +* 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_DEFINES_H__) +#define __REST_DEFINES_H__ + +#include "Defines.h" + +// --------------------------------------------------------------------------- +// 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 RID_CMD_PAGE "page" +#define RID_CMD_CHECK "check" +#define RID_CMD_INHIBIT "inhibit" +#define RID_CMD_UNINHIBIT "uninhibit" +#define RID_CMD_GAQ "group-aff-req" +#define RID_CMD_UREG "unit-reg" + +#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_BASE "/rid-whitelist/" +#define GET_RID_WHITELIST GET_RID_WHITELIST_BASE"(\\d+)" +#define GET_RID_BLACKLIST_BASE "/rid-blacklist/" +#define GET_RID_BLACKLIST GET_RID_BLACKLIST_BASE"(\\d+)" + +#define GET_DMR_BEACON "/dmr/beacon" +#define GET_DMR_DEBUG_BASE "/dmr/debug/" +#define GET_DMR_DEBUG GET_DMR_DEBUG_BASE"(\\d+)/(\\d+)" +#define GET_DMR_DUMP_CSBK_BASE "/dmr/dump-csbk/" +#define GET_DMR_DUMP_CSBK GET_DMR_DUMP_CSBK_BASE"(\\d+)" +#define PUT_DMR_RID "/dmr/rid" +#define GET_DMR_CC_DEDICATED "/dmr/cc-enable" +#define GET_DMR_CC_BCAST "/dmr/cc-broadcast" + +#define GET_P25_CC "/p25/cc" +//#define GET_P25_CC_FALLBACK_BASE "/p25/cc-fallback/" +//#define GET_P25_CC_FALLBACK GET_P25_CC_FALLBACK_BASE"(\\d+)" +#define GET_P25_DEBUG_BASE "/p25/debug/(\\d+)/(\\d+)" +#define GET_P25_DEBUG GET_P25_DEBUG_BASE"(\\d+)/(\\d+)" +#define GET_P25_DUMP_TSBK_BASE "/p25/dump-tsbk/(\\d+)" +#define GET_P25_DUMP_TSBK GET_P25_DUMP_TSBK_BASE"(\\d+)" +#define PUT_P25_RID "/p25/rid" +#define GET_P25_CC_DEDICATED "/p25/cc-enable" +#define GET_P25_CC_BCAST "/p25/cc-broadcast" + +#define GET_NXDN_CC "/nxdn/cc" +#define GET_NXDN_DEBUG_BASE "/nxdn/debug/(\\d+)/(\\d+)" +#define GET_NXDN_DEBUG GET_NXDN_DEBUG_BASE"(\\d+)/(\\d+)" +#define GET_NXDN_DUMP_RCCH_BASE "/nxdn/dump-rcch/(\\d+)" +#define GET_NXDN_DUMP_RCCH GET_NXDN_DUMP_RCCH_BASE"(\\d+)" +#define GET_NXDN_CC_DEDICATED "/nxdn/cc-enable" + +#endif // __REST_API_H__ diff --git a/network/json/json.h b/network/json/json.h index 080a7911..eea726dc 100644 --- a/network/json/json.h +++ b/network/json/json.h @@ -11,6 +11,7 @@ // Licensed under the BSD-2-Clause License (https://opensource.org/licenses/BSD-2-Clause) // /* + * Copyright (C) 2023 by Bryan Biedenkapp N2PLL * Copyright 2009-2010 Cybozu Labs, Inc. * Copyright 2011-2014 Kazuho Oku * All rights reserved. @@ -135,6 +136,9 @@ namespace json null_type, boolean_type, number_type, +#ifdef PICOJSON_USE_INT64 + int64_type, +#endif int32_type, uint32_type, uint16_type, @@ -143,9 +147,6 @@ namespace json string_type, array_type, object_type -#ifdef PICOJSON_USE_INT64 - ,int64_type -#endif }; enum { INDENT_WIDTH = 2, DEFAULT_MAX_DEPTHS = 100 }; @@ -230,6 +231,7 @@ namespace json bool contains(const std::string &key) const; std::string to_str() const; + std::string to_type() const; template void serialize(Iter os, bool prettify = false) const; std::string serialize(bool prettify = false) const; @@ -400,19 +402,26 @@ namespace json return type_ == jtype##_type; \ } + #define IS_NUMBER(ctype, jtype) \ + template <> inline bool value::is() const { \ + return type_ == jtype##_type || type_ == number_type; \ + } + IS(null, null) IS(bool, boolean) #ifdef PICOJSON_USE_INT64 IS(int64_t, int64) #endif - IS(int, int32) - IS(uint32_t, uint32) - IS(uint16_t, uint16) - IS(uint8_t, uint8) + IS_NUMBER(int, int32) + IS_NUMBER(uint32_t, uint32) + IS_NUMBER(uint16_t, uint16) + IS_NUMBER(uint8_t, uint8) + IS_NUMBER(float, float) IS(std::string, string) IS(array, array) IS(object, object) #undef IS + #undef IS_NUMBER template <> inline bool value::is() const { return type_ == number_type @@ -444,11 +453,21 @@ namespace json #else GET(double, u_.number_) #endif - GET(int, u_.int32_) - GET(uint32_t, u_.uint32_) - GET(uint16_t, u_.uint16_) - GET(uint8_t, u_.uint8_) - GET(float, u_.float_) + GET(int, + (type_ == number_type && (const_cast(this)->type_ = int32_type, (const_cast(this)->u_.int32_ = u_.number_)), + u_.int32_)) + GET(uint32_t, + (type_ == number_type && (const_cast(this)->type_ = uint32_type, (const_cast(this)->u_.uint32_ = u_.number_)), + u_.uint32_)) + GET(uint16_t, + (type_ == number_type && (const_cast(this)->type_ = uint16_type, (const_cast(this)->u_.uint16_ = u_.number_)), + u_.uint16_)) + GET(uint8_t, + (type_ == number_type && (const_cast(this)->type_ = uint8_type, (const_cast(this)->u_.uint8_ = u_.number_)), + u_.uint8_)) + GET(float, + (type_ == number_type && (const_cast(this)->type_ = float_type, (const_cast(this)->u_.float_ = u_.number_)), + u_.float_)) #undef GET #define SET(ctype, jtype, setter) \ @@ -625,6 +644,44 @@ namespace json return std::string(); } + inline std::string value::to_type() const { + switch (type_) { + case null_type: + return "null"; + case boolean_type: + return "boolean"; +#ifdef PICOJSON_USE_INT64 + case int64_type: + return "int64"; +#endif + case number_type: + return "number"; + case int32_type: + return "int32"; + case uint32_type: + return "uint32"; + case uint16_type: + return "uint16"; + case uint8_type: + return "uint8"; + case float_type: + return "float"; + case string_type: + return "string"; + case array_type: + return "array"; + case object_type: + return "object"; + default: + PICOJSON_ASSERT(0); +#ifdef _MSC_VER + __assume(0); +#endif + } + + return std::string(); + } + template void copy(const std::string &s, Iter oi) { std::copy(s.begin(), s.end(), oi); } diff --git a/network/rest/RequestDispatcher.h b/network/rest/RequestDispatcher.h index c5578e5d..493aad0a 100644 --- a/network/rest/RequestDispatcher.h +++ b/network/rest/RequestDispatcher.h @@ -190,6 +190,33 @@ namespace network // This class implements a generic debug request dispatcher. // --------------------------------------------------------------------------- + template + class BasicRequestDispatcher { + public: + typedef std::function RequestHandlerType; + + /// Initializes a new instance of the DebugRequestDispatcher class. + BasicRequestDispatcher() { /* stub */ } + /// Initializes a new instance of the BasicRequestDispatcher class. + BasicRequestDispatcher(RequestHandlerType handler) : m_handler(handler) { /* stub */ } + + /// + void handleRequest(const Request& request, Reply& reply) + { + if (m_handler) { + m_handler(request, reply); + } + } + + private: + RequestHandlerType m_handler; + }; + + // --------------------------------------------------------------------------- + // Class Declaration + // This class implements a generic debug request dispatcher. + // --------------------------------------------------------------------------- + template class DebugRequestDispatcher { public: diff --git a/network/rest/http/Connection.h b/network/rest/http/Connection.h index d376192b..2df630f3 100644 --- a/network/rest/http/Connection.h +++ b/network/rest/http/Connection.h @@ -40,6 +40,7 @@ #include "Defines.h" #include "network/rest/http/HTTPLexer.h" #include "network/rest/http/HTTPPayload.h" +#include "Utils.h" #include #include @@ -93,12 +94,21 @@ namespace network /// Start the first asynchronous operation for the connection. void start() { read(); } /// Stop all asynchronous operations associated with the connection. - void stop() { m_socket.close(); } + void stop() + { + try + { + if (m_socket.is_open()) { + m_socket.close(); + } + } + catch(const std::exception&) { /* ignore */ } + } - /// Perform an asynchronous write operation. + /// Perform an synchronous write operation. void send(HTTPPayload request) { - request.attachHostHeader(m_socket.local_endpoint()); + request.attachHostHeader(m_socket.remote_endpoint()); write(request); } private: @@ -149,7 +159,9 @@ namespace network } else if (ec != asio::error::operation_aborted) { if (m_client) { - m_socket.close(); + if (m_socket.is_open()) { + m_socket.close(); + } } else { m_connectionManager.stop(this->shared_from_this()); @@ -171,7 +183,8 @@ namespace network m_reply.headers.add("Connection", "keep-alive"); } - asio::async_write(m_socket, m_reply.toBuffers(), [=](asio::error_code ec, std::size_t) { + auto buffers = m_reply.toBuffers(); + asio::async_write(m_socket, buffers, [=](asio::error_code ec, std::size_t) { if (m_persistent) { m_lexer.reset(); m_reply.headers = HTTPHeaders(); @@ -194,27 +207,23 @@ namespace network }); } - /// Perform an asynchronous write operation. + /// Perform an synchronous write operation. void write(HTTPPayload request) { - if (!m_client) { - return; + try + { + auto buffers = request.toBuffers(); + asio::write(m_socket, buffers); } - - asio::async_write(m_socket, request.toBuffers(), [=](asio::error_code ec, std::size_t) { - if (ec) { + catch(const asio::system_error& e) + { + asio::error_code ec = e.code(); + if (ec) { // initiate graceful connection closure asio::error_code ignored_ec; m_socket.shutdown(asio::ip::tcp::socket::shutdown_both, ignored_ec); } - - if (ec.value() == 0) { - return; - } - else if (ec != asio::error::operation_aborted) { - m_socket.close(); - } - }); + } } asio::ip::tcp::socket m_socket; diff --git a/network/rest/http/HTTPClient.h b/network/rest/http/HTTPClient.h index 7f516e8c..368a1ee4 100644 --- a/network/rest/http/HTTPClient.h +++ b/network/rest/http/HTTPClient.h @@ -103,12 +103,16 @@ namespace network /// Opens connection to the network. bool open() { + m_running = true; return run(); } /// Closes connection to the network. void close() { + m_running = false; + m_ioContext.stop(); + if (m_connection != nullptr) { m_connection->stop(); } @@ -125,14 +129,24 @@ namespace network connect(endpoints); - // the entry() call will block until all asynchronous operations - // have finished - m_ioContext.run(); + while (m_running) { + // the entry() call will block until all asynchronous operations + // have finished + m_ioContext.run(); + + m_ioContext.restart(); + connect(endpoints); + } } /// Perform an asynchronous connect operation. void connect(asio::ip::basic_resolver_results& endpoints) { + if (m_connection != nullptr) { + m_connection->stop(); + m_socket = asio::ip::tcp::socket(m_ioContext); + } + asio::connect(m_socket, endpoints); m_connection = std::make_shared(std::move(m_socket), m_connectionManager, m_requestHandler, false, true); m_connection->start(); @@ -146,6 +160,7 @@ namespace network ConnectionTypePtr m_connection; + bool m_running = false; asio::io_context m_ioContext; ConnectionManager m_connectionManager; diff --git a/network/rest/http/HTTPPayload.cpp b/network/rest/http/HTTPPayload.cpp index 5ac59889..19044dad 100644 --- a/network/rest/http/HTTPPayload.cpp +++ b/network/rest/http/HTTPPayload.cpp @@ -351,10 +351,10 @@ std::vector HTTPPayload::toBuffers() /// /// /// -void HTTPPayload::payload(json::object obj, HTTPPayload::StatusType s) +void HTTPPayload::payload(json::object& obj, HTTPPayload::StatusType s) { json::value v = json::value(obj); - std::string json = v.serialize(); + std::string json = std::string(v.serialize()); payload(json, s, "application/json"); } @@ -364,7 +364,7 @@ void HTTPPayload::payload(json::object obj, HTTPPayload::StatusType s) /// /// /// -void HTTPPayload::payload(std::string c, HTTPPayload::StatusType s, const std::string contentType) +void HTTPPayload::payload(std::string& c, HTTPPayload::StatusType s, const std::string& contentType) { content = c; status = s; @@ -380,15 +380,12 @@ void HTTPPayload::payload(std::string c, HTTPPayload::StatusType s, const std::s /// /// /// -/// -HTTPPayload HTTPPayload::requestPayload(std::string method, std::string uri, const std::string contentType) +HTTPPayload HTTPPayload::requestPayload(std::string method, std::string uri) { HTTPPayload rep; rep.isClientPayload = true; rep.method = ::strtoupper(method); rep.uri = std::string(uri); - rep.ensureDefaultHeaders(contentType); - return rep; } @@ -397,7 +394,7 @@ HTTPPayload HTTPPayload::requestPayload(std::string method, std::string uri, con /// /// /// -HTTPPayload HTTPPayload::statusPayload(HTTPPayload::StatusType status, const std::string contentType) +HTTPPayload HTTPPayload::statusPayload(HTTPPayload::StatusType status, const std::string& contentType) { HTTPPayload rep; rep.isClientPayload = false; @@ -415,10 +412,10 @@ HTTPPayload HTTPPayload::statusPayload(HTTPPayload::StatusType status, const std /// /// /// -/// -void HTTPPayload::attachHostHeader(const asio::ip::tcp::endpoint localEndpoint) +/// +void HTTPPayload::attachHostHeader(const asio::ip::tcp::endpoint remoteEndpoint) { - headers.add("Host", std::string(localEndpoint.address().to_string() + ":" + std::to_string(localEndpoint.port()))); + headers.add("Host", std::string(remoteEndpoint.address().to_string() + ":" + std::to_string(remoteEndpoint.port()))); } // --------------------------------------------------------------------------- @@ -432,7 +429,7 @@ void HTTPPayload::attachHostHeader(const asio::ip::tcp::endpoint localEndpoint) void HTTPPayload::ensureDefaultHeaders(const std::string& contentType) { if (!isClientPayload) { - headers.add("Content-Type", contentType); + headers.add("Content-Type", std::string(contentType)); headers.add("Content-Length", std::to_string(content.size())); headers.add("Server", std::string((__EXE_NAME__ "/" __VER__))); } @@ -440,11 +437,8 @@ void HTTPPayload::ensureDefaultHeaders(const std::string& contentType) headers.add("User-Agent", std::string((__EXE_NAME__ "/" __VER__))); headers.add("Accept", "*/*"); if (::strtoupper(method) != HTTP_GET) { - headers.add("Content-Type", contentType); + headers.add("Content-Type", std::string(contentType)); headers.add("Content-Length", std::to_string(content.size())); } } - - //for (auto header : headers.headers()) - // ::LogDebug(LOG_REST, "HTTPPayload::ensureDefaultHeaders() header = %s, value = %s", header.name.c_str(), header.value.c_str()); } diff --git a/network/rest/http/HTTPPayload.h b/network/rest/http/HTTPPayload.h index b9d9eb3f..a85cd0f8 100644 --- a/network/rest/http/HTTPPayload.h +++ b/network/rest/http/HTTPPayload.h @@ -110,17 +110,17 @@ namespace network std::vector toBuffers(); /// Prepares payload for transmission by finalizing status and content type. - void payload(json::object obj, StatusType status = OK); + void payload(json::object& obj, StatusType status = OK); /// Prepares payload for transmission by finalizing status and content type. - void payload(std::string content, StatusType status = OK, const std::string contentType = "text/html"); + void payload(std::string& content, StatusType status = OK, const std::string& contentType = "text/html"); /// Get a request payload. - static HTTPPayload requestPayload(std::string method, std::string uri, const std::string contentType = "text/html"); + static HTTPPayload requestPayload(std::string method, std::string uri); /// Get a status payload. - static HTTPPayload statusPayload(StatusType status, const std::string contentType = "text/html"); + static HTTPPayload statusPayload(StatusType status, const std::string& contentType = "text/html"); /// - void attachHostHeader(const asio::ip::tcp::endpoint localEndpoint); + void attachHostHeader(const asio::ip::tcp::endpoint remoteEndpoint); private: /// void ensureDefaultHeaders(const std::string& contentType = "text/html"); diff --git a/nxdn/packet/Trunk.cpp b/nxdn/packet/Trunk.cpp index e89f2e8e..2d67edce 100644 --- a/nxdn/packet/Trunk.cpp +++ b/nxdn/packet/Trunk.cpp @@ -36,7 +36,7 @@ #include "nxdn/Sync.h" #include "nxdn/NXDNUtils.h" #include "edac/CRC.h" -#include "remote/RemoteCommand.h" +#include "remote/RESTClient.h" #include "HostMain.h" #include "Log.h" #include "Utils.h" @@ -514,11 +514,13 @@ bool Trunk::writeRF_Message_Grant(uint32_t srcId, uint32_t dstId, uint8_t servic ::lookups::VoiceChData voiceChData = m_nxdn->m_affiliations.getRFChData(chNo); if (voiceChData.isValidCh() && !voiceChData.address().empty() && voiceChData.port() > 0 && chNo != m_nxdn->m_siteData.channelNo()) { - std::stringstream ss; - ss << "permit-tg " << modem::DVM_STATE::STATE_NXDN << " " << dstId; + json::object req = json::object(); + int state = modem::DVM_STATE::STATE_NXDN; + req["state"].set(state); + req["dstId"].set(dstId); - RemoteCommand::send(voiceChData.address(), voiceChData.port(), voiceChData.password(), - ss.str(), m_nxdn->m_debug); + RESTClient::send(voiceChData.address(), voiceChData.port(), voiceChData.password(), + HTTP_PUT, PUT_PERMIT_TG, req, m_nxdn->m_debug); } } diff --git a/p25/packet/Trunk.cpp b/p25/packet/Trunk.cpp index ab223bda..1033ca4b 100644 --- a/p25/packet/Trunk.cpp +++ b/p25/packet/Trunk.cpp @@ -35,7 +35,7 @@ #include "p25/P25Utils.h" #include "p25/Sync.h" #include "edac/CRC.h" -#include "remote/RemoteCommand.h" +#include "remote/RESTClient.h" #include "Log.h" #include "Utils.h" @@ -2223,11 +2223,13 @@ bool Trunk::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_t serviceOp ::lookups::VoiceChData voiceChData = m_p25->m_affiliations.getRFChData(chNo); if (voiceChData.isValidCh() && !voiceChData.address().empty() && voiceChData.port() > 0 && chNo != m_p25->m_siteData.channelNo()) { - std::stringstream ss; - ss << "permit-tg " << modem::DVM_STATE::STATE_P25 << " " << dstId; + json::object req = json::object(); + int state = modem::DVM_STATE::STATE_P25; + req["state"].set(state); + req["dstId"].set(dstId); - RemoteCommand::send(voiceChData.address(), voiceChData.port(), voiceChData.password(), - ss.str(), m_p25->m_debug); + RESTClient::send(voiceChData.address(), voiceChData.port(), voiceChData.password(), + HTTP_PUT, PUT_PERMIT_TG, req, m_p25->m_debug); } } @@ -2258,11 +2260,13 @@ bool Trunk::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_t serviceOp ::lookups::VoiceChData voiceChData = m_p25->m_affiliations.getRFChData(chNo); if (voiceChData.isValidCh() && !voiceChData.address().empty() && voiceChData.port() > 0 && chNo != m_p25->m_siteData.channelNo()) { - std::stringstream ss; - ss << "permit-tg " << modem::DVM_STATE::STATE_P25 << " " << dstId; + json::object req = json::object(); + int state = modem::DVM_STATE::STATE_P25; + req["state"].set(state); + req["dstId"].set(dstId); - RemoteCommand::send(voiceChData.address(), voiceChData.port(), voiceChData.password(), - ss.str(), m_p25->m_debug); + RESTClient::send(voiceChData.address(), voiceChData.port(), voiceChData.password(), + HTTP_PUT, PUT_PERMIT_TG, req, m_p25->m_debug); } } diff --git a/remote/RESTClient.cpp b/remote/RESTClient.cpp new file mode 100644 index 00000000..76b67823 --- /dev/null +++ b/remote/RESTClient.cpp @@ -0,0 +1,275 @@ +/** +* 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 +* +* 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 "network/json/json.h" +#include "network/rest/http/HTTPClient.h" +#include "network/rest/RequestDispatcher.h" +#include "remote/RESTClient.h" +#include "Thread.h" +#include "Log.h" +#include "Utils.h" + +using namespace network; +using namespace network::rest::http; + +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +#define ERRNO_SOCK_OPEN 98 +#define ERRNO_BAD_API_RESPONSE 97 +#define ERRNO_API_CALL_TIMEOUT 96 + +// --------------------------------------------------------------------------- +// Static Class Members +// --------------------------------------------------------------------------- + +bool RESTClient::m_responseAvailable = false; +HTTPPayload RESTClient::m_response; + +bool RESTClient::m_debug = false; + +// --------------------------------------------------------------------------- +// Global Functions +// --------------------------------------------------------------------------- + +/// +/// +/// +/// +/// +/// +bool parseResponseBody(const HTTPPayload& response, json::object& obj) +{ + std::string contentType = response.headers.find("Content-Type"); + if (contentType != "application/json") { + return false; + } + + // parse JSON body + json::value v; + std::string err = json::parse(v, response.content); + if (!err.empty()) { + return false; + } + + // ensure parsed JSON is an object + if (!v.is()) { + return false; + } + + obj = v.get(); + return true; +} + +// --------------------------------------------------------------------------- +// Public Class Members +// --------------------------------------------------------------------------- + +/// +/// Initializes a new instance of the RESTClient class. +/// +/// Network Hostname/IP address to connect to. +/// Network port number. +/// Authentication password. +/// Flag indicating whether debug is enabled. +RESTClient::RESTClient(const std::string& address, uint32_t port, const std::string& password, bool debug) : + m_address(address), + m_port(port), + m_password(password) +{ + assert(!address.empty()); + assert(port > 0U); + + m_debug = debug; +} + +/// +/// Finalizes a instance of the RESTClient class. +/// +RESTClient::~RESTClient() +{ + /* stub */ +} + +/// +/// Sends remote control command to the specified modem. +/// +/// REST API method. +/// REST API endpoint. +/// REST API endpoint payload. +/// EXIT_SUCCESS, if command was sent, otherwise EXIT_FAILURE. +int RESTClient::send(const std::string method, const std::string endpoint, json::object payload) +{ + assert(!m_address.empty()); + assert(m_port > 0U); + + return send(m_address, m_port, m_password, method, endpoint, payload, m_debug); +} + +/// +/// Sends remote control command to the specified modem. +/// +/// Network Hostname/IP address to connect to. +/// Network port number. +/// Authentication password. +/// REST API method. +/// REST API endpoint. +/// REST API endpoint payload. +/// Flag indicating whether debug is enabled. +/// EXIT_SUCCESS, if command was sent, otherwise EXIT_FAILURE. +int RESTClient::send(const std::string& address, uint32_t port, const std::string& password, const std::string method, + const std::string endpoint, json::object payload, bool debug) +{ + assert(!address.empty()); + assert(port > 0U); + assert(password.empty()); + + m_debug = debug; + + typedef network::rest::BasicRequestDispatcher RESTDispatcherType; + RESTDispatcherType m_dispatcher(RESTClient::responseHandler); + + HTTPClient client(address, port); + if (!client.open()) + return ERRNO_SOCK_OPEN; + client.setHandler(m_dispatcher); + + // generate password SHA hash + 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); + + uint8_t out[32U]; + ::memset(out, 0x00U, 32U); + + edac::SHA256 sha256; + sha256.buffer(in, (uint32_t)(size), out); + + std::stringstream ss; + ss << std::hex; + + for (uint8_t i = 0; i < 32U; i++) + ss << std::setw(2) << std::setfill('0') << (int)out[i]; + + std::string hash = ss.str(); + + // send authentication API + json::object request = json::object(); + request["auth"].set(hash); + + HTTPPayload httpPayload = HTTPPayload::requestPayload(HTTP_PUT, "/auth"); + httpPayload.payload(request); + client.request(httpPayload); + + // wait for response and parse + if (wait()) { + client.close(); + return ERRNO_API_CALL_TIMEOUT; + } + + json::object rsp = json::object(); + if (!parseResponseBody(m_response, rsp)) { + return ERRNO_BAD_API_RESPONSE; + } + + std::string token = ""; + int status = rsp["status"].get(); + if (status == HTTPPayload::StatusType::OK) { + token = rsp["token"].get(); + } + else { + client.close(); + return ERRNO_BAD_API_RESPONSE; + } + + // send actual API request + httpPayload = HTTPPayload::requestPayload(method, endpoint); + httpPayload.headers.add("X-DVM-Auth-Token", token); + httpPayload.payload(payload); + client.request(httpPayload); + + // wait for response and parse + if (wait()) { + client.close(); + return ERRNO_API_CALL_TIMEOUT; + } + + fprintf(stdout, "%s\r\n", m_response.content.c_str()); + + client.close(); + return EXIT_SUCCESS; +} + +// --------------------------------------------------------------------------- +// Private Class Members +// --------------------------------------------------------------------------- + +/// +/// +/// +/// +/// +void RESTClient::responseHandler(const HTTPPayload& request, HTTPPayload& reply) +{ + m_responseAvailable = true; + m_response = request; +} + +/// +/// +/// +bool RESTClient::wait() +{ + m_responseAvailable = false; + + int timeout = 500; + while (!m_responseAvailable && timeout > 0) { + timeout--; + Thread::sleep(1); + } + + if (timeout == 0) { + return true; + } + + return false; +} diff --git a/remote/RemoteCommand.h b/remote/RESTClient.h similarity index 57% rename from remote/RemoteCommand.h rename to remote/RESTClient.h index 2022acec..ba93135b 100644 --- a/remote/RemoteCommand.h +++ b/remote/RESTClient.h @@ -6,13 +6,8 @@ * @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 +* Copyright (C) 2023 by Bryan Biedenkapp * * 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 @@ -28,38 +23,51 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ -#if !defined(__REMOTE_COMMAND_H__) -#define __REMOTE_COMMAND_H__ +#if !defined(__REST_CLIENT_H__) +#define __REST_CLIENT_H__ #include "Defines.h" +#include "network/json/json.h" +#include "network/rest/http/HTTPPayload.h" #include // --------------------------------------------------------------------------- // Class Declaration -// This class implements the core remote command logic. +// This class implements the REST client logic. // --------------------------------------------------------------------------- -class HOST_SW_API RemoteCommand +class HOST_SW_API RESTClient { public: - /// Initializes a new instance of the RemoteCommand class. - RemoteCommand(const std::string& address, uint32_t port, const std::string& password, bool debug); - /// Finalizes a instance of the RemoteCommand class. - ~RemoteCommand(); + /// Initializes a new instance of the RESTClient class. + RESTClient(const std::string& address, uint32_t port, const std::string& password, bool debug); + /// Finalizes a instance of the RESTClient class. + ~RESTClient(); /// Sends remote control command to the specified modem. - int send(const std::string& command); + int send(const std::string method, const std::string endpoint, json::object payload); /// Sends remote control command to the specified modem. - static int send(const std::string& address, uint32_t port, const std::string& password, const std::string& command, bool debug = false); + static int send(const std::string& address, uint32_t port, const std::string& password, const std::string method, + const std::string endpoint, json::object payload, bool debug = false); private: + typedef network::rest::http::HTTPPayload HTTPPayload; + /// + static void responseHandler(const HTTPPayload& request, HTTPPayload& reply); + + /// + static bool wait(); + std::string m_address; uint32_t m_port; std::string m_password; - bool m_debug; + static bool m_responseAvailable; + static HTTPPayload m_response; + + static bool m_debug; }; #endif // __REMOTE_COMMAND_H__ diff --git a/remote/RESTClientMain.cpp b/remote/RESTClientMain.cpp new file mode 100644 index 00000000..705b755f --- /dev/null +++ b/remote/RESTClientMain.cpp @@ -0,0 +1,684 @@ +/** +* Digital Voice Modem - Remote Command Client +* GPLv2 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Remote Command Client +* +*/ +/* +* Copyright (C) 2023 by Bryan Biedenkapp +* +* 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 "remote/RESTClient.h" +#include "network/RESTDefines.h" +#include "Thread.h" +#include "Log.h" +#include "Utils.h" + +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +#undef __PROG_NAME__ +#define __PROG_NAME__ "Digital Voice Modem (DVM) REST API Tool" +#undef __EXE_NAME__ +#define __EXE_NAME__ "dvmcmd" + +#define ERRNO_REMOTE_CMD 99 + +// --------------------------------------------------------------------------- +// 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_RELEASE_GRANTS "rel-grnts" +#define RCD_RELEASE_AFFS "rel-affs" + +#define RCD_DMR_BEACON "dmr-beacon" +#define RCD_P25_CC "p25-cc" +#define RCD_P25_CC_FALLBACK "p25-cc-fallback" +#define RCD_NXDN_CC "nxdn-cc" + +#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_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_NXDN_CC_DEDICATED "nxdn-cc-dedicated" + +#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 BAD_CMD_STR "Bad or invalid remote command" +#define NO_DATA_CMD_STR "No data" +#define INVALID_AUTH_STR "Invalid authentication" +#define INVALID_OPT_STR "Invalid command arguments, " + +// --------------------------------------------------------------------------- +// Macros +// --------------------------------------------------------------------------- + +#define IS(s) (::strcmp(argv[i], s) == 0) + +// --------------------------------------------------------------------------- +// Global Variables +// --------------------------------------------------------------------------- + +static std::string g_progExe = std::string(__EXE_NAME__); +static std::string g_remoteAddress = std::string("127.0.0.1"); +static uint32_t g_remotePort = REST_API_DEFAULT_PORT; +static std::string g_remotePassword = std::string(); +static bool g_debug = false; + +// --------------------------------------------------------------------------- +// Global Functions +// --------------------------------------------------------------------------- + +void fatal(const char* message) +{ + ::fprintf(stderr, "%s: %s\n", g_progExe.c_str(), message); + exit(EXIT_FAILURE); +} + +void usage(const char* message, const char* arg) +{ + ::fprintf(stdout, __PROG_NAME__ " %s (built %s)\r\n", __VER__, __BUILD__); + ::fprintf(stdout, "Copyright (c) 2023 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors.\n"); + ::fprintf(stdout, "Portions Copyright (c) 2015-2021 by Jonathan Naylor, G4KLX and others\n\n"); + if (message != nullptr) { + ::fprintf(stderr, "%s: ", g_progExe.c_str()); + ::fprintf(stderr, message, arg); + ::fprintf(stderr, "\n\n"); + } + + ::fprintf(stdout, "usage: %s [-dvh] [-a
] [-p ] [-P ] \n\n" + " -a remote modem command address\n" + " -p remote modem command port\n" + " -P remote modem authentication password\n" + "\n" + " -d enable debug\n" + " -v show version information\n" + " -h show this screen\n" + " -- stop handling options\n", + g_progExe.c_str()); + exit(EXIT_FAILURE); +} + +int checkArgs(int argc, char* argv[]) +{ + int i, p = 0; + + // iterate through arguments + for (i = 1; i <= argc; i++) + { + if (argv[i] == nullptr) { + break; + } + + if (*argv[i] != '-') { + continue; + } + else if (IS("--")) { + ++p; + break; + } + else if (IS("-a")) { + if ((argc - 1) <= 0) + usage("error: %s", "must specify the address to connect to"); + g_remoteAddress = std::string(argv[++i]); + + if (g_remoteAddress == "") + usage("error: %s", "remote address cannot be blank!"); + + p += 2; + } + else if (IS("-p")) { + if ((argc - 1) <= 0) + usage("error: %s", "must specify the port to connect to"); + g_remotePort = (uint32_t)::atoi(argv[++i]); + + if (g_remotePort == 0) + usage("error: %s", "remote port number cannot be blank or 0!"); + + p += 2; + } + else if (IS("-P")) { + if ((argc - 1) <= 0) + usage("error: %s", "must specify the auth password"); + g_remotePassword = std::string(argv[++i]); + + if (g_remotePassword == "") + usage("error: %s", "remote auth password cannot be blank!"); + + p += 2; + } + else if (IS("-d")) { + ++p; + g_debug = true; + } + else if (IS("-v")) { + ::fprintf(stdout, __PROG_NAME__ " %s (built %s)\r\n", __VER__, __BUILD__); + ::fprintf(stdout, "Copyright (c) 2017-2023 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors.\r\n"); + if (argc == 2) + exit(EXIT_SUCCESS); + } + else if (IS("-h")) { + usage(nullptr, nullptr); + if (argc == 2) + exit(EXIT_SUCCESS); + } + else { + usage("unrecognized option `%s'", argv[i]); + } + } + + if (p < 0 || p > argc) { + p = 0; + } + + return ++p; +} + +/// +std::string getArgString(std::vector args, uint32_t n) +{ + n += 1; + if (n >= args.size()) + return ""; + + return args.at(n); +} + +/// +uint64_t getArgUInt64(std::vector args, uint32_t n) { return (uint64_t)::atol(getArgString(args, n).c_str()); } +/// +uint32_t getArgUInt32(std::vector args, uint32_t n) { return (uint32_t)::atoi(getArgString(args, n).c_str()); } +/// +int32_t getArgInt32(std::vector args, uint32_t n) { return ::atoi(getArgString(args, n).c_str()); } +/// +uint16_t getArgUInt16(std::vector args, uint32_t n) { return (uint16_t)::atoi(getArgString(args, n).c_str()); } +/// +int16_t getArgInt16(std::vector args, uint32_t n) { return (int16_t)::atoi(getArgString(args, n).c_str()); } +/// +uint8_t getArgUInt8(std::vector args, uint32_t n) { return (uint8_t)::atoi(getArgString(args, n).c_str()); } +/// +int8_t getArgInt8(std::vector args, uint32_t n) { return (int8_t)::atoi(getArgString(args, n).c_str()); } + +/// +/// Helper to print the remote control help. +/// +/// +std::string displayHelp() +{ + std::string reply = ""; + + reply += __PROG_NAME__ " " __VER__ " (built " __BUILD__ ")\r\n"; + reply += "Copyright (c) 2017-2023 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors.\r\n\r\n"; + + 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"; + reply += " rel-grnts Forcibly releases all channel grants\r\n"; + reply += " rel-affs Forcibly releases all group affiliations\r\n"; + reply += "\r\n"; + reply += " dmr-beacon Transmits a DMR beacon burst\r\n"; + 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"; + reply += " nxdn-cc Transmits a non-continous NXDN CC burst\r\n"; + reply += "\r\n"; + reply += " dmr-debug \r\n"; + reply += " dmr-dump-csbk <0/1>\r\n"; + reply += " p25-debug \r\n"; + reply += " p25-dump-tsbk <0/1>\r\n"; + reply += " nxdn-debug \r\n"; + reply += " nxdn-dump-rcch <0/1>\r\n"; + 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 Enables or disables dedicated control channel\r\n"; + reply += " dmr-cc-bcast Enables or disables broadcast of the control channel\r\n"; + 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-cc-dedicated Enables or disables dedicated control channel\r\n"; + reply += " p25-cc-bcast Enables or disables broadcast of the control channel\r\n"; + reply += "\r\nNXDN Commands:\r\n"; + reply += " nxdn-cc-dedicated Enables or disables dedicated control channel\r\n"; + + return reply; +} + +// --------------------------------------------------------------------------- +// Program Entry Point +// --------------------------------------------------------------------------- + +int main(int argc, char** argv) +{ + if (argv[0] != nullptr && *argv[0] != 0) + g_progExe = std::string(argv[0]); + + if (argc < 2) { + usage("error: %s", "must specify the remote command!"); + return ERRNO_REMOTE_CMD; + } + + if (argc > 1) { + // check arguments + int i = checkArgs(argc, argv); + if (i < argc) { + argc -= i; + argv += i; + } + else { + argc--; + argv++; + } + } + + // process command + std::string cmd = std::string(argv[0]); + for (int i = 1; i < argc; i++) { + cmd += " "; + cmd += std::string(argv[i]); + } + + // initialize system logging + bool ret = ::LogInitialise("", "", 0U, 1U, true); + if (!ret) { + ::fprintf(stderr, "unable to open the log file\n"); + return 1; + } + + if (g_remotePassword.empty()) { + ::fprintf(stderr, "must specify password!\n"); + return 1; + } + + RESTClient* client = new RESTClient(g_remoteAddress, g_remotePort, g_remotePassword, g_debug); + int retCode = EXIT_SUCCESS; + + std::vector args = std::vector(); + args.clear(); + + // parse the original command into a vector of strings. + char* b = const_cast(cmd.c_str()); + 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_REST, BAD_CMD_STR); + } + else { + std::string rcom = args.at(0); + uint32_t argCnt = args.size() - 1; + if (g_debug) { + LogInfoEx(LOG_REST, "cmd = %s, argCnt = %u", rcom.c_str(), argCnt); + } + + // determine command to execute + if (rcom == RCD_GET_HELP) { + std::string help = displayHelp(); + fprintf(stdout, "%s", help.c_str()); + } + else if (rcom == RCD_GET_VERSION) { + retCode = client->send(HTTP_GET, GET_VERSION, json::object()); + } + else if (rcom == RCD_GET_STATUS) { + retCode = client->send(HTTP_GET, GET_STATUS, json::object()); + } + else if (rcom == RCD_GET_VOICE_CH) { + retCode = client->send(HTTP_GET, GET_VOICE_CH, json::object()); + } + else if (rcom == RCD_MODE && argCnt >= 1U) { + std::string mode = getArgString(args, 0U); + + json::object req = json::object(); + req["mode"].set(mode); + + retCode = client->send(HTTP_PUT, PUT_MDM_MODE, req); + } + else if (rcom == RCD_KILL) { + json::object req = json::object(); + bool force = false; + req["force"].set(force); + + retCode = client->send(HTTP_PUT, PUT_MDM_KILL, req); + } + else if (rcom == RCD_FORCE_KILL) { + json::object req = json::object(); + bool force = true; + req["force"].set(force); + + retCode = client->send(HTTP_PUT, PUT_MDM_KILL, req); + } + else if (rcom == RCD_PERMIT_TG && argCnt >= 1U) { + json::object req = json::object(); + int state = getArgInt32(args, 0U); + req["state"].set(state); + + uint32_t dstId = getArgInt32(args, 1U); + req["dstId"].set(dstId); + + if (state == 1) { + uint8_t slot = getArgInt8(args, 2U); + req["slot"].set(slot); + } + + retCode = client->send(HTTP_PUT, PUT_PERMIT_TG, req); + } + else if (rcom == RCD_GRANT_TG && argCnt >= 1U) { + json::object req = json::object(); + int state = getArgInt32(args, 0U); + req["state"].set(state); + + uint32_t dstId = getArgInt32(args, 1U); + req["dstId"].set(dstId); + + uint8_t unitToUnit = getArgInt32(args, 2U); + if (unitToUnit == 0U) { + bool b = false; + req["unitToUnit"].set(b); + } else { + bool b = true; + req["unitToUnit"].set(b); + } + + if (state == 1) { + uint8_t slot = getArgInt8(args, 3U); + req["slot"].set(slot); + } + + retCode = client->send(HTTP_PUT, PUT_GRANT_TG, req); + } + else if (rcom == RCD_RID_WLIST && argCnt >= 1U) { + uint32_t srcId = getArgUInt32(args, 0U); + retCode = client->send(HTTP_GET, GET_RID_WHITELIST_BASE + std::to_string(srcId), json::object()); + } + else if (rcom == RCD_RID_BLIST && argCnt >= 1U) { + uint32_t srcId = getArgUInt32(args, 0U); + retCode = client->send(HTTP_GET, GET_RID_BLACKLIST_BASE + std::to_string(srcId), json::object()); + } + else if (rcom == RCD_RELEASE_GRANTS) { + retCode = client->send(HTTP_GET, GET_RELEASE_GRNTS, json::object()); + } + else if (rcom == RCD_RELEASE_AFFS) { + retCode = client->send(HTTP_GET, GET_RELEASE_AFFS, json::object()); + } + + /* + ** Digital Mobile Radio + */ + + else if (rcom == RCD_DMR_BEACON) { + retCode = client->send(HTTP_GET, GET_DMR_BEACON, json::object()); + } + else if (rcom == RCD_DMR_DEBUG) { + if (argCnt < 2U) { + retCode = client->send(HTTP_GET, GET_DMR_DEBUG_BASE, json::object()); + } + else { + uint8_t debug = getArgUInt8(args, 0U); + uint8_t verbose = getArgUInt8(args, 1U); + retCode = client->send(HTTP_GET, GET_DMR_DEBUG_BASE + std::to_string(debug) + "/" + std::to_string(verbose), json::object()); + } + } + else if (rcom == RCD_DMR_DUMP_CSBK) { + if (argCnt < 1U) { + retCode = client->send(HTTP_GET, GET_DMR_DUMP_CSBK_BASE, json::object()); + } + else { + uint8_t verbose = getArgUInt8(args, 0U); + retCode = client->send(HTTP_GET, GET_DMR_DUMP_CSBK_BASE + std::to_string(verbose), json::object()); + } + } + else if (rcom == RCD_DMR_RID_PAGE && argCnt >= 2U) { + json::object req = json::object(); + req["command"].set(std::string(RID_CMD_PAGE)); + uint8_t slotNo = getArgInt8(args, 0U); + req["slot"].set(slotNo); + uint32_t dstId = getArgUInt32(args, 1U); + req["dstId"].set(dstId); + + retCode = client->send(HTTP_PUT, PUT_DMR_RID, req); + } + else if (rcom == RCD_DMR_RID_CHECK && argCnt >= 2U) { + json::object req = json::object(); + req["command"].set(std::string(RID_CMD_CHECK)); + uint8_t slotNo = getArgInt8(args, 0U); + req["slot"].set(slotNo); + uint32_t dstId = getArgUInt32(args, 1U); + req["dstId"].set(dstId); + + retCode = client->send(HTTP_PUT, PUT_DMR_RID, req); + } + else if (rcom == RCD_DMR_RID_INHIBIT && argCnt >= 2U) { + json::object req = json::object(); + req["command"].set(std::string(RID_CMD_INHIBIT)); + uint8_t slotNo = getArgInt8(args, 0U); + req["slot"].set(slotNo); + uint32_t dstId = getArgUInt32(args, 1U); + req["dstId"].set(dstId); + + retCode = client->send(HTTP_PUT, PUT_DMR_RID, req); + } + else if (rcom == RCD_DMR_RID_UNINHIBIT && argCnt >= 2U) { + json::object req = json::object(); + req["command"].set(std::string(RID_CMD_UNINHIBIT)); + uint8_t slotNo = getArgInt8(args, 0U); + req["slot"].set(slotNo); + uint32_t dstId = getArgUInt32(args, 1U); + req["dstId"].set(dstId); + + retCode = client->send(HTTP_PUT, PUT_DMR_RID, req); + } + else if (rcom == RCD_DMR_CC_DEDICATED) { + retCode = client->send(HTTP_GET, GET_DMR_CC_DEDICATED, json::object()); + } + else if (rcom == RCD_DMR_CC_BCAST) { + retCode = client->send(HTTP_GET, GET_DMR_CC_BCAST, json::object()); + } + + /* + ** Project 25 + */ + + else if (rcom == RCD_P25_CC) { + retCode = client->send(HTTP_GET, GET_P25_CC, json::object()); + } + else if (rcom == RCD_P25_DEBUG) { + if (argCnt < 2U) { + retCode = client->send(HTTP_GET, GET_P25_DEBUG_BASE, json::object()); + } + else { + uint8_t debug = getArgUInt8(args, 0U); + uint8_t verbose = getArgUInt8(args, 1U); + retCode = client->send(HTTP_GET, GET_P25_DEBUG_BASE + std::to_string(debug) + "/" + std::to_string(verbose), json::object()); + } + } + else if (rcom == RCD_P25_DUMP_TSBK) { + if (argCnt < 1U) { + retCode = client->send(HTTP_GET, GET_P25_DUMP_TSBK_BASE, json::object()); + } + else { + uint8_t verbose = getArgUInt8(args, 0U); + retCode = client->send(HTTP_GET, GET_P25_DUMP_TSBK_BASE + std::to_string(verbose), json::object()); + } + } + else if (rcom == RCD_P25_RID_PAGE && argCnt >= 2U) { + json::object req = json::object(); + req["command"].set(std::string(RID_CMD_PAGE)); + uint32_t dstId = getArgUInt32(args, 0U); + req["dstId"].set(dstId); + + retCode = client->send(HTTP_PUT, PUT_P25_RID, req); + } + else if (rcom == RCD_P25_RID_CHECK && argCnt >= 2U) { + json::object req = json::object(); + req["command"].set(std::string(RID_CMD_CHECK)); + uint32_t dstId = getArgUInt32(args, 0U); + req["dstId"].set(dstId); + + retCode = client->send(HTTP_PUT, PUT_P25_RID, req); + } + else if (rcom == RCD_P25_RID_INHIBIT && argCnt >= 2U) { + json::object req = json::object(); + req["command"].set(std::string(RID_CMD_INHIBIT)); + uint32_t dstId = getArgUInt32(args, 0U); + req["dstId"].set(dstId); + + retCode = client->send(HTTP_PUT, PUT_P25_RID, req); + } + else if (rcom == RCD_P25_RID_UNINHIBIT && argCnt >= 2U) { + json::object req = json::object(); + req["command"].set(std::string(RID_CMD_UNINHIBIT)); + uint32_t dstId = getArgUInt32(args, 0U); + req["dstId"].set(dstId); + + retCode = client->send(HTTP_PUT, PUT_P25_RID, req); + } + else if (rcom == RCD_P25_RID_GAQ && argCnt >= 2U) { + json::object req = json::object(); + req["command"].set(std::string(RID_CMD_GAQ)); + uint32_t dstId = getArgUInt32(args, 0U); + req["dstId"].set(dstId); + + retCode = client->send(HTTP_PUT, PUT_P25_RID, req); + } + else if (rcom == RCD_P25_RID_UREG && argCnt >= 2U) { + json::object req = json::object(); + req["command"].set(std::string(RID_CMD_UREG)); + uint32_t dstId = getArgUInt32(args, 0U); + req["dstId"].set(dstId); + + retCode = client->send(HTTP_PUT, PUT_P25_RID, req); + } + else if (rcom == RCD_P25_CC_DEDICATED) { + retCode = client->send(HTTP_GET, GET_P25_CC_DEDICATED, json::object()); + } + else if (rcom == RCD_P25_CC_BCAST) { + retCode = client->send(HTTP_GET, GET_P25_CC_BCAST, json::object()); + } + + /* + ** Next Generation Digital Narrowband + */ + + else if (rcom == RCD_NXDN_CC) { + retCode = client->send(HTTP_GET, GET_NXDN_CC, json::object()); + } + else if (rcom == RCD_NXDN_DEBUG) { + if (argCnt < 2U) { + retCode = client->send(HTTP_GET, GET_NXDN_DEBUG_BASE, json::object()); + } + else { + uint8_t debug = getArgUInt8(args, 0U); + uint8_t verbose = getArgUInt8(args, 1U); + retCode = client->send(HTTP_GET, GET_NXDN_DEBUG_BASE + std::to_string(debug) + "/" + std::to_string(verbose), json::object()); + } + } + else if (rcom == RCD_NXDN_DUMP_RCCH) { + if (argCnt < 1U) { + retCode = client->send(HTTP_GET, GET_NXDN_DUMP_RCCH_BASE, json::object()); + } + else { + uint8_t verbose = getArgUInt8(args, 0U); + retCode = client->send(HTTP_GET, GET_NXDN_DUMP_RCCH_BASE + std::to_string(verbose), json::object()); + } + } + else if (rcom == RCD_NXDN_CC_DEDICATED) { + retCode = client->send(HTTP_GET, GET_NXDN_CC_DEDICATED, json::object()); + } + + else { + args.clear(); + LogError(LOG_REST, BAD_CMD_STR " (\"%s\")", rcom.c_str()); + } + } + + ::LogFinalise(); + return retCode; +} diff --git a/remote/RemoteCommand.cpp b/remote/RemoteCommand.cpp deleted file mode 100644 index f23dc8ac..00000000 --- a/remote/RemoteCommand.cpp +++ /dev/null @@ -1,219 +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 -* -* 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 "network/UDPSocket.h" -#include "RemoteCommand.h" -#include "Thread.h" -#include "Log.h" -#include "Utils.h" - -using namespace network; - -#include -#include -#include -#include - -// --------------------------------------------------------------------------- -// Constants -// --------------------------------------------------------------------------- - -#define ERRNO_SOCK_OPEN 98 -#define ERRNO_ADDR_LOOKUP 97 -#define ERRNO_FAILED_TO_SEND 96 - -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; -const uint32_t RESPONSE_BUFFER_LEN = 4095U; - -// --------------------------------------------------------------------------- -// Public Class Members -// --------------------------------------------------------------------------- - -/// -/// Initializes a new instance of the RemoteCommand class. -/// -/// Network Hostname/IP address to connect to. -/// Network port number. -/// Authentication password. -/// Flag indicating whether debug is enabled. -RemoteCommand::RemoteCommand(const std::string& address, uint32_t port, const std::string& password, bool debug) : - m_address(address), - m_port(port), - m_password(password), - m_debug(debug) -{ - assert(!address.empty()); - assert(port > 0U); -} - -/// -/// Finalizes a instance of the RemoteCommand class. -/// -RemoteCommand::~RemoteCommand() -{ - /* stub */ -} - -/// -/// Sends remote control command to the specified modem. -/// -/// Command string to send to remote modem. -/// EXIT_SUCCESS, if command was sent, otherwise EXIT_FAILURE. -int RemoteCommand::send(const std::string& command) -{ - assert(!m_address.empty()); - assert(m_port > 0U); - - return send(m_address, m_port, m_password, command, m_debug); -} - -/// -/// Sends remote control command to the specified modem. -/// -/// Network Hostname/IP address to connect to. -/// Network port number. -/// Authentication password. -/// Command string to send to remote modem. -/// Flag indicating whether debug is enabled. -/// EXIT_SUCCESS, if command was sent, otherwise EXIT_FAILURE. -int RemoteCommand::send(const std::string& address, uint32_t port, const std::string& password, const std::string& command, bool debug) -{ - assert(!address.empty()); - assert(port > 0U); - UDPSocket socket(0U); - - bool ret = socket.open(); - if (!ret) - return ERRNO_SOCK_OPEN; - - uint8_t buffer[RC_BUFFER_LENGTH]; - ::memset(buffer, 0x00U, RC_BUFFER_LENGTH); - - sockaddr_storage addr; - uint32_t addrLen; - - if (UDPSocket::lookup(address, port, addr, addrLen) != 0) { - ::LogError(LOG_HOST, "Could not lookup the address of remote"); - return ERRNO_ADDR_LOOKUP; - } - - ::LogInfoEx(LOG_HOST, "sending RCON command \"%s\" to %s:%u", command.c_str(), address.c_str(), port); - - buffer[0U] = RCON_FRAME_START; - buffer[1U] = START_OF_TEXT; - - 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); - - uint8_t out[32U]; - ::memset(out, 0x00U, 32U); - - edac::SHA256 sha256; - sha256.buffer(in, (uint32_t)(size), out); - - ::memcpy(buffer + 2U, out, 32U); - } - - buffer[34U] = REC_SEPARATOR; - ::memcpy(buffer + 35U, command.c_str(), command.size()); - - buffer[35U + command.size()] = END_OF_TEXT; - - if (debug) - Utils::dump(1U, "RCON Sent", (uint8_t*)buffer, 36U + command.size()); - - ret = socket.write((uint8_t *)buffer, 36U + command.size(), addr, addrLen); - if (!ret) { - socket.close(); - ::LogError(LOG_HOST, "Failed to send command: \"%s\"", command.c_str()); - return ERRNO_FAILED_TO_SEND; - } - - Thread::sleep(100); - - uint8_t response[RESPONSE_BUFFER_LEN]; - ::memset(response, 0x00U, RESPONSE_BUFFER_LEN); - - int len = 1; - uint32_t offs = 0U; - do - { - ::memset(buffer, 0x00U, RC_BUFFER_LENGTH); - int len = socket.read((uint8_t *)buffer, RC_BUFFER_LENGTH, addr, addrLen); - if (len <= 1) - break; - - if (offs + len > RESPONSE_BUFFER_LEN) - break; - - if (debug) - ::LogDebug(LOG_HOST, "RemoteCommand::send() block len = %u, offs = %u", len - 3, offs); - - buffer[len] = '\0'; - - if (debug) - Utils::dump(1U, "RCON Received", (uint8_t*)buffer, len); - - // make sure this is an RCON response - if (buffer[0U] != RCON_FRAME_START) { - ::LogError(LOG_HOST, "Invalid response from host %s:%u", address.c_str(), port); - socket.close(); - return EXIT_FAILURE; - } - - if (buffer[1U] != START_OF_TEXT) { - ::LogError(LOG_HOST, "Invalid response from host %s:%u", address.c_str(), port); - socket.close(); - return EXIT_FAILURE; - } - - ::memcpy(response + offs, buffer + 2U, len - 3U); - offs += len - 3U; - - Thread::sleep(100); - } while (buffer[len - 1U] != END_OF_TEXT); - - ::LogInfoEx(LOG_HOST, ">> %s", response); - - socket.close(); - return EXIT_SUCCESS; -} diff --git a/remote/RemoteCommandMain.cpp b/remote/RemoteCommandMain.cpp deleted file mode 100644 index 1215ab9e..00000000 --- a/remote/RemoteCommandMain.cpp +++ /dev/null @@ -1,228 +0,0 @@ -/** -* Digital Voice Modem - Remote Command Client -* GPLv2 Open Source. Use is subject to license terms. -* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. -* -* @package DVM / Remote Command Client -* -*/ -// -// 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 -* -* 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 "RemoteCommand.h" -#include "edac/SHA256.h" -#include "network/UDPSocket.h" -#include "Thread.h" -#include "Log.h" -#include "Utils.h" - -using namespace network; - -#include -#include -#include -#include - -// --------------------------------------------------------------------------- -// Constants -// --------------------------------------------------------------------------- - -#undef __PROG_NAME__ -#define __PROG_NAME__ "Digital Voice Modem (DVM) RCON Tool" -#undef __EXE_NAME__ -#define __EXE_NAME__ "dvmcmd" - -#define ERRNO_REMOTE_CMD 99 - -// --------------------------------------------------------------------------- -// Macros -// --------------------------------------------------------------------------- - -#define IS(s) (::strcmp(argv[i], s) == 0) - -// --------------------------------------------------------------------------- -// Global Variables -// --------------------------------------------------------------------------- - -static std::string g_progExe = std::string(__EXE_NAME__); -static std::string g_remoteAddress = std::string("127.0.0.1"); -static uint32_t g_remotePort = REST_API_DEFAULT_PORT; -static std::string g_remotePassword = std::string(); -static bool g_debug = false; - -// --------------------------------------------------------------------------- -// Global Functions -// --------------------------------------------------------------------------- - -void fatal(const char* message) -{ - ::fprintf(stderr, "%s: %s\n", g_progExe.c_str(), message); - exit(EXIT_FAILURE); -} - -void usage(const char* message, const char* arg) -{ - ::fprintf(stdout, __PROG_NAME__ " %s (built %s)\r\n", __VER__, __BUILD__); - ::fprintf(stdout, "Copyright (c) 2017-2023 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors.\n"); - ::fprintf(stdout, "Portions Copyright (c) 2015-2021 by Jonathan Naylor, G4KLX and others\n\n"); - if (message != nullptr) { - ::fprintf(stderr, "%s: ", g_progExe.c_str()); - ::fprintf(stderr, message, arg); - ::fprintf(stderr, "\n\n"); - } - - ::fprintf(stdout, "usage: %s [-dvh] [-a
] [-p ] [-P ] \n\n" - " -a remote modem command address\n" - " -p remote modem command port\n" - " -P remote modem authentication password\n" - "\n" - " -d enable debug\n" - " -v show version information\n" - " -h show this screen\n" - " -- stop handling options\n", - g_progExe.c_str()); - exit(EXIT_FAILURE); -} - -int checkArgs(int argc, char* argv[]) -{ - int i, p = 0; - - // iterate through arguments - for (i = 1; i <= argc; i++) - { - if (argv[i] == nullptr) { - break; - } - - if (*argv[i] != '-') { - continue; - } - else if (IS("--")) { - ++p; - break; - } - else if (IS("-a")) { - if ((argc - 1) <= 0) - usage("error: %s", "must specify the address to connect to"); - g_remoteAddress = std::string(argv[++i]); - - if (g_remoteAddress == "") - usage("error: %s", "remote address cannot be blank!"); - - p += 2; - } - else if (IS("-p")) { - if ((argc - 1) <= 0) - usage("error: %s", "must specify the port to connect to"); - g_remotePort = (uint32_t)::atoi(argv[++i]); - - if (g_remotePort == 0) - usage("error: %s", "remote port number cannot be blank or 0!"); - - p += 2; - } - else if (IS("-P")) { - if ((argc - 1) <= 0) - usage("error: %s", "must specify the auth password"); - g_remotePassword = std::string(argv[++i]); - - if (g_remotePassword == "") - usage("error: %s", "remote auth password cannot be blank!"); - - p += 2; - } - else if (IS("-d")) { - ++p; - g_debug = true; - } - else if (IS("-v")) { - ::fprintf(stdout, __PROG_NAME__ " %s (built %s)\r\n", __VER__, __BUILD__); - ::fprintf(stdout, "Copyright (c) 2017-2023 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors.\r\n"); - ::fprintf(stdout, "Portions Copyright (c) 2015-2021 by Jonathan Naylor, G4KLX and others\r\n"); - if (argc == 2) - exit(EXIT_SUCCESS); - } - else if (IS("-h")) { - usage(nullptr, nullptr); - if (argc == 2) - exit(EXIT_SUCCESS); - } - else { - usage("unrecognized option `%s'", argv[i]); - } - } - - if (p < 0 || p > argc) { - p = 0; - } - - return ++p; -} - -// --------------------------------------------------------------------------- -// Program Entry Point -// --------------------------------------------------------------------------- - -int main(int argc, char** argv) -{ - if (argv[0] != nullptr && *argv[0] != 0) - g_progExe = std::string(argv[0]); - - if (argc < 2) { - usage("error: %s", "must specify the remote command!"); - return ERRNO_REMOTE_CMD; - } - - if (argc > 1) { - // check arguments - int i = checkArgs(argc, argv); - if (i < argc) { - argc -= i; - argv += i; - } - else { - argc--; - argv++; - } - } - - // process command - std::string cmd = std::string(argv[0]); - for (int i = 1; i < argc; i++) { - cmd += " "; - cmd += std::string(argv[i]); - } - - // initialize system logging - bool ret = ::LogInitialise("", "", 0U, 1U, true); - if (!ret) { - ::fprintf(stderr, "unable to open the log file\n"); - return 1; - } - - RemoteCommand* command = new RemoteCommand(g_remoteAddress, g_remotePort, g_remotePassword, g_debug); - int retCode = command->send(cmd); - - ::LogFinalise(); - return retCode; -}