You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
FreeDMR/API.py

211 lines
7.4 KiB

#
###############################################################################
# Copyright (C) 2023 Simon Adlem, G7RZU <g7rzu@gb7fr.org.uk>
#
# 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 3 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
###############################################################################
import json
import logging
from twisted.web.resource import Resource
from utils import bytes_4
logger = logging.getLogger(__name__)
MAX_API_BODY = 8192
class APIError(Exception):
def __init__(self, status, message):
self.status = status
self.message = message
super().__init__(message)
class FD_APIController(object):
"""Small, non-blocking control-plane API for live FreeDMR state.
The methods intentionally perform only in-memory dictionary reads/writes.
Expensive snapshots of CONFIG/BRIDGES are not exposed here because this API
shares the Twisted reactor with packet handling.
"""
version = 1
def __init__(self, CONFIG, BRIDGES):
self.CONFIG = CONFIG
self.BRIDGES = BRIDGES
def validateKey(self, dmrid, key):
try:
peer_id = bytes_4(int(dmrid))
except (TypeError, ValueError, OverflowError):
return False
for system, system_config in self.CONFIG['SYSTEMS'].items():
if system_config['MODE'] != 'MASTER':
continue
if peer_id not in system_config['PEERS']:
continue
if key == system_config.get('_opt_key'):
return system
return False
return False
def validateSystemKey(self, systemkey):
return systemkey == self.CONFIG['GLOBAL'].get('SYSTEM_API_KEY')
def reset(self, system):
self.CONFIG['SYSTEMS'][system]['_reset'] = True
def options(self, system, options):
self.CONFIG['SYSTEMS'][system]['OPTIONS'] = options
def getoptions(self, system):
options = self.CONFIG['SYSTEMS'][system].get('OPTIONS')
has_options = options is not None
if isinstance(options, bytes):
options = options.decode('utf-8', 'ignore')
elif options is None:
options = ''
else:
options = str(options)
return {
'connected': bool(self.CONFIG['SYSTEMS'][system]['PEERS']),
'has_options': has_options,
'options': options,
}
def killserver(self):
self.CONFIG['GLOBAL']['_KILL_SERVER'] = True
def resetAllConnections(self):
for system in self.CONFIG['SYSTEMS']:
self.CONFIG['SYSTEMS'][system]['_reset'] = True
class FD_APIResource(Resource):
isLeaf = True
def __init__(self, controller):
Resource.__init__(self)
self.controller = controller
def render_GET(self, request):
path = self._path(request)
if path == '/api/v1/version':
return self._json(request, 200, {'ok': True, 'version': self.controller.version})
if path == '/api/v1/health':
return self._json(request, 200, {'ok': True})
return self._json(request, 404, {'ok': False, 'error': 'not_found'})
def render_POST(self, request):
try:
payload = self._payload(request)
path = self._path(request)
if path == '/api/v1/reset':
return self._user_call(request, payload, self._reset)
if path == '/api/v1/options/get':
return self._user_call(request, payload, self._get_options)
if path == '/api/v1/options/set':
return self._user_call(request, payload, self._set_options)
if path == '/api/v1/system/kill':
return self._system_call(request, payload, self._kill_server)
if path == '/api/v1/system/resetall':
return self._system_call(request, payload, self._reset_all)
return self._json(request, 404, {'ok': False, 'error': 'not_found'})
except APIError as exc:
return self._json(request, exc.status, {'ok': False, 'error': exc.message})
except Exception:
logger.exception('(API) Unhandled API error')
return self._json(request, 500, {'ok': False, 'error': 'internal_error'})
def _path(self, request):
return (b'/' + b'/'.join(request.postpath)).decode('utf-8', 'ignore')
def _payload(self, request):
content_length = None
if hasattr(request, 'getHeader'):
content_length = request.getHeader('content-length')
if content_length is not None:
try:
if int(content_length) > MAX_API_BODY:
raise APIError(413, 'request_too_large')
except ValueError:
raise APIError(400, 'invalid_content_length')
body = request.content.read()
if len(body) > MAX_API_BODY:
raise APIError(413, 'request_too_large')
if not body:
return {}
try:
payload = json.loads(body.decode('utf-8'))
except (TypeError, ValueError):
raise APIError(400, 'invalid_json')
if not isinstance(payload, dict):
raise APIError(400, 'invalid_json')
return payload
def _user_call(self, request, payload, handler):
dmrid = payload.get('dmrid')
key = payload.get('key')
system = self.controller.validateKey(dmrid, key)
if not system:
raise APIError(401, 'invalid_credentials')
result = handler(system, payload)
return self._json(request, 200, {'ok': True, **result})
def _system_call(self, request, payload, handler):
if not self.controller.validateSystemKey(payload.get('systemkey')):
raise APIError(401, 'invalid_credentials')
result = handler(payload)
return self._json(request, 200, {'ok': True, **result})
def _reset(self, system, payload):
self.controller.reset(system)
return {'reset': True, 'system': system}
def _get_options(self, system, payload):
return self.controller.getoptions(system)
def _set_options(self, system, payload):
options = payload.get('options')
if not isinstance(options, str):
raise APIError(400, 'missing_options')
self.controller.options(system, options)
return {'updated': True, 'system': system}
def _kill_server(self, payload):
self.controller.killserver()
return {'killserver': True}
def _reset_all(self, payload):
self.controller.resetAllConnections()
return {'resetall': True}
def _json(self, request, status, payload):
request.setResponseCode(status)
request.setHeader(b'content-type', b'application/json')
return json.dumps(payload, separators=(',', ':')).encode('utf-8')
def make_api_resource(CONFIG, BRIDGES):
return FD_APIResource(FD_APIController(CONFIG, BRIDGES))

Powered by TurnKey Linux.