/** * 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) 2023 by Bryan Biedenkapp N2PLL * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ #include "Defines.h" #include "edac/SHA256.h" #include "dmr/Control.h" #include "p25/Control.h" #include "nxdn/Control.h" #include "modem/Modem.h" #include "host/Host.h" #include "network/json/json.h" #include "RESTAPI.h" #include "HostMain.h" #include "Log.h" #include "Thread.h" #include "Utils.h" using namespace network; using namespace network::rest; using namespace network::rest::http; using namespace modem; #include #include #include #include #include #include // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- #define REST_API_BIND(funcAddr, classInstance) std::bind(&funcAddr, classInstance, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3) // --------------------------------------------------------------------------- // Global Functions // --------------------------------------------------------------------------- /// /// /// /// void setResponseDefaultStatus(json::object& obj) { int s = (int)HTTPPayload::OK; obj["status"].set(s); } /// /// /// /// /// /// void errorPayload(HTTPPayload& reply, std::string message, HTTPPayload::StatusType status = HTTPPayload::BAD_REQUEST) { HTTPPayload rep; rep.status = status; json::object response = json::object(); int s = (int)rep.status; response["status"].set(s); response["message"].set(message); reply.payload(response); } /// /// /// /// /// /// /// bool parseRequestBody(const HTTPPayload& request, HTTPPayload& reply, json::object& obj) { std::string contentType = request.headers.find("Content-Type"); if (contentType != "application/json") { reply = HTTPPayload::statusPayload(HTTPPayload::BAD_REQUEST, "application/json"); return false; } // parse JSON body json::value v; std::string err = json::parse(v, request.content); if (!err.empty()) { errorPayload(reply, err); return false; } // ensure parsed JSON is an object if (!v.is()) { errorPayload(reply, "Request was not a valid JSON object."); return false; } obj = v.get(); return true; } // --------------------------------------------------------------------------- // Public Class Members // --------------------------------------------------------------------------- /// /// Initializes a new instance of the RESTAPI class. /// /// Network Hostname/IP address to connect to. /// Network port number. /// Authentication password. /// Instance of the Host class. /// RESTAPI::RESTAPI(const std::string& address, uint16_t port, const std::string& password, Host* host, bool debug) : m_dispatcher(debug), m_restServer(address, port), m_random(), m_p25MFId(p25::P25_MFG_STANDARD), m_password(password), m_passwordHash(nullptr), m_debug(debug), m_host(host), m_dmr(nullptr), m_p25(nullptr), m_nxdn(nullptr), m_ridLookup(nullptr), m_tidLookup(nullptr), m_authTokens() { assert(!address.empty()); assert(port > 0U); assert(!password.empty()); size_t size = password.size(); uint8_t* in = new uint8_t[size]; for (size_t i = 0U; i < size; i++) in[i] = password.at(i); m_passwordHash = new uint8_t[32U]; ::memset(m_passwordHash, 0x00U, 32U); edac::SHA256 sha256; sha256.buffer(in, (uint32_t)(size), m_passwordHash); if (m_debug) { Utils::dump("REST Password Hash", m_passwordHash, 32U); } std::random_device rd; std::mt19937 mt(rd()); m_random = mt; } /// /// Finalizes a instance of the RESTAPI class. /// RESTAPI::~RESTAPI() { /* stub */ } /// /// Sets the instances of the Radio ID and Talkgroup ID lookup tables. /// /// Radio ID Lookup Table Instance /// Talkgroup ID Lookup Table Instance void RESTAPI::setLookups(lookups::RadioIdLookup* ridLookup, lookups::TalkgroupIdLookup* tidLookup) { m_ridLookup = ridLookup; m_tidLookup = tidLookup; } /// /// Sets the instances of the digital radio protocols. /// /// Instance of the DMR Control class. /// Instance of the P25 Control class. /// Instance of the NXDN Control class. void RESTAPI::setProtocols(dmr::Control* dmr, p25::Control* p25, nxdn::Control* nxdn) { m_dmr = dmr; m_p25 = p25; m_nxdn = nxdn; } /// /// Opens connection to the network. /// /// bool RESTAPI::open() { initializeEndpoints(); m_restServer.setHandler(m_dispatcher); return run(); } /// /// Closes connection to the network. /// void RESTAPI::close() { m_restServer.stop(); wait(); } // --------------------------------------------------------------------------- // Private Class Members // --------------------------------------------------------------------------- /// /// /// void RESTAPI::entry() { m_restServer.run(); } /// /// Helper to initialize REST API endpoints. /// void RESTAPI::initializeEndpoints() { m_dispatcher.match(PUT_AUTHENTICATE).put(REST_API_BIND(RESTAPI::restAPI_PutAuth, this)); m_dispatcher.match(GET_VERSION).get(REST_API_BIND(RESTAPI::restAPI_GetVersion, this)); m_dispatcher.match(GET_STATUS).get(REST_API_BIND(RESTAPI::restAPI_GetStatus, this)); m_dispatcher.match(GET_VOICE_CH).get(REST_API_BIND(RESTAPI::restAPI_GetVoiceCh, this)); m_dispatcher.match(PUT_MDM_MODE).put(REST_API_BIND(RESTAPI::restAPI_PutModemMode, this)); m_dispatcher.match(PUT_MDM_KILL).get(REST_API_BIND(RESTAPI::restAPI_PutModemKill, this)); m_dispatcher.match(PUT_PERMIT_TG).get(REST_API_BIND(RESTAPI::restAPI_PutPermitTG, this)); m_dispatcher.match(PUT_GRANT_TG).get(REST_API_BIND(RESTAPI::restAPI_PutGrantTG, this)); m_dispatcher.match(GET_RELEASE_GRNTS).get(REST_API_BIND(RESTAPI::restAPI_GetReleaseGrants, this)); m_dispatcher.match(GET_RELEASE_AFFS).get(REST_API_BIND(RESTAPI::restAPI_GetReleaseAffs, this)); m_dispatcher.match(GET_RID_WHITELIST, true).get(REST_API_BIND(RESTAPI::restAPI_GetRIDWhitelist, this)); m_dispatcher.match(GET_RID_BLACKLIST, true).get(REST_API_BIND(RESTAPI::restAPI_GetRIDBlacklist, this)); /* ** Digital Mobile Radio */ m_dispatcher.match(GET_DMR_BEACON).get(REST_API_BIND(RESTAPI::restAPI_GetDMRBeacon, this)); m_dispatcher.match(GET_DMR_DEBUG, true).get(REST_API_BIND(RESTAPI::restAPI_GetDMRDebug, this)); m_dispatcher.match(GET_DMR_DUMP_CSBK, true).get(REST_API_BIND(RESTAPI::restAPI_GetDMRDumpCSBK, this)); m_dispatcher.match(PUT_DMR_RID).get(REST_API_BIND(RESTAPI::restAPI_PutDMRRID, this)); m_dispatcher.match(GET_DMR_CC_DEDICATED, true).get(REST_API_BIND(RESTAPI::restAPI_GetDMRCCEnable, this)); m_dispatcher.match(GET_DMR_CC_BCAST, true).get(REST_API_BIND(RESTAPI::restAPI_GetDMRCCBroadcast, this)); /* ** Project 25 */ m_dispatcher.match(GET_P25_CC).get(REST_API_BIND(RESTAPI::restAPI_GetP25CC, this)); m_dispatcher.match(GET_P25_DEBUG, true).get(REST_API_BIND(RESTAPI::restAPI_GetP25Debug, this)); m_dispatcher.match(GET_P25_DUMP_TSBK, true).get(REST_API_BIND(RESTAPI::restAPI_GetP25DumpTSBK, this)); m_dispatcher.match(PUT_P25_RID).get(REST_API_BIND(RESTAPI::restAPI_PutP25RID, this)); m_dispatcher.match(GET_P25_CC_DEDICATED, true).get(REST_API_BIND(RESTAPI::restAPI_GetP25CCEnable, this)); m_dispatcher.match(GET_P25_CC_BCAST, true).get(REST_API_BIND(RESTAPI::restAPI_GetP25CCBroadcast, this)); /* ** Next Generation Digital Narrowband */ m_dispatcher.match(GET_NXDN_DEBUG, true).get(REST_API_BIND(RESTAPI::restAPI_GetNXDNDebug, this)); m_dispatcher.match(GET_NXDN_DUMP_RCCH, true).get(REST_API_BIND(RESTAPI::restAPI_GetNXDNDumpRCCH, this)); } /// /// /// /// void RESTAPI::invalidateHostToken(const std::string host) { auto token = std::find_if(m_authTokens.begin(), m_authTokens.end(), [&](const AuthTokenValueType& tok) { return tok.first == host; }); if (token != m_authTokens.end()) { m_authTokens.erase(host); } } /// /// /// /// bool RESTAPI::validateAuth(const HTTPPayload& request, HTTPPayload& reply) { std::string host = request.headers.find("Host"); std::string headerToken = request.headers.find("X-DVM-Auth-Token"); if (headerToken == "") { errorPayload(reply, "invalid authentication token", HTTPPayload::UNAUTHORIZED); return false; } auto token = std::find_if(m_authTokens.begin(), m_authTokens.end(), [&](const AuthTokenValueType& tok) { return tok.first == host; }); if (token != m_authTokens.end()) { uint32_t storedToken = token->second; uint32_t passedToken = (uint32_t)::strtoul(headerToken.c_str(), NULL, 10); if (storedToken == passedToken) { return true; } else { m_authTokens.erase(host); // devalidate host errorPayload(reply, "invalid authentication token", HTTPPayload::UNAUTHORIZED); return false; } } else { errorPayload(reply, "invalid authentication token", HTTPPayload::UNAUTHORIZED); return false; } return false; } /// /// /// /// /// /// void RESTAPI::restAPI_PutAuth(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { std::string host = request.headers.find("Host"); json::object response = json::object(); setResponseDefaultStatus(response); json::object req = json::object(); if (!parseRequestBody(request, reply, req)) { return; } // validate auth is a string within the JSON blob if (!req["auth"].is()) { invalidateHostToken(host); errorPayload(reply, "password was not a valid string"); return; } std::string auth = req["auth"].get(); if (auth.empty()) { invalidateHostToken(host); errorPayload(reply, "auth cannot be empty"); return; } if (auth.size() > 64) { invalidateHostToken(host); errorPayload(reply, "auth cannot be longer than 64 characters"); return; } if (!(auth.find_first_not_of("0123456789abcdefABCDEF", 2) == std::string::npos)) { invalidateHostToken(host); errorPayload(reply, "auth contains invalid characters"); return; } if (m_debug) { ::LogDebug(LOG_REST, "/auth auth = %s", auth.c_str()); } const char* authPtr = auth.c_str(); uint8_t* passwordHash = new uint8_t[32U]; ::memset(passwordHash, 0x00U, 32U); for (uint8_t i = 0; i < 32U; i++) { char t[4] = {authPtr[0], authPtr[1], 0}; passwordHash[i] = (uint8_t)::strtoul(t, NULL, 16); authPtr += 2 * sizeof(char); } if (m_debug) { Utils::dump("Password Hash", passwordHash, 32U); } // compare hashes if (::memcmp(m_passwordHash, passwordHash, 32U) != 0) { invalidateHostToken(host); errorPayload(reply, "invalid password"); return; } delete[] passwordHash; invalidateHostToken(host); std::uniform_int_distribution dist(DVM_RAND_MIN, DVM_REST_RAND_MAX); uint64_t salt = dist(m_random); m_authTokens[host] = salt; response["token"].set(std::to_string(salt)); reply.payload(response); } /// /// /// /// /// /// void RESTAPI::restAPI_GetVersion(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { if (!validateAuth(request, reply)) { return; } json::object response = json::object(); setResponseDefaultStatus(response); response["version"].set(std::string((__PROG_NAME__ " " __VER__ " (" DESCR_DMR DESCR_P25 DESCR_NXDN "CW Id, Network) (built " __BUILD__ ")"))); reply.payload(response); } /// /// /// /// /// /// void RESTAPI::restAPI_GetStatus(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { if (!validateAuth(request, reply)) { return; } json::object response = json::object(); setResponseDefaultStatus(response); yaml::Node systemConf = m_host->m_conf["system"]; { yaml::Node modemConfig = m_host->m_conf["system"]["modem"]; std::string portType = modemConfig["protocol"]["type"].as(); response["portType"].set(portType); yaml::Node uartConfig = modemConfig["protocol"]["uart"]; std::string modemPort = uartConfig["port"].as(); response["modemPort"].set(modemPort); uint32_t portSpeed = uartConfig["speed"].as(115200U); response["portSpeed"].set(portSpeed); response["state"].set(m_host->m_state); bool dmrEnabled = m_dmr != nullptr; response["dmrEnabled"].set(dmrEnabled); bool p25Enabled = m_p25 != nullptr; response["p25Enabled"].set(p25Enabled); bool nxdnEnabled = m_nxdn != nullptr; response["nxdnEnabled"].set(nxdnEnabled); uint8_t protoVer = m_host->m_modem->getVersion(); response["protoVer"].set(protoVer); response["dmrCC"].set(m_host->m_dmrCtrlChannel); response["p25CC"].set(m_host->m_p25CtrlChannel); response["nxdnCC"].set(m_host->m_nxdnCtrlChannel); } { json::object modemInfo = json::object(); if (!m_host->m_modem->isHotspot()) { modemInfo["pttInvert"].set(m_host->m_modem->m_pttInvert); modemInfo["rxInvert"].set(m_host->m_modem->m_rxInvert); modemInfo["txInvert"].set(m_host->m_modem->m_txInvert); modemInfo["dcBlocker"].set(m_host->m_modem->m_dcBlocker); } modemInfo["rxLevel"].set(m_host->m_modem->m_rxLevel); modemInfo["cwTxLevel"].set(m_host->m_modem->m_cwIdTXLevel); modemInfo["dmrTxLevel"].set(m_host->m_modem->m_dmrTXLevel); modemInfo["p25TxLevel"].set(m_host->m_modem->m_p25TXLevel); modemInfo["nxdnTxLevel"].set(m_host->m_modem->m_nxdnTXLevel); modemInfo["rxDCOffset"].set(m_host->m_modem->m_rxDCOffset); modemInfo["txDCOffset"].set(m_host->m_modem->m_txDCOffset); if (!m_host->m_modem->isHotspot()) { modemInfo["dmrSymLevel3Adj"].set(m_host->m_modem->m_dmrSymLevel3Adj); modemInfo["dmrSymLevel1Adj"].set(m_host->m_modem->m_dmrSymLevel1Adj); modemInfo["p25SymLevel3Adj"].set(m_host->m_modem->m_p25SymLevel3Adj); modemInfo["p25SymLevel1Adj"].set(m_host->m_modem->m_p25SymLevel1Adj); // are we on a protocol version 3 firmware? if (m_host->m_modem->getVersion() >= 3U) { modemInfo["nxdnSymLevel3Adj"].set(m_host->m_modem->m_nxdnSymLevel3Adj); modemInfo["nxdnSymLevel1Adj"].set(m_host->m_modem->m_nxdnSymLevel1Adj); } } if (m_host->m_modem->isHotspot()) { modemInfo["dmrDiscBW"].set(m_host->m_modem->m_dmrDiscBWAdj); modemInfo["dmrPostBW"].set(m_host->m_modem->m_dmrPostBWAdj); modemInfo["p25DiscBW"].set(m_host->m_modem->m_p25DiscBWAdj); modemInfo["p25PostBW"].set(m_host->m_modem->m_p25PostBWAdj); // are we on a protocol version 3 firmware? if (m_host->m_modem->getVersion() >= 3U) { modemInfo["nxdnDiscBW"].set(m_host->m_modem->m_nxdnDiscBWAdj); modemInfo["nxdnPostBW"].set(m_host->m_modem->m_nxdnPostBWAdj); modemInfo["afcEnabled"].set(m_host->m_modem->m_afcEnable); modemInfo["afcKI"].set(m_host->m_modem->m_afcKI); modemInfo["afcKP"].set(m_host->m_modem->m_afcKP); modemInfo["afcRange"].set(m_host->m_modem->m_afcRange); } switch (m_host->m_modem->m_adfGainMode) { case ADF_GAIN_AUTO_LIN: modemInfo["gainMode"].set("ADF7021 Gain Mode: Auto High Linearity"); break; case ADF_GAIN_LOW: modemInfo["gainMode"].set("ADF7021 Gain Mode: Low"); break; case ADF_GAIN_HIGH: modemInfo["gainMode"].set("ADF7021 Gain Mode: High"); break; case ADF_GAIN_AUTO: default: modemInfo["gainMode"].set("ADF7021 Gain Mode: Auto"); break; } } modemInfo["fdmaPreambles"].set(m_host->m_modem->m_fdmaPreamble); modemInfo["dmrRxDelay"].set(m_host->m_modem->m_dmrRxDelay); modemInfo["p25CorrCount"].set(m_host->m_modem->m_p25CorrCount); modemInfo["rxFrequency"].set(m_host->m_modem->m_rxFrequency); modemInfo["txFrequency"].set(m_host->m_modem->m_txFrequency); modemInfo["rxTuning"].set(m_host->m_modem->m_rxTuning); modemInfo["txTuning"].set(m_host->m_modem->m_txTuning); uint32_t rxFreqEffective = m_host->m_modem->m_rxFrequency + m_host->m_modem->m_rxTuning; modemInfo["rxFrequencyEffective"].set(rxFreqEffective); uint32_t txFreqEffective = m_host->m_modem->m_txFrequency + m_host->m_modem->m_txTuning; modemInfo["txFrequencyEffective"].set(txFreqEffective); response["modem"].set(modemInfo); } reply.payload(response); } /// /// /// /// /// /// void RESTAPI::restAPI_GetVoiceCh(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { if (!validateAuth(request, reply)) { return; } json::object response = json::object(); setResponseDefaultStatus(response); json::array channels = json::array(); if (m_host->m_voiceChData.size() > 0) { for (auto entry : m_host->m_voiceChData) { uint32_t chNo = entry.first; lookups::VoiceChData data = entry.second; json::object channel = json::object(); channel["chNo"].set(chNo); std::string address = data.address(); channel["address"].set(address); uint16_t port = data.port(); channel["port"].set(port); channels.push_back(json::value(channel)); } } response["channels"].set(channels); reply.payload(response); } /// /// /// /// /// /// void RESTAPI::restAPI_PutModemMode(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { if (!validateAuth(request, reply)) { return; } json::object response = json::object(); setResponseDefaultStatus(response); json::object req = json::object(); if (!parseRequestBody(request, reply, req)) { return; } // validate mode is a string within the JSON blob if (!req["mode"].is()) { errorPayload(reply, "password was not a valid string"); return; } std::string mode = req["mode"].get(); if (mode == MODE_OPT_IDLE) { m_host->m_fixedMode = false; m_host->setState(STATE_IDLE); response["message"].set(std::string("Dynamic mode")); uint8_t hostMode = m_host->m_state; response["mode"].set(hostMode); reply.payload(response); } else if (mode == MODE_OPT_LCKOUT) { m_host->m_fixedMode = false; m_host->setState(HOST_STATE_LOCKOUT); response["message"].set(std::string("Lockout mode")); uint8_t hostMode = m_host->m_state; response["mode"].set(hostMode); reply.payload(response); } #if defined(ENABLE_DMR) else if (mode == MODE_OPT_FDMR) { if (m_dmr != nullptr) { m_host->m_fixedMode = true; m_host->setState(STATE_DMR); response["message"].set(std::string("Fixed mode")); uint8_t hostMode = m_host->m_state; response["mode"].set(hostMode); reply.payload(response); } else { errorPayload(reply, "DMR mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); } } #endif // defined(ENABLE_DMR) #if defined(ENABLE_P25) else if (mode == MODE_OPT_FP25) { if (m_p25 != nullptr) { m_host->m_fixedMode = true; m_host->setState(STATE_P25); response["message"].set(std::string("Fixed mode")); uint8_t hostMode = m_host->m_state; response["mode"].set(hostMode); reply.payload(response); } else { errorPayload(reply, "P25 mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); } } #endif // defined(ENABLE_P25) #if defined(ENABLE_NXDN) else if (mode == MODE_OPT_FNXDN) { if (m_nxdn != nullptr) { m_host->m_fixedMode = true; m_host->setState(STATE_NXDN); response["message"].set(std::string("Fixed mode")); uint8_t hostMode = m_host->m_state; response["mode"].set(hostMode); reply.payload(response); } else { errorPayload(reply, "NXDN mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); } } #endif // defined(ENABLE_NXDN) else { errorPayload(reply, "invalid mode"); } } /// /// /// /// /// /// void RESTAPI::restAPI_PutModemKill(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { if (!validateAuth(request, reply)) { return; } json::object req = json::object(); if (!parseRequestBody(request, reply, req)) { return; } errorPayload(reply, "OK", HTTPPayload::OK); if (!req["force"].is()) { g_killed = true; } else { bool force = req["force"].get(); if (force) { g_killed = true; m_host->setState(HOST_STATE_QUIT); } else { g_killed = true; } } } /// /// /// /// /// /// void RESTAPI::restAPI_PutPermitTG(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { if (!validateAuth(request, reply)) { return; } json::object req = json::object(); if (!parseRequestBody(request, reply, req)) { return; } errorPayload(reply, "OK", HTTPPayload::OK); if (!m_host->m_authoritative) { errorPayload(reply, "Host is authoritative, cannot permit TG"); return; } // validate state is a string within the JSON blob if (!req["state"].is()) { errorPayload(reply, "state was not a valid integer"); return; } DVM_STATE state = (DVM_STATE)req["state"].get(); // validate destination ID is a integer within the JSON blob if (!req["dstId"].is()) { errorPayload(reply, "destination ID was not a valid integer"); return; } uint32_t dstId = req["dstId"].get(); if (dstId == 0U) { errorPayload(reply, "destination ID is an illegal TGID"); return; } switch (state) { case STATE_DMR: #if defined(ENABLE_DMR) { // validate slot is a integer within the JSON blob if (!req["slot"].is()) { errorPayload(reply, "slot was not a valid integer"); return; } uint8_t slot = (uint8_t)req["slot"].get(); if (slot == 0U || slot > 2U) { errorPayload(reply, "illegal DMR slot"); return; } if (m_dmr != nullptr) { m_dmr->permittedTG(dstId, slot); } else { errorPayload(reply, "DMR mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); } } #else { errorPayload(reply, "DMR operations are unavailable", HTTPPayload::SERVICE_UNAVAILABLE); } #endif // defined(ENABLE_DMR) break; case STATE_P25: #if defined(ENABLE_P25) { if (m_p25 != nullptr) { m_p25->permittedTG(dstId); } else { errorPayload(reply, "P25 mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); } } #else { errorPayload(reply, "P25 operations are unavailable", HTTPPayload::SERVICE_UNAVAILABLE); } #endif // defined(ENABLE_P25) break; case STATE_NXDN: #if defined(ENABLE_NXDN) { if (m_nxdn != nullptr) { m_nxdn->permittedTG(dstId); } else { errorPayload(reply, "NXDN mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); } } #else { errorPayload(reply, "NXDN operations are unavailable", HTTPPayload::SERVICE_UNAVAILABLE); } #endif // defined(ENABLE_NXDN) break; default: errorPayload(reply, "invalid mode"); break; } } /// /// /// /// /// /// void RESTAPI::restAPI_PutGrantTG(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { if (!validateAuth(request, reply)) { return; } json::object req = json::object(); if (!parseRequestBody(request, reply, req)) { return; } errorPayload(reply, "OK", HTTPPayload::OK); if (m_host->m_authoritative && (m_host->m_dmrCtrlChannel || m_host->m_p25CtrlChannel || m_host->m_nxdnCtrlChannel)) { errorPayload(reply, "Host is authoritative, cannot grant TG"); return; } // validate state is a string within the JSON blob if (!req["state"].is()) { errorPayload(reply, "state was not a valid integer"); return; } DVM_STATE state = (DVM_STATE)req["state"].get(); // validate destination ID is a integer within the JSON blob if (!req["dstId"].is()) { errorPayload(reply, "destination ID was not a valid integer"); return; } uint32_t dstId = req["dstId"].get(); if (dstId == 0U) { errorPayload(reply, "destination ID is an illegal TGID"); return; } // 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"); return; } uint8_t unitToUnit = (uint8_t)req["unitToUnit"].get(); if (unitToUnit > 1U) { errorPayload(reply, "unit-to-unit must be a 0 or 1"); return; } switch (state) { case STATE_DMR: #if defined(ENABLE_DMR) { // validate slot is a integer within the JSON blob if (!req["slot"].is()) { errorPayload(reply, "slot was not a valid integer"); return; } uint8_t slot = (uint8_t)req["slot"].get(); if (slot == 0U || slot > 2U) { errorPayload(reply, "illegal DMR slot"); return; } if (m_dmr != nullptr) { // TODO TODO //m_dmr->grantTG(dstId, slot, unitToUnit == 1U); } else { errorPayload(reply, "DMR mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); } } #else { errorPayload(reply, "DMR operations are unavailable", HTTPPayload::SERVICE_UNAVAILABLE); } #endif // defined(ENABLE_DMR) break; case STATE_P25: #if defined(ENABLE_P25) { if (m_p25 != nullptr) { // TODO TODO //m_p25->grantTG(dstId, unitToUnit == 1U); } else { errorPayload(reply, "P25 mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); } } #else { errorPayload(reply, "P25 operations are unavailable", HTTPPayload::SERVICE_UNAVAILABLE); } #endif // defined(ENABLE_P25) break; case STATE_NXDN: #if defined(ENABLE_NXDN) { if (m_nxdn != nullptr) { // TODO TODO //nxdn->grantTG(dstId, unitToUnit == 1U); } else { errorPayload(reply, "NXDN mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); } } #else { errorPayload(reply, "NXDN operations are unavailable", HTTPPayload::SERVICE_UNAVAILABLE); } #endif // defined(ENABLE_NXDN) break; default: errorPayload(reply, "invalid mode"); } } /// /// /// /// /// /// void RESTAPI::restAPI_GetReleaseGrants(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { if (!validateAuth(request, reply)) { return; } errorPayload(reply, "OK", HTTPPayload::OK); #if defined(ENABLE_DMR) if (m_dmr != nullptr) { m_dmr->affiliations().releaseGrant(0, true); } #endif // defined(ENABLE_DMR) #if defined(ENABLE_P25) if (m_p25 != nullptr) { m_p25->affiliations().releaseGrant(0, true); } #endif // defined(ENABLE_P25) #if defined(ENABLE_NXDN) if (m_nxdn != nullptr) { m_nxdn->affiliations().releaseGrant(0, true); } #endif // defined(ENABLE_NXDN) } /// /// /// /// /// /// void RESTAPI::restAPI_GetReleaseAffs(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { if (!validateAuth(request, reply)) { return; } errorPayload(reply, "OK", HTTPPayload::OK); #if defined(ENABLE_DMR) if (m_dmr != nullptr) { m_dmr->affiliations().clearGroupAff(0, true); } #endif // defined(ENABLE_DMR) #if defined(ENABLE_P25) if (m_p25 != nullptr) { m_p25->affiliations().clearGroupAff(0, true); } #endif // defined(ENABLE_P25) #if defined(ENABLE_NXDN) if (m_nxdn != nullptr) { m_nxdn->affiliations().clearGroupAff(0, true); } #endif // defined(ENABLE_NXDN) } /// /// /// /// /// /// void RESTAPI::restAPI_GetRIDWhitelist(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { if (!validateAuth(request, reply)) { return; } if (match.size() < 2) { errorPayload(reply, "invalid API call arguments"); return; } errorPayload(reply, "OK", HTTPPayload::OK); uint32_t srcId = (uint32_t)::strtoul(match.str(1).c_str(), NULL, 10); if (srcId != 0U) { m_ridLookup->toggleEntry(srcId, true); } else { errorPayload(reply, "tried to whitelist RID 0"); } } /// /// /// /// /// /// void RESTAPI::restAPI_GetRIDBlacklist(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { if (!validateAuth(request, reply)) { return; } if (match.size() < 2) { errorPayload(reply, "invalid API call arguments"); return; } errorPayload(reply, "OK", HTTPPayload::OK); uint32_t srcId = (uint32_t)::strtoul(match.str(1).c_str(), NULL, 10); if (srcId != 0U) { m_ridLookup->toggleEntry(srcId, false); } else { errorPayload(reply, "tried to blacklist RID 0"); } } /* ** Digital Mobile Radio */ /// /// /// /// /// /// void RESTAPI::restAPI_GetDMRBeacon(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { if (!validateAuth(request, reply)) { return; } #if defined(ENABLE_DMR) errorPayload(reply, "OK", HTTPPayload::OK); if (m_dmr != nullptr) { if (m_host->m_dmrBeacons) { g_fireDMRBeacon = true; } else { errorPayload(reply, "DMR beacons are not enabled", HTTPPayload::SERVICE_UNAVAILABLE); return; } } else { errorPayload(reply, "DMR mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); return; } #else errorPayload(reply, "DMR operations are unavailable", HTTPPayload::SERVICE_UNAVAILABLE); #endif // defined(ENABLE_DMR) } /// /// /// /// /// /// void RESTAPI::restAPI_GetDMRDebug(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { if (!validateAuth(request, reply)) { return; } json::object response = json::object(); setResponseDefaultStatus(response); #if defined(ENABLE_DMR) errorPayload(reply, "OK", HTTPPayload::OK); if (m_dmr != nullptr) { if (match.size() <= 1) { bool debug = m_dmr->getDebug(); bool verbose = m_dmr->getVerbose(); response["debug"].set(debug); response["verbose"].set(verbose); reply.payload(response); return; } else { if (match.size() == 3) { uint8_t debug = (uint8_t)::strtoul(match.str(1).c_str(), NULL, 10); uint8_t verbose = (uint8_t)::strtoul(match.str(2).c_str(), NULL, 10); m_dmr->setDebugVerbose((debug == 1U) ? true : false, (verbose == 1U) ? true : false); } } } else { errorPayload(reply, "DMR mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); return; } #else errorPayload(reply, "DMR operations are unavailable", HTTPPayload::SERVICE_UNAVAILABLE); #endif // defined(ENABLE_DMR) } /// /// /// /// /// /// void RESTAPI::restAPI_GetDMRDumpCSBK(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { if (!validateAuth(request, reply)) { return; } json::object response = json::object(); setResponseDefaultStatus(response); #if defined(ENABLE_DMR) errorPayload(reply, "OK", HTTPPayload::OK); if (m_dmr != nullptr) { if (match.size() <= 1) { bool csbkDump = m_dmr->getCSBKVerbose(); response["verbose"].set(csbkDump); reply.payload(response); return; } else { if (match.size() == 2) { uint8_t enable = (uint8_t)::strtoul(match.str(1).c_str(), NULL, 10); m_dmr->setCSBKVerbose((enable == 1U) ? true : false); } } } else { errorPayload(reply, "DMR mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); return; } #else errorPayload(reply, "DMR operations are unavailable", HTTPPayload::SERVICE_UNAVAILABLE); #endif // defined(ENABLE_DMR) } /// /// /// /// /// /// void RESTAPI::restAPI_PutDMRRID(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { if (!validateAuth(request, reply)) { return; } json::object req = json::object(); if (!parseRequestBody(request, reply, req)) { return; } errorPayload(reply, "OK", HTTPPayload::OK); } /// /// /// /// /// /// void RESTAPI::restAPI_GetDMRCCEnable(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { if (!validateAuth(request, reply)) { return; } if (match.size() < 2) { errorPayload(reply, "invalid API call arguments"); return; } errorPayload(reply, "OK", HTTPPayload::OK); } /// /// /// /// /// /// void RESTAPI::restAPI_GetDMRCCBroadcast(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { if (!validateAuth(request, reply)) { return; } if (match.size() < 2) { errorPayload(reply, "invalid API call arguments"); return; } errorPayload(reply, "OK", HTTPPayload::OK); } /* ** Project 25 */ /// /// /// /// /// /// void RESTAPI::restAPI_GetP25CC(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { if (!validateAuth(request, reply)) { return; } #if defined(ENABLE_P25) errorPayload(reply, "OK", HTTPPayload::OK); if (m_p25 != nullptr) { if (m_host->m_p25CCData) { g_fireP25Control = true; } else { errorPayload(reply, "P25 control data is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); return; } } else { errorPayload(reply, "P25 mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); return; } #else errorPayload(reply, "P25 operations are unavailable", HTTPPayload::SERVICE_UNAVAILABLE); #endif // defined(ENABLE_P25) } /// /// /// /// /// /// void RESTAPI::restAPI_GetP25Debug(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { if (!validateAuth(request, reply)) { return; } json::object response = json::object(); setResponseDefaultStatus(response); #if defined(ENABLE_P25) errorPayload(reply, "OK", HTTPPayload::OK); if (m_dmr != nullptr) { if (match.size() <= 1) { bool debug = m_p25->getDebug(); bool verbose = m_p25->getVerbose(); response["debug"].set(debug); response["verbose"].set(verbose); reply.payload(response); return; } else { if (match.size() == 3) { uint8_t debug = (uint8_t)::strtoul(match.str(1).c_str(), NULL, 10); uint8_t verbose = (uint8_t)::strtoul(match.str(2).c_str(), NULL, 10); m_p25->setDebugVerbose((debug == 1U) ? true : false, (verbose == 1U) ? true : false); } } } else { errorPayload(reply, "P25 mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); return; } #else errorPayload(reply, "P25 operations are unavailable", HTTPPayload::SERVICE_UNAVAILABLE); #endif // defined(ENABLE_P25) } /// /// /// /// /// /// void RESTAPI::restAPI_GetP25DumpTSBK(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { if (!validateAuth(request, reply)) { return; } json::object response = json::object(); setResponseDefaultStatus(response); #if defined(ENABLE_P25) errorPayload(reply, "OK", HTTPPayload::OK); if (m_p25 != nullptr) { if (match.size() <= 1) { bool tsbkDump = m_p25->trunk()->getTSBKVerbose(); response["verbose"].set(tsbkDump); reply.payload(response); return; } else { if (match.size() == 2) { uint8_t enable = (uint8_t)::strtoul(match.str(1).c_str(), NULL, 10); m_p25->trunk()->setTSBKVerbose((enable == 1U) ? true : false); } } } else { errorPayload(reply, "P25 mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); return; } #else errorPayload(reply, "P25 operations are unavailable", HTTPPayload::SERVICE_UNAVAILABLE); #endif // defined(ENABLE_P25) } /// /// /// /// /// /// void RESTAPI::restAPI_PutP25RID(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { if (!validateAuth(request, reply)) { return; } json::object req = json::object(); if (!parseRequestBody(request, reply, req)) { return; } errorPayload(reply, "OK", HTTPPayload::OK); } /// /// /// /// /// /// void RESTAPI::restAPI_GetP25CCEnable(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { if (!validateAuth(request, reply)) { return; } if (match.size() < 2) { errorPayload(reply, "invalid API call arguments"); return; } errorPayload(reply, "OK", HTTPPayload::OK); } /// /// /// /// /// /// void RESTAPI::restAPI_GetP25CCBroadcast(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { if (!validateAuth(request, reply)) { return; } if (match.size() < 2) { errorPayload(reply, "invalid API call arguments"); return; } errorPayload(reply, "OK", HTTPPayload::OK); } /* ** Next Generation Digital Narrowband */ /// /// /// /// /// /// void RESTAPI::restAPI_GetNXDNDebug(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { if (!validateAuth(request, reply)) { return; } json::object response = json::object(); setResponseDefaultStatus(response); #if defined(ENABLE_NXDN) errorPayload(reply, "OK", HTTPPayload::OK); if (m_dmr != nullptr) { if (match.size() <= 1) { bool debug = m_nxdn->getDebug(); bool verbose = m_nxdn->getVerbose(); response["debug"].set(debug); response["verbose"].set(verbose); reply.payload(response); return; } else { if (match.size() == 3) { uint8_t debug = (uint8_t)::strtoul(match.str(1).c_str(), NULL, 10); uint8_t verbose = (uint8_t)::strtoul(match.str(2).c_str(), NULL, 10); m_nxdn->setDebugVerbose((debug == 1U) ? true : false, (verbose == 1U) ? true : false); } } } else { 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) } /// /// /// /// /// /// void RESTAPI::restAPI_GetNXDNDumpRCCH(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) { if (!validateAuth(request, reply)) { return; } json::object response = json::object(); setResponseDefaultStatus(response); #if defined(ENABLE_NXDN) errorPayload(reply, "OK", HTTPPayload::OK); if (m_p25 != nullptr) { if (match.size() <= 1) { bool rcchDump = m_nxdn->getRCCHVerbose(); response["verbose"].set(rcchDump); reply.payload(response); return; } else { if (match.size() == 2) { uint8_t enable = (uint8_t)::strtoul(match.str(1).c_str(), NULL, 10); m_nxdn->setRCCHVerbose((enable == 1U) ? true : false); } } } else { 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) }