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);