diff --git a/client/app.js b/client/app.js index 7c2ad473..8fbd41c1 100644 --- a/client/app.js +++ b/client/app.js @@ -3,6 +3,7 @@ var path = require('path'); var cookieParser = require('cookie-parser'); var logger = require('morgan'); var fs = require('fs'); +var basicAuth = require('express-basic-auth') var atcRouter = require('./routes/api/atc'); var indexRouter = require('./routes/index'); @@ -26,7 +27,8 @@ app.set('view engine', 'ejs'); let rawdata = fs.readFileSync('../olympus.json'); let config = JSON.parse(rawdata); -app.get('/config', (req, res) => res.send(config)); +if (config["server"] != undefined) + app.get('/config', (req, res) => res.send(config["server"])); module.exports = app; @@ -38,3 +40,8 @@ app.get('/demo/bullseyes', (req, res) => demoDataGenerator.bullseyes(req, res)); app.get('/demo/airbases', (req, res) => demoDataGenerator.airbases(req, res)); app.get('/demo/mission', (req, res) => demoDataGenerator.mission(req, res)); +app.use('/demo', basicAuth({ + users: { 'admin': 'socks' } +})) + + diff --git a/client/package-lock.json b/client/package-lock.json index e7858b9a..2f89a883 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -31,6 +31,7 @@ "browserify": "^17.0.0", "concurrently": "^7.6.0", "esmify": "^2.1.1", + "express-basic-auth": "^1.2.1", "nodemon": "^2.0.20", "sortablejs": "^1.15.0", "tsify": "^5.0.4", @@ -3283,6 +3284,15 @@ "node": ">= 0.10.0" } }, + "node_modules/express-basic-auth": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.1.tgz", + "integrity": "sha512-L6YQ1wQ/mNjVLAmK3AG1RK6VkokA1BIY6wmiH304Xtt/cLTps40EusZsU1Uop+v9lTDPxdtzbFmdXfFO3KEnwA==", + "dev": true, + "dependencies": { + "basic-auth": "^2.0.1" + } + }, "node_modules/express/node_modules/cookie": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", @@ -8148,6 +8158,15 @@ } } }, + "express-basic-auth": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.1.tgz", + "integrity": "sha512-L6YQ1wQ/mNjVLAmK3AG1RK6VkokA1BIY6wmiH304Xtt/cLTps40EusZsU1Uop+v9lTDPxdtzbFmdXfFO3KEnwA==", + "dev": true, + "requires": { + "basic-auth": "^2.0.1" + } + }, "fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", diff --git a/client/package.json b/client/package.json index 40823578..47fe7cd0 100644 --- a/client/package.json +++ b/client/package.json @@ -33,6 +33,7 @@ "browserify": "^17.0.0", "concurrently": "^7.6.0", "esmify": "^2.1.1", + "express-basic-auth": "^1.2.1", "nodemon": "^2.0.20", "sortablejs": "^1.15.0", "tsify": "^5.0.4", diff --git a/client/public/stylesheets/layout.css b/client/public/stylesheets/layout.css index ff8e8a54..5c60d1bb 100644 --- a/client/public/stylesheets/layout.css +++ b/client/public/stylesheets/layout.css @@ -16,12 +16,13 @@ #olympus-toolbar-summary { background-image: url("/images/icon-round.png"); - background-position: 25px 20px; + background-position: 20px 22px; background-repeat: no-repeat; - background-size: 36px 36px; + background-size: 45px 45px; display: flex; flex-direction: column; - text-indent: 44px; + text-indent: 60px; + padding: 20px; } dl.ol-data-grid { diff --git a/client/public/stylesheets/olympus.css b/client/public/stylesheets/olympus.css index 1d2aed0b..d7905e9b 100644 --- a/client/public/stylesheets/olympus.css +++ b/client/public/stylesheets/olympus.css @@ -251,13 +251,14 @@ form>div { text-align: left; white-space: nowrap; width: 100%; - padding: 10px; + padding: 5px; border-radius: var(--border-radius-sm); } .ol-select>.ol-select-options>div a:hover, .ol-select>.ol-select-options>div button:hover { background-color: #FFF3; + text-decoration: none; } .ol-panel-list { @@ -725,21 +726,21 @@ body[data-hide-navyunit] #unit-visibility-control-navyunit { #splash-screen { background-image: url("/images/splash/splash_pic_ship.png"); background-position: 100% 50%; - background-size: 320px; + background-size: 60%; border-radius: var(--border-radius-lg); - display: none; overflow: hidden; - width: 700px; + width: 1200px; + z-index: 99999; } #splash-content { background-color: var(--background-steel); display: flex; flex-direction: column; - padding: 20px; + padding: 30px; position: relative; row-gap: 10px; - width: 55%; + width: 50%; z-index: 10; } @@ -747,7 +748,7 @@ body[data-hide-navyunit] #unit-visibility-control-navyunit { background-color: var(--background-steel); content: ""; display: block; - height: 250px; + height: 800px; position: absolute; right: 0; top: 0; @@ -775,20 +776,84 @@ body[data-hide-navyunit] #unit-visibility-control-navyunit { line-height: 25px; white-space: nowrap; width: fit-content; + padding: 2px; } #splash-content .app-version { font-size: 11px; } -#splash-content #legal-stuff h4 { +#splash-content #legal-stuff h5 { text-transform: uppercase; } #splash-content #legal-stuff p { font-size: 10px; + color:#FFF7; + width: 120%; +} + +#splash-content.ol-dialog-content { + margin: 0px; } .feature-splashScreen #splash-screen { display: flex; +} + +#gray-out { + position: fixed; + height: 100%; + width: 100%; + left: 0px; + top: 0px; + z-index: 9999; + background-color: #000A; +} + +#authentication-form { + display: flex; + align-items: end; + column-gap: 10px; + margin: 10px 0px; + flex-direction: row; +} + +#authentication-form>div { + display: flex; + align-items: start; + row-gap: 4px; + flex-direction: column; +} + +#authentication-form>div>input { + height: 35px; + border-radius: var(--border-radius-sm); + border: 0px solid transparent; + width: 200px; +} + +#splash-content a { + color: #FFFB; + font-weight: bold; +} + +#connection-status { + margin-bottom: 5px; +} + +#connection-status[data-status="connecting"]::before { + content: "Connecting..."; + animation: blinker 1s linear infinite; +} + +#connection-status[data-status="failed"]::before { + content: "Incorrect username/password!"; + color: var(--primary-red); +} + +@keyframes blinker { + 50% { + opacity: 0; + } } \ No newline at end of file diff --git a/client/src/featureswitches.ts b/client/src/featureswitches.ts index ed52d8eb..c5776683 100644 --- a/client/src/featureswitches.ts +++ b/client/src/featureswitches.ts @@ -90,7 +90,7 @@ export class FeatureSwitches { }), new FeatureSwitch({ - "defaultEnabled": false, + "defaultEnabled": true, "label": "Show splash screen", "masterSwitch": true, "name": "splashScreen" @@ -116,36 +116,24 @@ export class FeatureSwitches { #testSwitches() { - for ( const featureSwitch of this.#featureSwitches ) { - if ( featureSwitch.isEnabled() ) { - if ( typeof featureSwitch.onEnabled === "function" ) { featureSwitch.onEnabled(); } - } else { - document.querySelectorAll( "[data-feature-switch='" + featureSwitch.name + "']" ).forEach( el => { - if ( featureSwitch.removeArtifactsIfDisabled === false ) { el.remove(); } else { el.classList.add( "hide" ); } - }); - } - document.body.classList.toggle( "feature-" + featureSwitch.name, featureSwitch.isEnabled() ); - } - } - savePreferences() { let preferences:any = {}; diff --git a/client/src/index.ts b/client/src/index.ts index d7843625..b3e7e63e 100644 --- a/client/src/index.ts +++ b/client/src/index.ts @@ -9,7 +9,7 @@ import { AIC } from "./aic/aic"; import { ATC } from "./atc/atc"; import { FeatureSwitches } from "./featureswitches"; import { LogPanel } from "./panels/logpanel"; -import { getAirbases, getBullseye as getBullseyes, getConfig, getMission, getUnits, setAddress, toggleDemoEnabled } from "./server/server"; +import { getAirbases, getBullseye, getConfig, getFreezed, getMission, getUnits, setAddress, setCredentials, setFreezed, startUpdate, toggleDemoEnabled } from "./server/server"; import { UnitDataTable } from "./units/unitdatatable"; import { keyEventWasInInput } from "./other/utils"; import { Popup } from "./popups/popup"; @@ -31,11 +31,8 @@ var logPanel: LogPanel; var infoPopup: Popup; -var connected: boolean = false; -var paused: boolean = false; var activeCoalition: string = "blue"; -var sessionHash: string | null = null; var unitDataTable: UnitDataTable; var featureSwitches; @@ -49,15 +46,18 @@ function setup() { missionHandler = new MissionHandler(); /* Panels */ - unitInfoPanel = new UnitInfoPanel("unit-info-panel"); - unitControlPanel = new UnitControlPanel("unit-control-panel"); - connectionStatusPanel = new ConnectionStatusPanel("connection-status-panel"); - mouseInfoPanel = new MouseInfoPanel("mouse-info-panel"); + unitInfoPanel = new UnitInfoPanel("unit-info-panel"); + unitControlPanel = new UnitControlPanel("unit-control-panel"); + connectionStatusPanel = new ConnectionStatusPanel("connection-status-panel"); + mouseInfoPanel = new MouseInfoPanel("mouse-info-panel"); //logPanel = new LogPanel("log-panel"); /* Popups */ infoPopup = new Popup("info-popup"); + /* Controls */ + new Dropdown("app-icon", () => { }); + /* Unit data table */ unitDataTable = new UnitDataTable("unit-data-table"); @@ -65,7 +65,6 @@ function setup() { let aicFeatureSwitch = featureSwitches.getSwitch("aic"); if (aicFeatureSwitch?.isEnabled()) { aic = new AIC(); - // TODO: add back buttons } /* ATC */ @@ -75,82 +74,23 @@ function setup() { atc.startUpdates(); } - new Dropdown( "app-icon", () => {} ); - /* Setup event handlers */ setupEvents(); - getConfig(readConfig) + /* Load the config file */ + getConfig(readConfig); } -function readConfig(config: any) -{ - if (config && config["server"] != undefined && config["server"]["address"] != undefined && config["server"]["port"] != undefined) - { - const address = config["server"]["address"]; - const port = config["server"]["port"]; +function readConfig(config: any) { + if (config && config["address"] != undefined && config["port"] != undefined) { + const address = config["address"]; + const port = config["port"]; if (typeof address === 'string' && typeof port == 'number') - setAddress(address == "*"? window.location.hostname: address, port); - - /* On the first connection, force request of full data */ - getAirbases((data: AirbasesData) => getMissionData()?.update(data)); - getBullseyes((data: BullseyesData) => getMissionData()?.update(data)); - getMission((data: any) => {getMissionData()?.update(data)}); - getUnits((data: UnitsData) => getUnitsManager()?.update(data), true /* Does a full refresh */); - - /* Start periodically requesting updates */ - startPeriodicUpdate(); + setAddress(address == "*" ? window.location.hostname : address, port); } else { throw new Error('Could not read configuration file!'); - } -} - -function startPeriodicUpdate() { - requestUpdate(); - requestRefresh(); -} - -function requestUpdate() { - /* Main update rate = 250ms is minimum time, equal to server update time. */ - getUnits((data: UnitsData) => { - if (!getPaused()){ - getUnitsManager()?.update(data); - checkSessionHash(data.sessionHash); - } - }, false); - window.setTimeout(() => requestUpdate(), getConnected() ? 250 : 1000); - - getConnectionStatusPanel()?.update(getConnected()); -} - -function requestRefresh() { - /* Main refresh rate = 5000ms. */ - getUnits((data: UnitsData) => { - if (!getPaused()){ - getUnitsManager()?.update(data); - getAirbases((data: AirbasesData) => getMissionData()?.update(data)); - getBullseyes((data: BullseyesData) => getMissionData()?.update(data)); - getMission((data: any) => { - getMissionData()?.update(data) - }); - - // Update the list of existing units - getUnitDataTable()?.update(); - - checkSessionHash(data.sessionHash); - } - }, true); - window.setTimeout(() => requestRefresh(), 5000); -} - -function checkSessionHash(newSessionHash: string) { - if (sessionHash != null) { - if (newSessionHash != sessionHash) - location.reload(); } - else - sessionHash = newSessionHash; } function setupEvents() { @@ -164,7 +104,7 @@ function setupEvents() { } const triggerElement = target.closest("[data-on-click]"); - + if (triggerElement instanceof HTMLElement) { const eventName: string = triggerElement.dataset.onClick || ""; let params = JSON.parse(triggerElement.dataset.onClickParams || "{}"); @@ -181,7 +121,7 @@ function setupEvents() { /* Keyup events */ document.addEventListener("keyup", ev => { - if ( keyEventWasInInput( ev ) ) { + if (keyEventWasInInput(ev)) { return; } switch (ev.code) { @@ -195,13 +135,13 @@ function setupEvents() { unitDataTable.toggle(); break case "Space": - setPaused(!getPaused()); + setFreezed(!getFreezed()); break; case "KeyW": case "KeyA": case "KeyS": case "KeyD": - case "ArrowLeft": + case "ArrowLeft": case "ArrowRight": case "ArrowUp": case "ArrowDown": @@ -212,7 +152,7 @@ function setupEvents() { /* Keydown events */ document.addEventListener("keydown", ev => { - if ( keyEventWasInInput( ev ) ) { + if (keyEventWasInInput(ev)) { return; } switch (ev.code) { @@ -220,7 +160,7 @@ function setupEvents() { case "KeyA": case "KeyS": case "KeyD": - case "ArrowLeft": + case "ArrowLeft": case "ArrowRight": case "ArrowUp": case "ArrowDown": @@ -229,15 +169,31 @@ function setupEvents() { } }); - document.addEventListener( "closeDialog", (ev: CustomEventInit) => { - ev.detail._element.closest( ".ol-dialog" ).classList.add( "hide" ); + document.addEventListener("closeDialog", (ev: CustomEventInit) => { + ev.detail._element.closest(".ol-dialog").classList.add("hide"); }); - document.addEventListener( "toggleElements", (ev: CustomEventInit) => { - document.querySelectorAll( ev.detail.selector ).forEach( el => { - el.classList.toggle( "hide" ); + document.addEventListener("toggleElements", (ev: CustomEventInit) => { + document.querySelectorAll(ev.detail.selector).forEach(el => { + el.classList.toggle("hide"); }) }); + + document.addEventListener("tryConnection", () => { + const form = document.querySelector("#splash-content")?.querySelector("#authentication-form"); + const username = ( (form?.querySelector("#username"))).value; + const password = ( (form?.querySelector("#password"))).value; + setCredentials(username, btoa("admin" + ":" + password)); + + /* Start periodically requesting updates */ + startUpdate(); + + setConnectionStatus("connecting"); + }) + + document.addEventListener("reloadPage", () => { + location.reload(); + }) } export function getMap() { @@ -285,23 +241,10 @@ export function getActiveCoalition() { return activeCoalition; } -export function setConnected(newConnected: boolean) { - if (connected != newConnected) - newConnected? getInfoPopup().setText("Connected to DCS Olympus server"): getInfoPopup().setText("Disconnected from DCS Olympus server"); - connected = newConnected; -} - -export function getConnected() { - return connected; -} - -export function setPaused(newPaused: boolean) { - paused = newPaused; - paused? getInfoPopup().setText("Paused"): getInfoPopup().setText("Unpaused"); -} - -export function getPaused() { - return paused; +export function setConnectionStatus(status: string) { + const el = document.querySelector("#connection-status") as HTMLElement; + if (el) + el.dataset["status"] = status; } export function getInfoPopup() { diff --git a/client/src/map/map.ts b/client/src/map/map.ts index c7bfd727..a531e33a 100644 --- a/client/src/map/map.ts +++ b/client/src/map/map.ts @@ -316,7 +316,7 @@ export class Map extends L.Map { } this.setView(bounds.getCenter(), 8); - this.setMaxBounds(bounds); + //this.setMaxBounds(bounds); if (this.#miniMap) this.#miniMap.remove(); diff --git a/client/src/panels/unitcontrolpanel.ts b/client/src/panels/unitcontrolpanel.ts index 4723d1ba..4fe1de17 100644 --- a/client/src/panels/unitcontrolpanel.ts +++ b/client/src/panels/unitcontrolpanel.ts @@ -219,7 +219,7 @@ export class UnitControlPanel extends Panel { // Default values for "normal" units this.#radioCallsignDropdown.setOptions(["Enfield", "Springfield", "Uzi", "Colt", "Dodge", "Ford", "Chevy", "Pontiac"]); - this.#radioCallsignDropdown.selectValue(unit.getTaskData().radioCallsign); + this.#radioCallsignDropdown.selectValue(unit.getTaskData().radioCallsign - 1); // Input values var tankerCheckbox = this.#advancedSettingsDialog.querySelector("#tanker-checkbox")?.querySelector("input") @@ -241,7 +241,7 @@ export class UnitControlPanel extends Panel { this.#radioDecimalsDropdown.setValue("." + radioDecimals); // Make sure its in the valid range - if (!this.#radioCallsignDropdown.selectValue(unit.getTaskData().radioCallsign)) + if (!this.#radioCallsignDropdown.selectValue(unit.getTaskData().radioCallsign - 1)) this.#radioCallsignDropdown.selectValue(0); // Set options for tankers @@ -249,7 +249,7 @@ export class UnitControlPanel extends Panel { if (roles != undefined && Array.prototype.concat.apply([], roles)?.includes("Tanker")){ this.#advancedSettingsDialog.querySelector("#tanker-checkbox")?.classList.remove("hide"); this.#radioCallsignDropdown.setOptions(["Texaco", "Arco", "Shell"]); - this.#radioCallsignDropdown.selectValue(unit.getTaskData().radioCallsign); + this.#radioCallsignDropdown.selectValue(unit.getTaskData().radioCallsign - 1); } else { this.#advancedSettingsDialog.querySelector("#tanker-checkbox")?.classList.add("hide"); @@ -259,7 +259,7 @@ export class UnitControlPanel extends Panel { if (roles != undefined && Array.prototype.concat.apply([], roles)?.includes("AWACS")){ this.#advancedSettingsDialog.querySelector("#AWACS-checkbox")?.classList.remove("hide"); this.#radioCallsignDropdown.setOptions(["Overlord", "Magic", "Wizard", "Focus", "Darkstar"]); - this.#radioCallsignDropdown.selectValue(unit.getTaskData().radioCallsign); + this.#radioCallsignDropdown.selectValue(unit.getTaskData().radioCallsign - 1); } else { this.#advancedSettingsDialog.querySelector("#AWACS-checkbox")?.classList.add("hide"); } @@ -276,7 +276,7 @@ export class UnitControlPanel extends Panel { const TACANCallsign = this.#advancedSettingsDialog.querySelector("#tacan-callsign")?.querySelector("input")?.value const radioMHz = Number(this.#advancedSettingsDialog.querySelector("#radio-mhz")?.querySelector("input")?.value); const radioDecimals = this.#radioDecimalsDropdown.getValue(); - const radioCallsign = this.#radioCallsignDropdown.getIndex(); + const radioCallsign = this.#radioCallsignDropdown.getIndex() + 1; const radioCallsignNumber = Number(this.#advancedSettingsDialog.querySelector("#radio-callsign-number")?.querySelector("input")?.value); var radioFrequency = (radioMHz * 1000 + Number(radioDecimals.substring(1))) * 1000; diff --git a/client/src/server/server.ts b/client/src/server/server.ts index c1cd1420..80e10563 100644 --- a/client/src/server/server.ts +++ b/client/src/server/server.ts @@ -1,7 +1,10 @@ import * as L from 'leaflet' -import { setConnected } from '..'; +import { getConnectionStatusPanel, getInfoPopup, getMissionData, getUnitDataTable, getUnitsManager, setConnectionStatus } from '..'; import { SpawnOptions } from '../controls/mapcontextmenu'; +var connected: boolean = false; +var freezed: boolean = false; + var REST_ADDRESS = "http://localhost:30000/olympus"; var DEMO_ADDRESS = window.location.href + "demo"; const UNITS_URI = "units"; @@ -10,29 +13,50 @@ const AIRBASES_URI = "airbases"; const BULLSEYE_URI = "bullseyes"; const MISSION_URI = "mission"; +var username = ""; +var credentials = ""; + +var sessionHash: string | null = null; var lastUpdateTime = 0; var demoEnabled = false; -export function toggleDemoEnabled() -{ +export function toggleDemoEnabled() { demoEnabled = !demoEnabled; } -export function GET(callback: CallableFunction, uri: string, options?: string){ +export function setCredentials(newUsername: string, newCredentials: string) { + username = newUsername; + credentials = newCredentials; +} + +export function GET(callback: CallableFunction, uri: string, options?: string) { var xmlHttp = new XMLHttpRequest(); xmlHttp.open("GET", `${demoEnabled? DEMO_ADDRESS: REST_ADDRESS}/${uri}${options? options: ''}`, true); + if (credentials) + xmlHttp.setRequestHeader("Authorization", "Basic " + credentials); xmlHttp.onload = function (e) { - var data = JSON.parse(xmlHttp.responseText); - if (uri !== UNITS_URI || parseInt(data.time) > lastUpdateTime) - { - callback(data); - lastUpdateTime = parseInt(data.time); - if (isNaN(lastUpdateTime)) - lastUpdateTime = 0; - setConnected(true); + if (xmlHttp.status == 200) { + var data = JSON.parse(xmlHttp.responseText); + if (uri !== UNITS_URI || parseInt(data.time) > lastUpdateTime) + { + callback(data); + lastUpdateTime = parseInt(data.time); + if (isNaN(lastUpdateTime)) + lastUpdateTime = 0; + setConnected(true); + } + } else if (xmlHttp.status == 401) { + console.error("Incorrect username/password"); + setConnectionStatus("failed"); + } else { + setConnected(false); } }; - xmlHttp.onerror = function () { + xmlHttp.onreadystatechange = function (res) { + console.error("An error occurred during the XMLHttpRequest"); + setConnected(false); + }; + xmlHttp.onerror = function (res) { console.error("An error occurred during the XMLHttpRequest"); setConnected(false); }; @@ -40,13 +64,15 @@ export function GET(callback: CallableFunction, uri: string, options?: string){ } export function POST(request: object, callback: CallableFunction){ - var xhr = new XMLHttpRequest(); - xhr.open("PUT", demoEnabled? DEMO_ADDRESS: REST_ADDRESS); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { + var xmlHttp = new XMLHttpRequest(); + xmlHttp.open("PUT", demoEnabled? DEMO_ADDRESS: REST_ADDRESS); + xmlHttp.setRequestHeader("Content-Type", "application/json"); + if (credentials) + xmlHttp.setRequestHeader("Authorization", "Basic " + credentials); + xmlHttp.onreadystatechange = () => { callback(); }; - xhr.send(JSON.stringify(request)); + xmlHttp.send(JSON.stringify(request)); } export function getConfig(callback: CallableFunction) { @@ -208,4 +234,81 @@ export function setAdvacedOptions(ID: number, isTanker: boolean, isAWACS: boolea var data = { "setAdvancedOptions": command }; POST(data, () => { }); +} + +export function startUpdate() { + /* On the first connection, force request of full data */ + getAirbases((data: AirbasesData) => getMissionData()?.update(data)); + getBullseye((data: BullseyesData) => getMissionData()?.update(data)); + getMission((data: any) => { getMissionData()?.update(data) }); + getUnits((data: UnitsData) => getUnitsManager()?.update(data), true /* Does a full refresh */); + + requestUpdate(); + requestRefresh(); +} + +export function requestUpdate() { + /* Main update rate = 250ms is minimum time, equal to server update time. */ + getUnits((data: UnitsData) => { + if (!getFreezed()) { + getUnitsManager()?.update(data); + checkSessionHash(data.sessionHash); + } + }, false); + window.setTimeout(() => requestUpdate(), getConnected() ? 250 : 1000); + + getConnectionStatusPanel()?.update(getConnected()); +} + +export function requestRefresh() { + /* Main refresh rate = 5000ms. */ + getUnits((data: UnitsData) => { + if (!getFreezed()) { + getUnitsManager()?.update(data); + getAirbases((data: AirbasesData) => getMissionData()?.update(data)); + getBullseye((data: BullseyesData) => getMissionData()?.update(data)); + getMission((data: any) => { + getMissionData()?.update(data) + }); + + // Update the list of existing units + getUnitDataTable()?.update(); + + checkSessionHash(data.sessionHash); + } + }, true); + window.setTimeout(() => requestRefresh(), 5000); +} + +export function checkSessionHash(newSessionHash: string) { + if (sessionHash != null) { + if (newSessionHash != sessionHash) + location.reload(); + } + else + sessionHash = newSessionHash; +} + +export function setConnected(newConnected: boolean) { + if (connected != newConnected) + newConnected ? getInfoPopup().setText("Connected to DCS Olympus server") : getInfoPopup().setText("Disconnected from DCS Olympus server"); + connected = newConnected; + + if (connected) { + document.querySelector("#splash-screen")?.classList.add("hide"); + document.querySelector("#gray-out")?.classList.add("hide"); + } +} + +export function getConnected() { + return connected; +} + +export function setFreezed(newFreezed: boolean) { + freezed = newFreezed; + freezed ? getInfoPopup().setText("Freezed") : getInfoPopup().setText("Unfreezed"); +} + +export function getFreezed() { + return freezed; } \ No newline at end of file diff --git a/client/views/dialogs.ejs b/client/views/dialogs.ejs index 042c05be..31183517 100644 --- a/client/views/dialogs.ejs +++ b/client/views/dialogs.ejs @@ -1,20 +1,25 @@ -
- +
-

