Files
ols/ols/server.py
2023-06-11 12:40:02 +03:00

247 lines
8.0 KiB
Python

#!/usr/bin/env python3
import asyncio
import logging
import os.path
import sys
from datetime import datetime
from aiohttp import web
import fastjsonschema # type: ignore
from . import config
from . import obsdb
from . import schemas
locators = None
observationdb = None
geolocate_validate = fastjsonschema.compile(schemas.geolocate_v1_schema)
geosubmit_validate = fastjsonschema.compile(schemas.geosubmit_v2_schema)
hint = None
async def all_handler(request):
return web.json_response({}, status=404)
async def locate_handler(request):
global hint
global locators
global observationdb
notfound_error_res = {
'error': {
'errors': [{
'domain': 'geolocation',
'reason': 'notFound',
'message': 'Not found',
}],
'code': 404,
'message': 'Not found',
}
}
parse_error_res = {
# 'details': {
# 'decode': "JSONDecodeError('Expecting value: line 1 column 1 (char 0)')"
# },
'error': {
'code': 400,
'errors': [
{
'domain': 'global',
'message': 'Parse Error',
'reason': 'parseError'
}
],
'message': 'Parse Error'
}
}
unvalidated = await request.json()
try:
data = geolocate_validate(unvalidated)
# Make sure all cell items have a radioType and convert it to upper case
if 'cellTowers' in data:
data['radioType'] = data.get('radioType', 'GSM').upper()
for cell in data.get('cellTowers', []):
cell['radioType'] = cell.get('radioType', data['radioType']).upper()
except fastjsonschema.JsonSchemaValueException as e:
log.warning('Input JSON did not validate: ' + str(e))
log.debug(unvalidated)
return web.json_response(parse_error_res, status=400)
log.info('Got locate request with %s' % ', '.join(
f'{len(data[k])} {k}' for k in data if k in (
'bluetoothBeacons', 'cellTowers', 'wifiAccessPoints', 'fallbacks')))
data['time'] = datetime.now().astimezone()
for station in data.get('bluetoothBeacons', []) + data.get('wifiAccessPoints', []):
station['macAddress'] = station['macAddress'].lower()
observationdb.insert_locate(data)
for locator in locators:
try:
async with asyncio.timeout(20):
latlon, accuracy = await locator.locate(data, hint=hint)
except asyncio.TimeoutError:
log.warning('Locator timeout')
latlon, accuracy = None, None
if latlon is not None:
break
if latlon is not None:
hint = latlon
status = 200
resd = {
'location': {
'lat': latlon[0],
'lng': latlon[1],
},
}
if accuracy is not None and accuracy >= 0:
resd['accuracy'] = accuracy
else:
hint = None
status = 404
resd = notfound_error_res
return web.json_response(resd, status=status)
async def submit_handler(request):
global observationdb
parse_error_res = {
# 'details': {
# 'validation': {
# 'items': 'Required'
# },
# },
'error': {
'code': 400,
'errors': [
{
'domain': 'global',
'message': 'Parse Error',
'reason': 'parseError'
}
],
'message': 'Parse Error'
},
}
unvalidated = await request.json()
try:
data = geosubmit_validate(unvalidated)
except fastjsonschema.JsonSchemaValueException as e:
log.warning('submit: Input JSON did not validate: ' + str(e))
log.debug(unvalidated)
return web.json_response(parse_error_res, status=400)
log.info(f'Got submission with {len(data["items"])} items')
observationdb.insert_submit(data)
return web.json_response({}, status=200)
def main():
global locators
global observationdb
global log
try:
conf = config.get_config()
except config.MissingConfigException as e:
print(str(e))
sys.exit(1)
except FileNotFoundError as e:
print('Could not load config: ' + str(e))
sys.exit(1)
timestampformat = '[%(asctime)s.%(msecs)03d] ' if conf['logtimestamps'] else ''
logging.basicConfig(
level=logging.WARNING, # Loglevel for library modules
format=timestampformat + '%(module)s %(levelname)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S')
log = logging.getLogger(__name__)
logging.getLogger('ols').setLevel(conf['loglevel'])
def get_resolver(name):
if name is None:
log.error(f"Unknown resolver '{name}'")
return None
rtype = conf['resolver'][name]['type']
if rtype == 'cellid':
from .resolver.cellid import CellIdResolver
return CellIdResolver(conf['resolver'][name]['db'])
elif rtype == 'wiglenet':
from .resolver.wiglenet import WiglenetResolver
wconf = conf['resolver'][name]
return WiglenetResolver(wconf['apiuser'], wconf['apitoken'], wconf['db'])
elif rtype == 'wiglenetcache':
from .resolver.wiglenet import WiglenetCacheOnlyResolver
wconf = conf['resolver'][name]
return WiglenetCacheOnlyResolver(wconf['db'])
elif rtype == 'localdb':
from .resolver.local import get_localresolver
lconf = conf['resolver'][name]
return get_localresolver(lconf['db'])
else:
log.error(f"Unknown resolver type '{rtype}'")
return None
locators = []
for method in conf['methods']:
method_conf = conf['locator'][method]
mtype = method_conf.get('type')
if mtype is None:
raise config.MissingConfigException
elif mtype == 'clustering':
from .locator.clustering import ClusteringLocator
wlist = method_conf.get('wifiresolvers', [])
wifiresolvers = [get_resolver(e) for e in wlist]
clist = method_conf.get('cellresolvers', [])
cellresolvers = [get_resolver(e) for e in clist]
locators.append(ClusteringLocator(wifiresolvers, cellresolvers))
elif mtype == 'strongestcell':
from .locator.single import StrongestCellLocator
clist = method_conf.get('cellresolvers', [])
cellresolvers = [get_resolver(e) for e in clist]
locators.append(StrongestCellLocator(cellresolvers))
elif mtype == 'strongestwifi':
from .locator.single import StrongestWifiLocator
wlist = method_conf.get('wifiresolvers', [])
wifiresolvers = [get_resolver(e) for e in wlist]
locators.append(StrongestWifiLocator(wifiresolvers))
elif mtype == 'm8b':
from .locator.m8b import M8BLocator
locators.append(M8BLocator(os.path.expanduser(method_conf['datafile'])))
elif mtype == 'web':
from .locator.web import WebLocator
locators.append(WebLocator(method_conf['locateurl'], method_conf['apikey']))
else:
raise ValueError(f'Unknown locator type in config: {mtype}')
log.info(f"Using '{mtype}' locator")
if conf['obsdb'] is None:
# Use the dummy obsdb class
observationdb = obsdb.ObservationDBBase(None)
else:
observationdb = obsdb.ObservationDB(conf['obsdb'])
log.warning(f"Recording all observations to '{conf['obsdb']}'")
app = web.Application()
app.add_routes([
web.post('/v1/geolocate', locate_handler),
web.post('/v2/geosubmit', submit_handler),
web.route('*', '/{path}', all_handler),
web.route('*', '/', all_handler),
])
log.info('Starting service...\n')
web.run_app(app, host=conf['host'], port=conf['port'])
log.info('Stopping service...\n')
observationdb.close()
if __name__ == '__main__':
main()