From 77d29e314cbf244bb877fab63a08518f76452c7c Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 19 Feb 2022 14:41:39 -0800 Subject: [PATCH] Add API key authentication. We don't have any sensitive data, but we do access the file system. On the off chance that some phishing website decides to try to use Liberation as an attack vector, prevent access to the API by unauthorized applications. An API key is generated at each program start and passed to the front end via the QWebChannel. --- game/server/app.py | 5 +++-- game/server/security.py | 15 +++++++++++++++ qt_ui/widgets/map/model/mapmodel.py | 6 ++++++ qt_ui/windows/QLiberationWindow.py | 3 +++ resources/ui/map/map.js | 12 +++++++++--- 5 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 game/server/security.py diff --git a/game/server/app.py b/game/server/app.py index 0def220a..2b02f3be 100644 --- a/game/server/app.py +++ b/game/server/app.py @@ -1,7 +1,8 @@ -from fastapi import FastAPI +from fastapi import Depends, FastAPI from . import debuggeometries, eventstream +from .security import ApiKeyManager -app = FastAPI() +app = FastAPI(dependencies=[Depends(ApiKeyManager.verify)]) app.include_router(debuggeometries.router) app.include_router(eventstream.router) diff --git a/game/server/security.py b/game/server/security.py new file mode 100644 index 00000000..de61bdc9 --- /dev/null +++ b/game/server/security.py @@ -0,0 +1,15 @@ +import secrets + +from fastapi import HTTPException, Security, status +from fastapi.security import APIKeyHeader + +API_KEY_HEADER = APIKeyHeader(name="X-API-Key") + + +class ApiKeyManager: + KEY = secrets.token_urlsafe() + + @classmethod + def verify(cls, api_key_header: str = Security(API_KEY_HEADER)) -> None: + if api_key_header != cls.KEY: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) diff --git a/qt_ui/widgets/map/model/mapmodel.py b/qt_ui/widgets/map/model/mapmodel.py index 1ee2b375..7d4ec26c 100644 --- a/qt_ui/widgets/map/model/mapmodel.py +++ b/qt_ui/widgets/map/model/mapmodel.py @@ -10,6 +10,7 @@ from game import Game from game.ato.airtaaskingorder import AirTaskingOrder from game.profiling import logged_duration from game.server.leaflet import LeafletLatLon +from game.server.security import ApiKeyManager from game.theater import ( ConflictTheater, ) @@ -46,6 +47,7 @@ from .unculledzonejs import UnculledZone class MapModel(QObject): cleared = Signal() + apiKeyChanged = Signal(str) mapCenterChanged = Signal(list) controlPointsChanged = Signal() groundObjectsChanged = Signal() @@ -187,6 +189,10 @@ class MapModel(QObject): self._map_center = [ll.latitude, ll.longitude] self.mapCenterChanged.emit(self._map_center) + @Property(str, notify=apiKeyChanged) + def apiKey(self) -> str: + return ApiKeyManager.KEY + @Property(list, notify=mapCenterChanged) def mapCenter(self) -> LeafletLatLon: return self._map_center diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py index abd1d471..1cc645d3 100644 --- a/qt_ui/windows/QLiberationWindow.py +++ b/qt_ui/windows/QLiberationWindow.py @@ -21,6 +21,7 @@ import qt_ui.uiconstants as CONST from game import Game, VERSION, persistency from game.debriefing import Debriefing from game.server import EventStream +from game.server.security import ApiKeyManager from qt_ui import liberation_install from qt_ui.dialogs import Dialog from qt_ui.models import GameModel @@ -88,6 +89,8 @@ class QLiberationWindow(QMainWindow): else: self.onGameGenerated(self.game) + logging.debug(f"API Key: {ApiKeyManager.KEY}") + def initUi(self): hbox = QSplitter(Qt.Horizontal) vbox = QSplitter(Qt.Vertical) diff --git a/resources/ui/map/map.js b/resources/ui/map/map.js index e86940e5..ca9e6d82 100644 --- a/resources/ui/map/map.js +++ b/resources/ui/map/map.js @@ -3,10 +3,15 @@ const ENABLE_EXPENSIVE_DEBUG_TOOLS = false; const HTTP_BACKEND = "http://[::1]:5000"; const WS_BACKEND = "ws://[::1]:5000/eventstream"; +// Uniquely generated at startup and passed to use by the QWebChannel. +var API_KEY = null; + function getJson(endpoint) { - return fetch(`${HTTP_BACKEND}${endpoint}`).then((response) => - response.json() - ); + return fetch(`${HTTP_BACKEND}${endpoint}`, { + headers: { + "X-API-Key": API_KEY, + }, + }).then((response) => response.json()); } const Colors = Object.freeze({ @@ -356,6 +361,7 @@ new QWebChannel(qt.webChannelTransport, function (channel) { }); game = channel.objects.game; + API_KEY = game.apiKey; drawInitialMap(); game.cleared.connect(clearAllLayers); game.mapCenterChanged.connect(recenterMap);