implement the majority of working REST API support (this commit deprecates RCON completely, RCON will no longer function with any build beyond this);

pull/19/head
Bryan Biedenkapp 3 years ago
parent df028fd2f8
commit 35298fe94b

@ -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"

@ -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<int>(state);
req["dstId"].set<uint32_t>(dstId);
req["slot"].set<uint8_t>(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<int>(state);
req["dstId"].set<uint32_t>(dstId);
req["slot"].set<uint8_t>(slot);
RESTClient::send(voiceChData.address(), voiceChData.port(), voiceChData.password(),
HTTP_PUT, PUT_PERMIT_TG, req, m_tscc->m_debug);
}
}

@ -41,10 +41,18 @@
#include <string>
#include <functional>
// ---------------------------------------------------------------------------
// 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)

@ -55,7 +55,7 @@ using namespace modem;
#include <stdexcept>
// ---------------------------------------------------------------------------
// 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<typename ... FormatArgs>
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_t>(size_s);
std::unique_ptr<char[]> buf(new char[ size ]);
std::snprintf(buf.get(), size, format.c_str(), args ...);
return std::string(buf.get(), buf.get() + size - 1);
}
/// <summary>
///
/// </summary>
@ -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));
}
/// <summary>
@ -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<bool>()) {
errorPayload(reply, "force was not a valid value");
return;
}
bool force = req["force"].get<bool>();
if (!force) {
g_killed = true;
} else {
bool force = req["force"].get<bool>();
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<int>()) {
errorPayload(reply, "unit-to-unit was not a valid integer");
if (!req["unitToUnit"].is<bool>()) {
errorPayload(reply, "unit-to-unit was not a valid boolean");
return;
}
uint8_t unitToUnit = (uint8_t)req["unitToUnit"].get<int>();
if (unitToUnit > 1U) {
errorPayload(reply, "unit-to-unit must be a 0 or 1");
return;
}
//bool unitToUnit = req["unitToUnit"].get<bool>();
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)
}
/// <summary>
@ -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)
}
/// <summary>
@ -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
*/
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="reply"></param>
/// <param name="match"></param>
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)
}
/// <summary>
///
/// </summary>
@ -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)
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="reply"></param>
/// <param name="match"></param>
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)
}

@ -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 <string>
#include <random>
// ---------------------------------------------------------------------------
// 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
*/
/// <summary></summary>
/// <summary></summary>
void restAPI_GetNXDNCC(const HTTPPayload& request, HTTPPayload& reply, const network::rest::RequestMatch& match);
/// <summary></summary>
void restAPI_GetNXDNDebug(const HTTPPayload& request, HTTPPayload& reply, const network::rest::RequestMatch& match);
/// <summary></summary>
void restAPI_GetNXDNDumpRCCH(const HTTPPayload& request, HTTPPayload& reply, const network::rest::RequestMatch& match);
/// <summary></summary>
void restAPI_GetNXDNCCEnable(const HTTPPayload& request, HTTPPayload& reply, const network::rest::RequestMatch& match);
};
#endif // __REST_API_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__

