247 lines
8.0 KiB
Python
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()
|