DCS Olympus

Dynamic Unit Command

Version v0.2.0
- +

+ +
diff --git a/client/views/index.ejs b/client/views/index.ejs index c477210c..ae159a9d 100644 --- a/client/views/index.ejs +++ b/client/views/index.ejs @@ -34,6 +34,8 @@ <%- include('dialogs.ejs') %> <%- include('unitdatatable.ejs') %> <%- include('popups.ejs') %> + +
<% /* %> <%- include('log.ejs') %> diff --git a/client/views/navbar.ejs b/client/views/navbar.ejs index 2ddf7c8d..5362d56f 100644 --- a/client/views/navbar.ejs +++ b/client/views/navbar.ejs @@ -6,8 +6,8 @@
-

Olympus

-
v0.2.0
+

DCS Olympus

+
version v0.2.0
Discord @@ -15,6 +15,9 @@ +
diff --git a/olympus.json b/olympus.json index 7b1b026b..3618c236 100644 --- a/olympus.json +++ b/olympus.json @@ -2,5 +2,8 @@ "server": { "address": "localhost", "port": 30000 + }, + "authentication": { + "password": "password" } } diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj index b6c5614d..4759cb4f 100644 --- a/src/core/core.vcxproj +++ b/src/core/core.vcxproj @@ -185,7 +185,7 @@ NotUsing - include;..\..\third-party\lua\include;..\utils\include;..\shared\include;..\dcstools\include;..\logger\include;..\luatools\include + include;..\..\third-party\base64\include;..\..\third-party\lua\include;..\utils\include;..\shared\include;..\dcstools\include;..\logger\include;..\luatools\include stdcpp20 diff --git a/src/core/include/server.h b/src/core/include/server.h index f77cba5b..ec08d264 100644 --- a/src/core/include/server.h +++ b/src/core/include/server.h @@ -27,5 +27,7 @@ private: void task(); atomic runListener; + + wstring password = L""; }; diff --git a/src/core/include/unit.h b/src/core/include/unit.h index 0f80a3ba..e54401ff 100644 --- a/src/core/include/unit.h +++ b/src/core/include/unit.h @@ -96,14 +96,13 @@ public: void pushActivePathBack(Coords newActivePathBack); void popActivePathFront(); void setTargetID(int newTargetID) { targetID = newTargetID; addMeasure(L"targetID", json::value(newTargetID));} - void setIsTanker(bool newIsTanker) { isTanker = newIsTanker; addMeasure(L"isTanker", json::value(newIsTanker));} - void setIsAWACS(bool newIsAWACS) { isAWACS = newIsAWACS; addMeasure(L"isAWACS", json::value(newIsAWACS));} - void setTACANOn(bool newTACANOn); + void setIsTanker(bool newIsTanker); + void setIsAWACS(bool newIsAWACS); void setTACANChannel(int newTACANChannel); void setTACANXY(wstring newTACANXY); void setTACANCallsign(wstring newTACANCallsign); void setTACAN(); - void setRadioOn(bool newRadioOn); + void setEPLRS(bool state); void setRadioFrequency(int newRadioFrequency); void setRadioCallsign(int newRadioCallsign); void setRadioCallsignNumber(int newRadioCallsignNumber); @@ -116,11 +115,9 @@ public: int getTargetID() { return targetID; } bool getIsTanker() { return isTanker; } bool getIsAWACS() { return isAWACS; } - bool getTACANOn() { return TACANOn; } int getTACANChannel() { return TACANChannel; } wstring getTACANXY() { return TACANXY; } wstring getTACANCallsign() { return TACANCallsign; } - bool getRadioOn() { return radioOn; } int getRadioFrequency() { return radioFrequency; } int getRadioCallsign() { return radioCallsign; } int getRadioCallsignNumber() { return radioCallsignNumber; } @@ -182,11 +179,9 @@ protected: int targetID = NULL; bool isTanker = false; bool isAWACS = false; - bool TACANOn = false; int TACANChannel = 40; wstring TACANXY = L"X"; wstring TACANCallsign = L"TKR"; - bool radioOn = false; int radioFrequency = 260000000; // MHz int radioCallsign = 1; int radioCallsignNumber = 1; diff --git a/src/core/src/scheduler.cpp b/src/core/src/scheduler.cpp index 2bd4c98c..8bf448e9 100644 --- a/src/core/src/scheduler.cpp +++ b/src/core/src/scheduler.cpp @@ -251,13 +251,11 @@ void Scheduler::handleRequest(wstring key, json::value value) unit->setIsTanker(value[L"isTanker"].as_bool()); unit->setIsAWACS(value[L"isAWACS"].as_bool()); - unit->setTACANOn(true); // TODO Remove unit->setTACANChannel(value[L"TACANChannel"].as_number().to_int32()); unit->setTACANXY(value[L"TACANXY"].as_string()); unit->setTACANCallsign(value[L"TACANCallsign"].as_string()); unit->setTACAN(); - unit->setRadioOn(true); // TODO Remove unit->setRadioFrequency(value[L"radioFrequency"].as_number().to_int32()); unit->setRadioCallsign(value[L"radioCallsign"].as_number().to_int32()); unit->setRadioCallsignNumber(value[L"radioCallsignNumber"].as_number().to_int32()); diff --git a/src/core/src/server.cpp b/src/core/src/server.cpp index 192a0440..a2b01978 100644 --- a/src/core/src/server.cpp +++ b/src/core/src/server.cpp @@ -6,9 +6,11 @@ #include "luatools.h" #include #include +#include "base64.hpp" #include using namespace std::chrono; +using namespace base64; extern UnitsManager* unitsManager; extern Scheduler* scheduler; @@ -54,10 +56,10 @@ void Server::stop(lua_State* L) void Server::handle_options(http_request request) { http_response response(status_codes::OK); - response.headers().add(U("Allow"), U("GET, POST, PUT, OPTIONS")); + response.headers().add(U("Allow"), U("GET, PUT, OPTIONS")); response.headers().add(U("Access-Control-Allow-Origin"), U("*")); - response.headers().add(U("Access-Control-Allow-Methods"), U("GET, POST, PUT, OPTIONS")); - response.headers().add(U("Access-Control-Allow-Headers"), U("Content-Type")); + response.headers().add(U("Access-Control-Allow-Methods"), U("GET, PUT, OPTIONS")); + response.headers().add(U("Access-Control-Allow-Headers"), U("Content-Type, Authorization")); request.reply(response); } @@ -68,87 +70,102 @@ void Server::handle_get(http_request request) lock_guard guard(mutexLock); http_response response(status_codes::OK); - response.headers().add(U("Allow"), U("GET, POST, PUT, OPTIONS")); - response.headers().add(U("Access-Control-Allow-Origin"), U("*")); - response.headers().add(U("Access-Control-Allow-Methods"), U("GET, POST, PUT, OPTIONS")); - response.headers().add(U("Access-Control-Allow-Headers"), U("Content-Type")); + string authorization = to_base64("admin:" + to_string(password)); + if (password == L"" || (request.headers().has(L"Authorization") && request.headers().find(L"Authorization")->second == L"Basic " + to_wstring(authorization))) + { + std::exception_ptr eptr; + try { + auto answer = json::value::object(); + auto path = uri::split_path(uri::decode(request.relative_uri().path())); - std::exception_ptr eptr; - try { - auto answer = json::value::object(); - auto path = uri::split_path(uri::decode(request.relative_uri().path())); - - if (path.size() > 0) - { - if (path[0] == UNITS_URI) + if (path.size() > 0) { - map query = request.relative_uri().split_query(request.relative_uri().query()); - long long time = 0; - if (query.find(L"time") != query.end()) + if (path[0] == UNITS_URI) { - try { - time = stoll((*(query.find(L"time"))).second); - } - catch (const std::exception& e) { - time = 0; + map query = request.relative_uri().split_query(request.relative_uri().query()); + long long time = 0; + if (query.find(L"time") != query.end()) + { + try { + time = stoll((*(query.find(L"time"))).second); + } + catch (const std::exception& e) { + time = 0; + } } + unitsManager->getData(answer, time); } - unitsManager->getData(answer, time); - } - else if (path[0] == LOGS_URI) - { - auto logs = json::value::object(); - getLogsJSON(logs, 100); // By reference, for thread safety. Get the last 100 log entries - answer[L"logs"] = logs; - } - else if (path[0] == AIRBASES_URI) - answer[L"airbases"] = airbases; - else if (path[0] == BULLSEYE_URI) - answer[L"bullseyes"] = bullseyes; - else if (path[0] == MISSION_URI) - answer[L"mission"] = mission; + else if (path[0] == LOGS_URI) + { + auto logs = json::value::object(); + getLogsJSON(logs, 100); // By reference, for thread safety. Get the last 100 log entries + answer[L"logs"] = logs; + } + else if (path[0] == AIRBASES_URI) + answer[L"airbases"] = airbases; + else if (path[0] == BULLSEYE_URI) + answer[L"bullseyes"] = bullseyes; + else if (path[0] == MISSION_URI) + answer[L"mission"] = mission; - milliseconds ms = duration_cast(system_clock::now().time_since_epoch()); - answer[L"time"] = json::value::string(to_wstring(ms.count())); - answer[L"sessionHash"] = json::value::string(to_wstring(sessionHash)); + milliseconds ms = duration_cast(system_clock::now().time_since_epoch()); + answer[L"time"] = json::value::string(to_wstring(ms.count())); + answer[L"sessionHash"] = json::value::string(to_wstring(sessionHash)); + } + + response.set_body(answer); } + catch (...) { + eptr = std::current_exception(); // capture + } + handle_eptr(eptr); + } + else { + response = status_codes::Unauthorized; + } - response.set_body(answer); - } - catch (...) { - eptr = std::current_exception(); // capture - } - handle_eptr(eptr); + response.headers().add(U("Allow"), U("GET, PUT, OPTIONS")); + response.headers().add(U("Access-Control-Allow-Origin"), U("*")); + response.headers().add(U("Access-Control-Allow-Methods"), U("GET, PUT, OPTIONS")); + response.headers().add(U("Access-Control-Allow-Headers"), U("Content-Type, Authorization")); request.reply(response); } void Server::handle_request(http_request request, function action) { - auto answer = json::value::object(); - request.extract_json().then([&answer, &action](pplx::task task) - { - try - { - auto const& jvalue = task.get(); - - if (!jvalue.is_null()) - { - action(jvalue, answer); - } - } - catch (http_exception const& e) - { - log(e.what()); - } - }).wait(); - http_response response(status_codes::OK); - response.headers().add(U("Allow"), U("GET, POST, PUT, OPTIONS")); + string authorization = to_base64("admin:" + to_string(password)); + if (password == L"" || (request.headers().has(L"Authorization") && request.headers().find(L"Authorization")->second == L"Basic " + to_wstring(authorization))) + { + auto answer = json::value::object(); + request.extract_json().then([&answer, &action](pplx::task task) + { + try + { + auto const& jvalue = task.get(); + + if (!jvalue.is_null()) + { + action(jvalue, answer); + } + } + catch (http_exception const& e) + { + log(e.what()); + } + }).wait(); + response.set_body(answer); + } + else { + response = status_codes::Unauthorized; + } + + response.headers().add(U("Allow"), U("GET, PUT, OPTIONS")); response.headers().add(U("Access-Control-Allow-Origin"), U("*")); - response.headers().add(U("Access-Control-Allow-Methods"), U("GET, POST, PUT, OPTIONS")); - response.headers().add(U("Access-Control-Allow-Headers"), U("Content-Type")); - response.set_body(answer); + response.headers().add(U("Access-Control-Allow-Methods"), U("GET, PUT, OPTIONS")); + response.headers().add(U("Access-Control-Allow-Headers"), U("Content-Type, Authorization")); + request.reply(response); } @@ -197,9 +214,15 @@ void Server::task() log(L"Starting server on " + address); } else - { log(L"Error reading configuration file. Starting server on " + address); + + if (config.is_object() && config.has_object_field(L"authentication") && + config[L"authentication"].has_string_field(L"password")) + { + password = config[L"authentication"][L"password"].as_string(); } + else + log(L"Error reading configuration file. No password set."); free(buf); } else diff --git a/src/core/src/unit.cpp b/src/core/src/unit.cpp index 228a26a9..5e9dd25d 100644 --- a/src/core/src/unit.cpp +++ b/src/core/src/unit.cpp @@ -337,9 +337,17 @@ void Unit::landAt(Coords loc) { setState(State::LAND); } -void Unit::setTACANOn(bool newTACANOn) { - TACANOn = newTACANOn; - addMeasure(L"TACANOn", json::value(newTACANOn)); +void Unit::setIsTanker(bool newIsTanker) { + isTanker = newIsTanker; + resetTask(); + addMeasure(L"isTanker", json::value(newIsTanker)); +} + +void Unit::setIsAWACS(bool newIsAWACS) { + isAWACS = newIsAWACS; + resetTask(); + addMeasure(L"isAWACS", json::value(newIsAWACS)); + setEPLRS(true); } void Unit::setTACANChannel(int newTACANChannel) { @@ -356,11 +364,6 @@ void Unit::setTACANCallsign(wstring newTACANCallsign) { addMeasure(L"TACANCallsign", json::value(newTACANCallsign)); } -void Unit::setRadioOn(bool newRadioOn) { - radioOn = newRadioOn; - addMeasure(L"radioOn", json::value(newRadioOn)); -} - void Unit::setRadioFrequency(int newRadioFrequency) { radioFrequency = newRadioFrequency; addMeasure(L"radioFrequency", json::value(newRadioFrequency)); @@ -376,6 +379,19 @@ void Unit::setRadioCallsignNumber(int newRadioCallsignNumber) { addMeasure(L"radioCallsignNumber", json::value(newRadioCallsignNumber)); } +void Unit::setEPLRS(bool state) +{ + std::wostringstream commandSS; + commandSS << "{" + << "id = 'EPLRS'," + << "params = {" + << "value = " << (state? "true": "false") << ", " + << "}" + << "}"; + Command* command = dynamic_cast(new SetCommand(ID, commandSS.str())); + scheduler->appendCommand(command); +} + void Unit::setTACAN() { std::wostringstream commandSS; @@ -383,9 +399,9 @@ void Unit::setTACAN() << "id = 'ActivateBeacon'," << "params = {" << "type = " << ((TACANXY.compare(L"X") == 0)? 4: 5) << "," - << "system = 4," - << "name = Olympus_TACAN," - << "callsign = " << TACANCallsign << ", " + << "system = 3," + << "name = \"Olympus_TACAN\"," + << "callsign = \"" << TACANCallsign << "\", " << "frequency = " << TACANChannelToFrequency(TACANChannel, TACANXY) << "," << "}" << "}"; diff --git a/third-party/base64/include/base64.hpp b/third-party/base64/include/base64.hpp new file mode 100644 index 00000000..adcf1c04 --- /dev/null +++ b/third-party/base64/include/base64.hpp @@ -0,0 +1,81 @@ +#ifndef BASE_64_HPP +#define BASE_64_HPP + +#include +#include + +namespace base64 { + +inline std::string get_base64_chars() { + static std::string base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + return base64_chars; +} + +inline std::string to_base64(std::string const &data) { + int counter = 0; + uint32_t bit_stream = 0; + const std::string base64_chars = get_base64_chars(); + std::string encoded; + int offset = 0; + for (unsigned char c : data) { + auto num_val = static_cast(c); + offset = 16 - counter % 3 * 8; + bit_stream += num_val << offset; + if (offset == 16) { + encoded += base64_chars.at(bit_stream >> 18 & 0x3f); + } + if (offset == 8) { + encoded += base64_chars.at(bit_stream >> 12 & 0x3f); + } + if (offset == 0 && counter != 3) { + encoded += base64_chars.at(bit_stream >> 6 & 0x3f); + encoded += base64_chars.at(bit_stream & 0x3f); + bit_stream = 0; + } + counter++; + } + if (offset == 16) { + encoded += base64_chars.at(bit_stream >> 12 & 0x3f); + encoded += "=="; + } + if (offset == 8) { + encoded += base64_chars.at(bit_stream >> 6 & 0x3f); + encoded += '='; + } + return encoded; +} + +inline std::string from_base64(std::string const &data) { + int counter = 0; + uint32_t bit_stream = 0; + std::string decoded; + int offset = 0; + const std::string base64_chars = get_base64_chars(); + for (unsigned char c : data) { + auto num_val = base64_chars.find(c); + if (num_val != std::string::npos) { + offset = 18 - counter % 4 * 6; + bit_stream += num_val << offset; + if (offset == 12) { + decoded += static_cast(bit_stream >> 16 & 0xff); + } + if (offset == 6) { + decoded += static_cast(bit_stream >> 8 & 0xff); + } + if (offset == 0 && counter != 4) { + decoded += static_cast(bit_stream & 0xff); + bit_stream = 0; + } + } else if (c != '=') { + return std::string(); + } + counter++; + } + return decoded; +} + +} + +#endif // BASE_64_HPP \ No newline at end of file