@ -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 <typename Iter> 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<ctype>() 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<double>() 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<value *>(this)->type_ = int32_type, (const_cast<value *>(this)->u_.int32_ = u_.number_)),
u_.int32_))
GET(uint32_t,
(type_ == number_type && (const_cast<value *>(this)->type_ = uint32_type, (const_cast<value *>(this)->u_.uint32_ = u_.number_)),
u_.uint32_))
GET(uint16_t,
(type_ == number_type && (const_cast<value *>(this)->type_ = uint16_type, (const_cast<value *>(this)->u_.uint16_ = u_.number_)),
u_.uint16_))
GET(uint8_t,
(type_ == number_type && (const_cast<value *>(this)->type_ = uint8_type, (const_cast<value *>(this)->u_.uint8_ = u_.number_)),
u_.uint8_))
GET(float,
(type_ == number_type && (const_cast<value *>(this)->type_ = float_type, (const_cast<value *>(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 <typename Iter> void copy(const std::string &s, Iter oi) {
std::copy(s.begin(), s.end(), oi);
}

@ -190,6 +190,33 @@ namespace network
// This class implements a generic debug request dispatcher.
// ---------------------------------------------------------------------------
template<typename Request = http::HTTPPayload, typename Reply = http::HTTPPayload>
class BasicRequestDispatcher {
public:
typedef std::function<void(const Request&, Reply&)> RequestHandlerType;
/// <summary>Initializes a new instance of the DebugRequestDispatcher class.</summary>
BasicRequestDispatcher() { /* stub */ }
/// <summary>Initializes a new instance of the BasicRequestDispatcher class.</summary>
BasicRequestDispatcher(RequestHandlerType handler) : m_handler(handler) { /* stub */ }
/// <summary></summary>
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<typename Request = http::HTTPPayload, typename Reply = http::HTTPPayload>
class DebugRequestDispatcher {
public:

@ -40,6 +40,7 @@
#include "Defines.h"
#include "network/rest/http/HTTPLexer.h"
#include "network/rest/http/HTTPPayload.h"
#include "Utils.h"
#include <array>
#include <memory>
@ -93,12 +94,21 @@ namespace network
/// <summary>Start the first asynchronous operation for the connection.</summary>
void start() { read(); }
/// <summary>Stop all asynchronous operations associated with the connection.</summary>
void stop() { m_socket.close(); }
void stop()
{
try
{
if (m_socket.is_open()) {
m_socket.close();
}
}
catch(const std::exception&) { /* ignore */ }
}
/// <summary>Perform an asynchronous write operation.</summary>
/// <summary>Perform an synchronous write operation.</summary>
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
});
}
/// <summary>Perform an asynchronous write operation.</summary>
/// <summary>Perform an synchronous write operation.</summary>
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) {
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;

@ -103,12 +103,16 @@ namespace network
/// <summary>Opens connection to the network.</summary>
bool open()
{
m_running = true;
return run();
}
/// <summary>Closes connection to the network.</summary>
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);
}
}
/// <summary>Perform an asynchronous connect operation.</summary>
void connect(asio::ip::basic_resolver_results<asio::ip::tcp>& 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<ConnectionType>(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<ConnectionTypePtr> m_connectionManager;

@ -351,10 +351,10 @@ std::vector<asio::const_buffer> HTTPPayload::toBuffers()
/// </summary>
/// <param name="obj"></param>
/// <param name="s"></param>
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)
/// <param name="c"></param>
/// <param name="s"></param>
/// <param name="contentType"></param>
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
/// </summary>
/// <param name="method"></param>
/// <param name="uri"></param>
/// <param name="contentType"></param>
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
/// </summary>
/// <param name="status"></param>
/// <param name="contentType"></param>
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
/// <summary>
///
/// </summary>
/// <param name="localEndpoint"></param>
void HTTPPayload::attachHostHeader(const asio::ip::tcp::endpoint localEndpoint)
/// <param name="remoteEndpoint"></param>
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());
}

@ -110,17 +110,17 @@ namespace network
std::vector<asio::const_buffer> toBuffers();
/// <summary>Prepares payload for transmission by finalizing status and content type.</summary>
void payload(json::object obj, StatusType status = OK);
void payload(json::object& obj, StatusType status = OK);
/// <summary>Prepares payload for transmission by finalizing status and content type.</summary>
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");
/// <summary>Get a request payload.</summary>
static HTTPPayload requestPayload(std::string method, std::string uri, const std::string contentType = "text/html");
static HTTPPayload requestPayload(std::string method, std::string uri);
/// <summary>Get a status payload.</summary>
static HTTPPayload statusPayload(StatusType status, const std::string contentType = "text/html");
static HTTPPayload statusPayload(StatusType status, const std::string& contentType = "text/html");
/// <summary></summary>
void attachHostHeader(const asio::ip::tcp::endpoint localEndpoint);
void attachHostHeader(const asio::ip::tcp::endpoint remoteEndpoint);
private:
/// <summary></summary>
void ensureDefaultHeaders(const std::string& contentType = "text/html");

@ -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<int>(state);
req["dstId"].set<uint32_t>(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);
}
}

@ -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<int>(state);
req["dstId"].set<uint32_t>(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<int>(state);
req["dstId"].set<uint32_t>(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);
}
}

@ -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 <gatekeep@gmail.com>
*
* 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 <cstdio>
#include <cassert>
#include <cstdlib>
#include <cstring>
#include <iomanip>
#include <sstream>
// ---------------------------------------------------------------------------
// 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
// ---------------------------------------------------------------------------
/// <summary>
///
/// </summary>
/// <param name="response"></param>
/// <param name="obj"></param>
/// <returns></returns>
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<json::object>()) {
return false;
}
obj = v.get<json::object>();
return true;
}
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/// <summary>
/// Initializes a new instance of the RESTClient class.
/// </summary>
/// <param name="address">Network Hostname/IP address to connect to.</param>
/// <param name="port">Network port number.</param>
/// <param name="password">Authentication password.</param>
/// <param name="debug">Flag indicating whether debug is enabled.</param>
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;
}
/// <summary>
/// Finalizes a instance of the RESTClient class.
/// </summary>
RESTClient::~RESTClient()
{
/* stub */
}
/// <summary>
/// Sends remote control command to the specified modem.
/// </summary>
/// <param name="method">REST API method.</param>
/// <param name="endpoint">REST API endpoint.</param>
/// <param name="payload">REST API endpoint payload.</param>
/// <returns>EXIT_SUCCESS, if command was sent, otherwise EXIT_FAILURE.</returns>
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);
}
/// <summary>
/// Sends remote control command to the specified modem.
/// </summary>
/// <param name="address">Network Hostname/IP address to connect to.</param>
/// <param name="port">Network port number.</param>
/// <param name="password">Authentication password.</param>
/// <param name="method">REST API method.</param>
/// <param name="endpoint">REST API endpoint.</param>
/// <param name="payload">REST API endpoint payload.</param>
/// <param name="debug">Flag indicating whether debug is enabled.</param>
/// <returns>EXIT_SUCCESS, if command was sent, otherwise EXIT_FAILURE.</returns>
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<network::rest::http::HTTPPayload, network::rest::http::HTTPPayload> RESTDispatcherType;
RESTDispatcherType m_dispatcher(RESTClient::responseHandler);
HTTPClient<RESTDispatcherType> 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<std::string>(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<int>();
if (status == HTTPPayload::StatusType::OK) {
token = rsp["token"].get<std::string>();
}
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
// ---------------------------------------------------------------------------
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="reply"></param>
void RESTClient::responseHandler(const HTTPPayload& request, HTTPPayload& reply)
{
m_responseAvailable = true;
m_response = request;
}
/// <summary>
///
/// </summary>
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;
}

@ -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 <gatekeep@gmail.com>
* Copyright (C) 2023 by Bryan Biedenkapp <gatekeep@gmail.com>
*
* 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 <string>
// ---------------------------------------------------------------------------
// 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:
/// <summary>Initializes a new instance of the RemoteCommand class.</summary>
RemoteCommand(const std::string& address, uint32_t port, const std::string& password, bool debug);
/// <summary>Finalizes a instance of the RemoteCommand class.</summary>
~RemoteCommand();
/// <summary>Initializes a new instance of the RESTClient class.</summary>
RESTClient(const std::string& address, uint32_t port, const std::string& password, bool debug);
/// <summary>Finalizes a instance of the RESTClient class.</summary>
~RESTClient();
/// <summary>Sends remote control command to the specified modem.</summary>
int send(const std::string& command);
int send(const std::string method, const std::string endpoint, json::object payload);
/// <summary>Sends remote control command to the specified modem.</summary>
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;
/// <summary></summary>
static void responseHandler(const HTTPPayload& request, HTTPPayload& reply);
/// <summary></summary>
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__

@ -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 <gatekeep@gmail.com>
*
* 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 <cstdio>
#include <cassert>
#include <cstdlib>
#include <cstring>
#include <vector>
// ---------------------------------------------------------------------------
// 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 <address>] [-p <port>] [-P <password>] <command>\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;
}
/// <summary></summary>
std::string getArgString(std::vector<std::string> args, uint32_t n)
{
n += 1;
if (n >= args.size())
return "";
return args.at(n);
}
/// <summary></summary>
uint64_t getArgUInt64(std::vector<std::string> args, uint32_t n) { return (uint64_t)::atol(getArgString(args, n).c_str()); }
/// <summary></summary>
uint32_t getArgUInt32(std::vector<std::string> args, uint32_t n) { return (uint32_t)::atoi(getArgString(args, n).c_str()); }
/// <summary></summary>
int32_t getArgInt32(std::vector<std::string> args, uint32_t n) { return ::atoi(getArgString(args, n).c_str()); }
/// <summary></summary>
uint16_t getArgUInt16(std::vector<std::string> args, uint32_t n) { return (uint16_t)::atoi(getArgString(args, n).c_str()); }
/// <summary></summary>
int16_t getArgInt16(std::vector<std::string> args, uint32_t n) { return (int16_t)::atoi(getArgString(args, n).c_str()); }
/// <summary></summary>
uint8_t getArgUInt8(std::vector<std::string> args, uint32_t n) { return (uint8_t)::atoi(getArgString(args, n).c_str()); }
/// <summary></summary>
int8_t getArgInt8(std::vector<std::string> args, uint32_t n) { return (int8_t)::atoi(getArgString(args, n).c_str()); }
/// <summary>
/// Helper to print the remote control help.
/// </summary>
/// <returns></returns>
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 <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 <state> <dstid> Causes the host to permit the specified destination ID if non-authoritative\r\n";
reply += " grant-tg <state> <dstid> <uu> Causes the host to grant the specified destination ID if non-authoritative\r\n";
reply += "\r\n";
reply += " rid-whitelist <rid> Whitelists the specified RID in the host ACL tables\r\n";
reply += " rid-blacklist <rid> 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 <debug 0/1> <verbose 0/1>\r\n";
reply += " dmr-dump-csbk <0/1>\r\n";
reply += " p25-debug <debug 0/1> <verbose 0/1>\r\n";
reply += " p25-dump-tsbk <0/1>\r\n";
reply += " nxdn-debug <debug 0/1> <verbose 0/1>\r\n";
reply += " nxdn-dump-rcch <0/1>\r\n";
reply += "\r\nDMR Commands:\r\n";
reply += " dmr-rid-page <rid> Pages/Calls the specified RID\r\n";
reply += " dmr-rid-check <rid> Radio Checks the specified RID\r\n";
reply += " dmr-rid-inhibit <rid> Inhibits the specified RID\r\n";
reply += " dmr-rid-uninhibit <rid> 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 <mfid> Sets the P25 MFId for the next sent P25 command\r\n";
reply += " p25-rid-page <rid> Pages/Calls the specified RID\r\n";
reply += " p25-rid-check <rid> Radio Checks the specified RID\r\n";
reply += " p25-rid-inhibit <rid> Inhibits the specified RID\r\n";
reply += " p25-rid-uninhibit <rid> Uninhibits the specified RID\r\n";
reply += " p25-rid-gaq <rid> Group affiliation queries the specified RID\r\n";
reply += " p25-rid-ureg <rid> 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<std::string> args = std::vector<std::string>();
args.clear();
// parse the original command into a vector of strings.
char* b = const_cast<char*>(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<std::string>(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<bool>(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<bool>(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<int>(state);
uint32_t dstId = getArgInt32(args, 1U);
req["dstId"].set<uint32_t>(dstId);
if (state == 1) {
uint8_t slot = getArgInt8(args, 2U);
req["slot"].set<uint8_t>(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<int>(state);
uint32_t dstId = getArgInt32(args, 1U);
req["dstId"].set<uint32_t>(dstId);
uint8_t unitToUnit = getArgInt32(args, 2U);
if (unitToUnit == 0U) {
bool b = false;
req["unitToUnit"].set<bool>(b);
} else {
bool b = true;
req["unitToUnit"].set<bool>(b);
}
if (state == 1) {
uint8_t slot = getArgInt8(args, 3U);
req["slot"].set<uint8_t>(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>(std::string(RID_CMD_PAGE));
uint8_t slotNo = getArgInt8(args, 0U);
req["slot"].set<uint8_t>(slotNo);
uint32_t dstId = getArgUInt32(args, 1U);
req["dstId"].set<uint32_t>(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>(std::string(RID_CMD_CHECK));
uint8_t slotNo = getArgInt8(args, 0U);
req["slot"].set<uint8_t>(slotNo);
uint32_t dstId = getArgUInt32(args, 1U);
req["dstId"].set<uint32_t>(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>(std::string(RID_CMD_INHIBIT));
uint8_t slotNo = getArgInt8(args, 0U);
req["slot"].set<uint8_t>(slotNo);
uint32_t dstId = getArgUInt32(args, 1U);
req["dstId"].set<uint32_t>(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>(std::string(RID_CMD_UNINHIBIT));
uint8_t slotNo = getArgInt8(args, 0U);
req["slot"].set<uint8_t>(slotNo);
uint32_t dstId = getArgUInt32(args, 1U);
req["dstId"].set<uint32_t>(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>(std::string(RID_CMD_PAGE));
uint32_t dstId = getArgUInt32(args, 0U);
req["dstId"].set<uint32_t>(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>(std::string(RID_CMD_CHECK));
uint32_t dstId = getArgUInt32(args, 0U);
req["dstId"].set<uint32_t>(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>(std::string(RID_CMD_INHIBIT));
uint32_t dstId = getArgUInt32(args, 0U);
req["dstId"].set<uint32_t>(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>(std::string(RID_CMD_UNINHIBIT));
uint32_t dstId = getArgUInt32(args, 0U);
req["dstId"].set<uint32_t>(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>(std::string(RID_CMD_GAQ));
uint32_t dstId = getArgUInt32(args, 0U);
req["dstId"].set<uint32_t>(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>(std::string(RID_CMD_UREG));
uint32_t dstId = getArgUInt32(args, 0U);
req["dstId"].set<uint32_t>(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;
}

@ -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 <gatekeep@gmail.com>
*
* 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 <cstdio>
#include <cassert>
#include <cstdlib>
#include <cstring>
// ---------------------------------------------------------------------------
// 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
// ---------------------------------------------------------------------------
/// <summary>
/// Initializes a new instance of the RemoteCommand class.
/// </summary>
/// <param name="address">Network Hostname/IP address to connect to.</param>
/// <param name="port">Network port number.</param>
/// <param name="password">Authentication password.</param>
/// <param name="debug">Flag indicating whether debug is enabled.</param>
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);
}
/// <summary>
/// Finalizes a instance of the RemoteCommand class.
/// </summary>
RemoteCommand::~RemoteCommand()
{
/* stub */
}
/// <summary>
/// Sends remote control command to the specified modem.
/// </summary>
/// <param name="command">Command string to send to remote modem.</param>
/// <returns>EXIT_SUCCESS, if command was sent, otherwise EXIT_FAILURE.</returns>
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);
}
/// <summary>
/// Sends remote control command to the specified modem.
/// </summary>
/// <param name="address">Network Hostname/IP address to connect to.</param>
/// <param name="port">Network port number.</param>
/// <param name="password">Authentication password.</param>
/// <param name="command">Command string to send to remote modem.</param>
/// <param name="debug">Flag indicating whether debug is enabled.</param>
/// <returns>EXIT_SUCCESS, if command was sent, otherwise EXIT_FAILURE.</returns>
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;
}

@ -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 <gatekeep@gmail.com>
*
* 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 <cstdio>
#include <cassert>
#include <cstdlib>
#include <cstring>
// ---------------------------------------------------------------------------
// 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 <address>] [-p <port>] [-P <password>] <command>\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;
}
Loading…
Cancel
Save

Powered by TurnKey Linux.