diff --git a/CHANGELOG.md b/CHANGELOG.md index 09655fd..5585f72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,6 @@ All notable changes to the "dcs-lua-runner" extension will be documented in this file. -Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. - ## [Unreleased] ### Added @@ -95,4 +93,14 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how ### Added - Integrate a detailed setup guide into extension. -- Automatically show setup guide on first activation. \ No newline at end of file +- Automatically show setup guide on first activation. + +## [1.2.0] - 2024-05-11 + +### Added +- Option to configure remote GUI environment address and port separately from mission environment for better HTTPS reverse proxy support. (PLEASE USE A REVERSE PROXY ON REMOTE SERVER FOR SECURITY!) + +### Changed +- Integrated Setup guide now includes instructions to help setting up a reverse proxy. +- Message displayed upon switching settings now includes protocol info (http/https). +- Move dcs-fiddle-server.lua script to an [external repository](https://github.com/omltcat/dcs-snippets/blob/master/Scripts/Hooks/dcs-fiddle-server.lua). \ No newline at end of file diff --git a/INSTALL.md b/INSTALL.md index daae94f..b6b121b 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -11,7 +11,7 @@ It is still compatible with the [DCS Fiddle website](https://dcsfiddle.pages.dev - Don't hesitate to report any problems, provide segguestions or request features using [**Issues**](https://github.com/omltcat/dcs-lua-runner/issues). ## Hook Script -1. Download [**dcs-fiddle-server.lua**](https://github.com/omltcat/dcs-lua-runner/blob/master/src/hooks/dcs-fiddle-server.lua). +1. Download [**dcs-fiddle-server.lua**](https://github.com/omltcat/dcs-snippets/blob/master/Scripts/Hooks/dcs-fiddle-server.lua). 2. Save to `%USERPROFILE%\Saved Games\\Scripts\Hooks`. - Generally this means `C:\Users\\Saved Games` - `` could be `DCS`, `DCS.openbeta`, `DCS.release_server`, etc. @@ -43,23 +43,45 @@ end ### Script Configuration If you want to run code on a remote DCS server, you need to expose the FIDDLE PORT to 0.0.0.0 by modifying the beginning of the `dcs-fiddle-server.lua` file. -It is highly recommended that you also set up the basic authentication, otherwise anyone can inject lua code into your server. For best security, also put the FIDDLE PORT behind a reverse proxy with HTTPS. + +It is highly recommended that you also set up the basic authentication, otherwise anyone can inject lua code into your server. For best security, also put the FIDDLE PORT behind a reverse proxy with HTTPS (see [below](#reverse-proxy)). ```lua --- Configs: FIDDLE.PORT = 12080 -- keep this at 12080 if you also want to use the DCS Fiddle website. -FIDDLE.BIND_IP = '0.0.0.0' -- for remote access +FIDDLE.BIND_IP = '0.0.0.0' -- Use '0.0.0.0' for remote access, default is '127.0.0.1' FIDDLE.AUTH = true -- set to true to enable basic auth, recommended for public servers. -FIDDLE.USERNAME = 'username' -FIDDLE.PASSWORD = 'password' +FIDDLE.USERNAME = 'username' -- set your username +FIDDLE.PASSWORD = 'password' -- set your password FIDDLE.BYPASS_LOCAL = true -- allow requests to 127.0.0.1:12080 without auth. ---- So DCS Fiddle website can still work. ---- (Not a very secure implementation. Use at your own risk if your 12080 port is public) +-- This local bypass allows DCS Fiddle website to still work. +-- It uses host header to determine if the request is local. +-- This is not the most secure method and can be spoofed, so use at your own risk. +-- Use a reverse proxy for best security. +``` +#### VS Code Settings +Open command pallette (`Ctrl`+`Shift`+`P`) and type `DCS Lua: Open Runner Settings`. +You need to set: +- Server Address: IP of the DCS server +- Web Auth Username: same as `FIDDLE.USERNAME` +- Web Auth Password: same as `FIDDLE.PASSWORD` + +### Reverse Proxy +The `dcs-fiddle-server.lua` script separates mission/GUI environment based on ports. By default the GUI env is automatically on the next port of mission env (12080-12081). When using a HTTPS reverse proxy, you can map the two ports to different subdomains, e.g.: + +``` +https://fiddle.example.com/ -> http://dcs-server-ip:12080/ +https://fiddle-gui.example.com/ -> http://dcs-server-ip:12081/ ``` -### VS Code Settings -1. In VS Code, open command pallette (`Ctrl`+`Shift`+`P`) and type `DCS Lua: Open Runner Settings`. -2. Set server address, port, username, and password accordingly. +In VS Code - DCS Lua Runner Settings, set the following accordingly: +- Server Address: `fiddle.example.com` +- Server Address GUI: `fiddle-gui.example.com` +- Server Port: `443` +- Server Port GUI: `443` +- Use Https: `true` +- Web Auth Username: same as `FIDDLE.USERNAME` +- Web Auth Password: same as `FIDDLE.PASSWORD` ## Credits -All credits of this scripts and its API implementations go to the original authors [JonathanTurnock](https://github.com/JonathanTurnock) and [john681611](https://github.com/john681611). +All credits of this scripts and its API implementations go to the original authors [JonathanTurnock](https://github.com/JonathanTurnock) and [john681611](https://github.com/john681611). +Under MIT License, see [dcsfiddle](https://github.com/JonathanTurnock/dcsfiddle?tab=MIT-1-ov-file). diff --git a/README.md b/README.md index 2f69264..fcfda4c 100644 --- a/README.md +++ b/README.md @@ -18,16 +18,20 @@ A VS Code extension to run lua code in DCS World (on local or remote server, in - in either JSON or Lua table format. ## Installation -See [**INSTALL.md**](INSTALL.md). - +See [**INSTALL.md**](INSTALL.md). +**YOU MUST SETUP THE DCS SCRIPT FOR THIS EXTENSION TO WORK.** ## Extension Settings This extension has the following settings: - `serverAddress`: Remote DCS server address. It can be an IP address or a domain. +- `serverAddressGUI`: Override remote DCS server address for GUI environment. + - `serverPort`: Port of the remote DCS Fiddle. Default is `12080`. +- `serverPortGUI`: Override port of the remote DCS Fiddle for GUI environment. + - `useHttps`: Specifies whether the server is behind a HTTPS reverse proxy. If this is set to `true`, you should also change the `dcsLuaRunner.serverPort` to `443`. Default is `false`. @@ -44,7 +48,6 @@ This setting can be quickly changed with the buttons on the upper-right of a lua - `runInMissionEnv`: Specifies whether to execute in mission or GUI Scripting Environment. This setting can be quickly changed with the buttons on the upper-right of a lua file. -**NEW:** - `returnDisplay`: Wether to use output panel or file (which supports syntax highlight) to display return value. - `returnDisplayFormat`: Display return value as JSON or Lua table. (Experimental feature, please report any issue.) @@ -55,7 +58,8 @@ See [**changelog**](CHANGELOG.md). ## Credits -All credits of this scripts and its API implementations go to the original authors [JonathanTurnock](https://github.com/JonathanTurnock) and [john681611](https://github.com/john681611). +All credits of this scripts and its API implementations go to the original authors [JonathanTurnock](https://github.com/JonathanTurnock) and [john681611](https://github.com/john681611). +Under MIT License, see [dcsfiddle](https://github.com/JonathanTurnock/dcsfiddle?tab=MIT-1-ov-file). ## License diff --git a/package.json b/package.json index 84030c0..0aa0ed4 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Quickly run lua code in DCS World (local or remote server). A reimplementation of the DCS Fiddle lua console in VS Code.", "license": "MIT", "publisher": "omltcat", - "version": "1.1.7", + "version": "1.2", "icon": "docs/img/icon.png", "repository": { "type": "git", @@ -27,17 +27,26 @@ "dcsLuaRunner.serverAddress": { "type": "string", "default": "", - "description": "Remote DCS server address (IP or domain)." + "description": "Remote DCS server address (IP or domain). See setup guide if you are using a HTTPS reverse proxy." + }, + "dcsLuaRunner.serverAddressGUI": { + "type": "string", + "description": "Override remote GUI env address. For example, you can put both endpoints behind a HTTPS reverse proxy with different subdomains. Their ports should both be 443." }, "dcsLuaRunner.serverPort": { "type": "number", "default": 12080, "description": "Remote DCS Fiddle port for mission env. GUI env will automatically be on the next port (12081 by default)." }, + "dcsLuaRunner.serverPortGUI": { + "type": "number", + "default": 12081, + "description": "Overide remote GUI env port. For example, you can put both endpoints behind a HTTPS reverse proxy with different subdomains. Their ports should both be 443." + }, "dcsLuaRunner.useHttps": { "type": "boolean", "default": false, - "description": "Set if server is behind a HTTPS reverse proxy. You should also change server port accordingly (443)." + "description": "Set if the remote server is behind a HTTPS reverse proxy. You should also change both ports accordingly (443)." }, "dcsLuaRunner.webAuthUsername": { "type": "string", diff --git a/src/extension.ts b/src/extension.ts index 3bd9bda..05cfb82 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -10,8 +10,12 @@ async function runLua(lua: string, outputChannel: vscode.OutputChannel, filename const returnDisplayFormat = config.get('returnDisplayFormat') === 'JSON' ? 'json' : 'lua' as string; const runCodeLocally = config.get('runCodeLocally') as boolean; const runInMissionEnv = config.get('runInMissionEnv') as boolean; - const serverAddress = runCodeLocally ? '127.0.0.1' : config.get('serverAddress') as string; - const serverPort = (runCodeLocally ? 12080 : config.get('serverPort') as number) + (runInMissionEnv ? 0 : 1); + const serverAddressMission = runCodeLocally ? '127.0.0.1' : config.get('serverAddress') as string; + const serverAddressGUI = (runCodeLocally || !config.get('serverAddressGUI')) ? serverAddressMission : config.get('serverAddressGUI') as string; + const serverAddress = runInMissionEnv ? serverAddressMission : serverAddressGUI; + const serverPortMission = runCodeLocally ? 12080 : config.get('serverPort') as number + const serverPortGUI = (runCodeLocally || !config.get('serverPortGUI')) ? (serverPortMission + 1) : config.get('serverPortGUI') as number; + const serverPort = runInMissionEnv ? serverPortMission : serverPortGUI; const useHttps = runCodeLocally ? false : config.get('useHttps') as boolean; const authUsername = config.get('webAuthUsername') as string; const authPassword = config.get('webAuthPassword') as string; @@ -162,21 +166,26 @@ export function activate(context: vscode.ExtensionContext) { const config = vscode.workspace.getConfiguration('dcsLuaRunner'); const runCodeLocally = config.get('runCodeLocally') as boolean; const runInMissionEnv = config.get('runInMissionEnv') as boolean; - const runTarget = runCodeLocally ? 'local machine' : 'remote server'; const runEnv = runInMissionEnv ? 'mission' : 'GUI'; - const serverAddress = runCodeLocally ? '127.0.0.1' : config.get('serverAddress') as string; - const serverPort = (runCodeLocally ? 12080 : config.get('serverPort') as number) + (runInMissionEnv ? 0 : 1); + const serverAddressMission = runCodeLocally ? '127.0.0.1' : config.get('serverAddress') as string; + const serverAddressGUI = (runCodeLocally || !config.get('serverAddressGUI')) ? serverAddressMission : config.get('serverAddressGUI') as string; + const serverAddress = runInMissionEnv ? serverAddressMission : serverAddressGUI; + const serverPortMission = runCodeLocally ? 12080 : config.get('serverPort') as number + const serverPortGUI = (runCodeLocally || !config.get('serverPortGUI')) ? (serverPortMission + 1) : config.get('serverPortGUI') as number; + const serverPort = runInMissionEnv ? serverPortMission : serverPortGUI; + const useHttps = runCodeLocally ? false : config.get('useHttps') as boolean; + const protocol = useHttps ? 'https' : 'http'; if (config.get('returnDisplay') === 'Console Output') { outputChannel.show(true); - outputChannel.appendLine(`[DCS] Settings: Run code in ${runEnv} environment on ${runTarget} (${serverAddress}:${serverPort}).`); + outputChannel.appendLine(`[DCS] Settings: Run code in ${runEnv} environment on ${protocol}://${serverAddress}:${serverPort}.`); } else { - vscode.window.showInformationMessage(`Run code in ${runEnv} environment on ${serverAddress}:${serverPort}`); + vscode.window.showInformationMessage(`Run code in ${runEnv} environment on ${protocol}://${serverAddress}:${serverPort}`); } }; const updateSetting = async (setting: string, targetState: boolean) => { const config = vscode.workspace.getConfiguration('dcsLuaRunner'); - if (setting === 'runCodeLocally' && targetState === false && config.get('serverAddress') === '') { + if (setting === 'runCodeLocally' && targetState === false && !config.get('serverAddress')) { vscode.window.showErrorMessage('Remote DCS server address not set.', 'Open Settings').then((choice) => { if (choice === 'Open Settings') { vscode.commands.executeCommand('workbench.action.openSettings', 'dcsLuaRunner'); diff --git a/src/hooks/dcs-fiddle-server.lua b/src/hooks/dcs-fiddle-server.lua deleted file mode 100644 index 07ece5c..0000000 --- a/src/hooks/dcs-fiddle-server.lua +++ /dev/null @@ -1,1066 +0,0 @@ --- Note: This is a modified version of the DCS Fiddle Server to add authentication for public servers. --- All credit goes to the original authors of DCS Fiddle: JonathanTurnock and john681611 - -FIDDLE = {} - --- Configs: -FIDDLE.PORT = 12080 -- keep this at 12080 if you also want to use the DCS Fiddle website. --- FIDDLE.BIND_IP = '0.0.0.0' -- for remote access -FIDDLE.AUTH = true -- set to true to enable basic auth, recommended for public servers. -FIDDLE.USERNAME = 'username' -FIDDLE.PASSWORD = 'password' -FIDDLE.BYPASS_LOCAL = true -- allow requests to 127.0.0.1:12080 without auth, so DCS Fiddle website can still work. (Not a very secure implementation. Use at your own risk if your 12080 port is public) ---[[ -Custom JSON Serialization Module that handles DCS' usage of integers as table keys such as {[1]=1, ["name"]="Enfield11", [2]=1, [3]=1} which is not valid json - -This is handled by encoding tables with mixed key types as objects so the above example would be - -{ - "_1": 1, - "_2": 1, - "name": "Enfield11", - "_3": 1 -} - -Anything that is clearly a table is still converted to a table - -]]-- - -fiddlejson = { _version = "0.2.0" } - -------------------------------------------------------------------------------- --- Encode -------------------------------------------------------------------------------- - -local encode - -local escape_char_map = { - [ "\\" ] = "\\", - [ "\"" ] = "\"", - [ "\b" ] = "b", - [ "\f" ] = "f", - [ "\n" ] = "n", - [ "\r" ] = "r", - [ "\t" ] = "t", -} - -local escape_char_map_inv = { [ "/" ] = "/" } -for k, v in pairs(escape_char_map) do - escape_char_map_inv[v] = k -end - - -local function escape_char(c) - return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) -end - - -local function encode_nil(val) - return "null" -end - ---- Checks if a given table is an array. --- The function verifies that all keys are numeric and sequential starting from 1. --- @param val Table to be checked. --- @return boolean Returns true if the table is an array, otherwise false. -local function is_table_array(val) - -- Check if the first element of the table is not nil or the table is empty. - -- These are two signs that the table might be an array. - if rawget(val, 1) ~= nil or next(val) == nil then - -- Extract all numeric keys and sort them. - local keys = {} - for k in pairs(val) do - if type(k) == "number" then - table.insert(keys, k) - else - -- If any key is not a number, the table is not an array. - return false - end - end - table.sort(keys) - - -- Check the sorted keys to see if they form a continuous sequence starting from 1. - for i, k in ipairs(keys) do - if i ~= k then - return false - end - end - - -- If all keys were numeric and in sequence, then the table is an array. - return true - else - -- If the first element of the table is nil and the table isn't empty, - -- then it's not an array. - return false - end -end - - -local function encode_table(val, stack) - local res = {} - stack = stack or {} - - -- Circular reference? - if stack[val] then error("circular reference") end - - stack[val] = true - - - - if is_table_array(val) then - -- Treat as array -- check keys are valid and it is not sparse - -- Encode - for i, v in ipairs(val) do - table.insert(res, encode(v, stack)) - end - stack[val] = nil - return "[" .. table.concat(res, ",") .. "]" - - else - -- Treat as an object - for k, v in pairs(val) do - if type(k) ~= "string" then - table.insert(res, encode("_"..k, stack) .. ":" .. encode(v, stack)) - else - table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) - end - end - stack[val] = nil - return "{" .. table.concat(res, ",") .. "}" - end -end - - -local function encode_string(val) - return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' -end - - -local function encode_number(val) - -- Check for NaN, -inf and inf - if val ~= val or val <= -math.huge or val >= math.huge then - error("unexpected number value '" .. tostring(val) .. "'") - end - return string.format("%.14g", val) -end - - -local type_func_map = { - [ "nil" ] = encode_nil, - [ "table" ] = encode_table, - [ "string" ] = encode_string, - [ "number" ] = encode_number, - [ "boolean" ] = tostring, -} - - -encode = function(val, stack) - local t = type(val) - local f = type_func_map[t] - if f then - return f(val, stack) - end - error("unexpected type '" .. t .. "'") -end - - -function fiddlejson.encode(val) - return ( encode(val) ) -end - - -------------------------------------------------------------------------------- --- Decode -------------------------------------------------------------------------------- - -local parse - -local function create_set(...) - local res = {} - for i = 1, select("#", ...) do - res[ select(i, ...) ] = true - end - return res -end - -local space_chars = create_set(" ", "\t", "\r", "\n") -local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") -local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") -local literals = create_set("true", "false", "null") - -local literal_map = { - [ "true" ] = true, - [ "false" ] = false, - [ "null" ] = nil, -} - - -local function next_char(str, idx, set, negate) - for i = idx, #str do - if set[str:sub(i, i)] ~= negate then - return i - end - end - return #str + 1 -end - - -local function decode_error(str, idx, msg) - local line_count = 1 - local col_count = 1 - for i = 1, idx - 1 do - col_count = col_count + 1 - if str:sub(i, i) == "\n" then - line_count = line_count + 1 - col_count = 1 - end - end - error( string.format("%s at line %d col %d", msg, line_count, col_count) ) -end - - -local function codepoint_to_utf8(n) - -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa - local f = math.floor - if n <= 0x7f then - return string.char(n) - elseif n <= 0x7ff then - return string.char(f(n / 64) + 192, n % 64 + 128) - elseif n <= 0xffff then - return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) - elseif n <= 0x10ffff then - return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, - f(n % 4096 / 64) + 128, n % 64 + 128) - end - error( string.format("invalid unicode codepoint '%x'", n) ) -end - - -local function parse_unicode_escape(s) - local n1 = tonumber( s:sub(1, 4), 16 ) - local n2 = tonumber( s:sub(7, 10), 16 ) - -- Surrogate pair? - if n2 then - return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) - else - return codepoint_to_utf8(n1) - end -end - - -local function parse_string(str, i) - local res = "" - local j = i + 1 - local k = j - - while j <= #str do - local x = str:byte(j) - - if x < 32 then - decode_error(str, j, "control character in string") - - elseif x == 92 then -- `\`: Escape - res = res .. str:sub(k, j - 1) - j = j + 1 - local c = str:sub(j, j) - if c == "u" then - local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) - or str:match("^%x%x%x%x", j + 1) - or decode_error(str, j - 1, "invalid unicode escape in string") - res = res .. parse_unicode_escape(hex) - j = j + #hex - else - if not escape_chars[c] then - decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") - end - res = res .. escape_char_map_inv[c] - end - k = j + 1 - - elseif x == 34 then -- `"`: End of string - res = res .. str:sub(k, j - 1) - return res, j + 1 - end - - j = j + 1 - end - - decode_error(str, i, "expected closing quote for string") -end - - -local function parse_number(str, i) - local x = next_char(str, i, delim_chars) - local s = str:sub(i, x - 1) - local n = tonumber(s) - if not n then - decode_error(str, i, "invalid number '" .. s .. "'") - end - return n, x -end - - -local function parse_literal(str, i) - local x = next_char(str, i, delim_chars) - local word = str:sub(i, x - 1) - if not literals[word] then - decode_error(str, i, "invalid literal '" .. word .. "'") - end - return literal_map[word], x -end - - -local function parse_array(str, i) - local res = {} - local n = 1 - i = i + 1 - while 1 do - local x - i = next_char(str, i, space_chars, true) - -- Empty / end of array? - if str:sub(i, i) == "]" then - i = i + 1 - break - end - -- Read token - x, i = parse(str, i) - res[n] = x - n = n + 1 - -- Next token - i = next_char(str, i, space_chars, true) - local chr = str:sub(i, i) - i = i + 1 - if chr == "]" then break end - if chr ~= "," then decode_error(str, i, "expected ']' or ','") end - end - return res, i -end - -function set_table(res, key, val) - -- Check if the key starts with an underscore - if string.sub(key, 1, 1) == "_" then - -- Remove the underscore and try to convert the rest to a number - local numKey = tonumber(string.sub(key, 2)) - - -- If the result is not nil, meaning the conversion was successful - if numKey then - -- Update the table with the number key instead of the string key - res[numKey] = val - end - else - -- If the key does not start with an underscore, just use it as is - res[key] = val - end -end - -local function parse_object(str, i) - local res = {} - i = i + 1 - while 1 do - local key, val - i = next_char(str, i, space_chars, true) - -- Empty / end of object? - if str:sub(i, i) == "}" then - i = i + 1 - break - end - -- Read key - if str:sub(i, i) ~= '"' then - decode_error(str, i, "expected string for key") - end - key, i = parse(str, i) - -- Read ':' delimiter - i = next_char(str, i, space_chars, true) - if str:sub(i, i) ~= ":" then - decode_error(str, i, "expected ':' after key") - end - i = next_char(str, i + 1, space_chars, true) - -- Read value - val, i = parse(str, i) - -- Set - --res[key] = val - set_table(res, key, val) - -- Next token - i = next_char(str, i, space_chars, true) - local chr = str:sub(i, i) - i = i + 1 - if chr == "}" then break end - if chr ~= "," then decode_error(str, i, "expected '}' or ','") end - end - return res, i -end - - -local char_func_map = { - [ '"' ] = parse_string, - [ "0" ] = parse_number, - [ "1" ] = parse_number, - [ "2" ] = parse_number, - [ "3" ] = parse_number, - [ "4" ] = parse_number, - [ "5" ] = parse_number, - [ "6" ] = parse_number, - [ "7" ] = parse_number, - [ "8" ] = parse_number, - [ "9" ] = parse_number, - [ "-" ] = parse_number, - [ "t" ] = parse_literal, - [ "f" ] = parse_literal, - [ "n" ] = parse_literal, - [ "[" ] = parse_array, - [ "{" ] = parse_object, -} - - -parse = function(str, idx) - local chr = str:sub(idx, idx) - local f = char_func_map[chr] - if f then - return f(str, idx) - end - decode_error(str, idx, "unexpected character '" .. chr .. "'") -end - - -function fiddlejson.decode(str) - if type(str) ~= "string" then - error("expected argument of type string, got " .. type(str)) - end - local res, idx = parse(str, next_char(str, 1, space_chars, true)) - idx = next_char(str, idx, space_chars, true) - if idx <= #str then - decode_error(str, idx, "trailing garbage") - end - return res -end - ---[[ - base64 -- v1.5.3 public domain Lua base64 encoder/decoder - no warranty implied; use at your own risk - Needs bit32.extract function. If not present it's implemented using BitOp - or Lua 5.3 native bit operators. For Lua 5.1 fallbacks to pure Lua - implementation inspired by Rici Lake's post: - http://ricilake.blogspot.co.uk/2007/10/iterating-bits-in-lua.html - author: Ilya Kolbin (iskolbin@gmail.com) - url: github.com/iskolbin/lbase64 - COMPATIBILITY - Lua 5.1+, LuaJIT - LICENSE - See end of file for license information. ---]] - -local base64 = {} - -local extract = _G.bit32 and _G.bit32.extract -- Lua 5.2/Lua 5.3 in compatibility mode -if not extract then - if _G.bit then - -- LuaJIT - local shl, shr, band = _G.bit.lshift, _G.bit.rshift, _G.bit.band - extract = function(v, from, width) - return band(shr(v, from), shl(1, width) - 1) - end - elseif _G._VERSION == "Lua 5.1" then - extract = function(v, from, width) - local w = 0 - local flag = 2 ^ from - for i = 0, width - 1 do - local flag2 = flag + flag - if v % flag2 >= flag then - w = w + 2 ^ i - end - flag = flag2 - end - return w - end - else - -- Lua 5.3+ - extract = load [[return function( v, from, width ) - return ( v >> from ) & ((1 << width) - 1) - end]]() - end -end - -function base64.makeencoder(s62, s63, spad) - local encoder = {} - for b64code, char in pairs { [0] = 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', - 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', - 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', - 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', - '3', '4', '5', '6', '7', '8', '9', s62 or '+', s63 or '/', spad or '=' } do - encoder[b64code] = char:byte() - end - return encoder -end - -function base64.makedecoder(s62, s63, spad) - local decoder = {} - for b64code, charcode in pairs(base64.makeencoder(s62, s63, spad)) do - decoder[charcode] = b64code - end - return decoder -end - -local DEFAULT_ENCODER = base64.makeencoder() -local DEFAULT_DECODER = base64.makedecoder() - -local char, concat = string.char, table.concat - -function base64.encode(str, encoder, usecaching) - encoder = encoder or DEFAULT_ENCODER - local t, k, n = {}, 1, #str - local lastn = n % 3 - local cache = {} - for i = 1, n - lastn, 3 do - local a, b, c = str:byte(i, i + 2) - local v = a * 0x10000 + b * 0x100 + c - local s - if usecaching then - s = cache[v] - if not s then - s = char(encoder[extract(v, 18, 6)], encoder[extract(v, 12, 6)], encoder[extract(v, 6, 6)], encoder[extract(v, 0, 6)]) - cache[v] = s - end - else - s = char(encoder[extract(v, 18, 6)], encoder[extract(v, 12, 6)], encoder[extract(v, 6, 6)], encoder[extract(v, 0, 6)]) - end - t[k] = s - k = k + 1 - end - if lastn == 2 then - local a, b = str:byte(n - 1, n) - local v = a * 0x10000 + b * 0x100 - t[k] = char(encoder[extract(v, 18, 6)], encoder[extract(v, 12, 6)], encoder[extract(v, 6, 6)], encoder[64]) - elseif lastn == 1 then - local v = str:byte(n) * 0x10000 - t[k] = char(encoder[extract(v, 18, 6)], encoder[extract(v, 12, 6)], encoder[64], encoder[64]) - end - return concat(t) -end - -function base64.decode(b64, decoder, usecaching) - decoder = decoder or DEFAULT_DECODER - local pattern = '[^%w%+%/%=]' - if decoder then - local s62, s63 - for charcode, b64code in pairs(decoder) do - if b64code == 62 then - s62 = charcode - elseif b64code == 63 then - s63 = charcode - end - end - pattern = ('[^%%w%%%s%%%s%%=]'):format(char(s62), char(s63)) - end - b64 = b64:gsub(pattern, '') - local cache = usecaching and {} - local t, k = {}, 1 - local n = #b64 - local padding = b64:sub(-2) == '==' and 2 or b64:sub(-1) == '=' and 1 or 0 - for i = 1, padding > 0 and n - 4 or n, 4 do - local a, b, c, d = b64:byte(i, i + 3) - local s - if usecaching then - local v0 = a * 0x1000000 + b * 0x10000 + c * 0x100 + d - s = cache[v0] - if not s then - local v = decoder[a] * 0x40000 + decoder[b] * 0x1000 + decoder[c] * 0x40 + decoder[d] - s = char(extract(v, 16, 8), extract(v, 8, 8), extract(v, 0, 8)) - cache[v0] = s - end - else - local v = decoder[a] * 0x40000 + decoder[b] * 0x1000 + decoder[c] * 0x40 + decoder[d] - s = char(extract(v, 16, 8), extract(v, 8, 8), extract(v, 0, 8)) - end - t[k] = s - k = k + 1 - end - if padding == 1 then - local a, b, c = b64:byte(n - 3, n - 1) - local v = decoder[a] * 0x40000 + decoder[b] * 0x1000 + decoder[c] * 0x40 - t[k] = char(extract(v, 16, 8), extract(v, 8, 8)) - elseif padding == 2 then - local a, b = b64:byte(n - 3, n - 2) - local v = decoder[a] * 0x40000 + decoder[b] * 0x1000 - t[k] = char(extract(v, 16, 8)) - end - return concat(t) -end - ---[[ ------------------------------------------------------------------------------- -This software is available under 2 licenses -- choose whichever you prefer. ------------------------------------------------------------------------------- -ALTERNATIVE A - MIT License -Copyright (c) 2018 Ilya Kolbin -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. ------------------------------------------------------------------------------- -ALTERNATIVE B - Public Domain (www.unlicense.org) -This is free and unencumbered software released into the public domain. -Anyone is free to copy, modify, publish, use, compile, sell, or distribute this -software, either in source code form or as a compiled binary, for any purpose, -commercial or non-commercial, and by any means. -In jurisdictions that recognize copyright laws, the author or authors of this -software dedicate any and all copyright interest in the software to the public -domain. We make this dedication for the benefit of the public at large and to -the detriment of our heirs and successors. We intend this dedication to be an -overt act of relinquishment in perpetuity of all present and future rights to -this software under copyright law. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------------------------------------------------------------------------------- ---]] - -local function dumpt(t) - if type(t) == 'table' then - local s = '{ ' - for k, v in pairs(t) do - if type(k) ~= 'number' then - k = '"' .. k .. '"' - end - s = s .. '[' .. k .. '] = ' .. dumpt(v) .. ',' - end - return s .. '} ' - else - return tostring(t) - end -end - - ------------------------------------------------------------------------------------------------------------------------- ---- Logger ------------------------------------------------------------------------------------------------------------------------- - -local __debug = function(message) - local message = '[dcs-fiddle-server] - ' .. message - if (log and log.debug) then - log.debug(message) - else - print('DEBUG - ' .. message) - end -end - -local __info = function(message) - local message = '[dcs-fiddle-server] - ' .. message - if (log and log.info) then - log.info(message) - else - print('INFO - ' .. message) - end -end - -local __error = function(message) - local message = '[dcs-fiddle-server] - ' .. message - if (log and log.error) then - log.error(message) - else - print('ERROR - ' .. message) - end -end - ------------------------------------------------------------------------------------------------------------------------- ---- DCS Instruction Handler ------------------------------------------------------------------------------------------------------------------------- - -local IS_DCS = false - ------------------------------------------------------------------------------------------------------------------------- ---- Takes a LUA string, executes it and returns the result as a JSON string ----@param env string Environment to run the lua string within ---- -local function handle_request(luastring, env) - __info("[handle_request] - Handling request to execute string in " .. env) - - if (env ~= "default") then - __info("[handle_request] - Executing string via dostring_in") - local str, err = net.dostring_in(env, luastring) - if (err) then - __error(string.format("Error while executing string in %s\n%s", env, str)) - end - return str - else - __info("[handle_request] - Loading LUA String...") - local loaded = assert(loadstring(luastring)) - - __info("[handle_request] - Executing LUA String...") - local result = loaded() - - __info("[handle_request] - Processing result...") - return result - end -end - ------------------------------------------------------------------------------------------------------------------------- ---- Url ---- https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_is_a_URL ------------------------------------------------------------------------------------------------------------------------- - ------------------------------------------------------------------------------------------------------------------------- ---- Parses the given url and returns a URL table ---- ---- `{ parameters={sort="asc", size=20}, path="/employees" }` ---- ---- @param original_url string - The Original Request URL i.e. `/employees?sort=asc&size=20` ---- @return string, table Returns the path part alongside a table of parsed parameters ---- -local function parse_url(original_url) - local resource_path, parameters = original_url:match('(.+)?(.*)') - if (parameters) then - local params = {} - for parameter in string.gmatch(parameters, "[^&]+") do - local name, value = parameter:match('(.+)=(.+)') - params[name] = value - end - - return resource_path, params - end - return original_url -end - ------------------------------------------------------------------------------------------------------------------------- ---- HTTP Receiver ------------------------------------------------------------------------------------------------------------------------- - ------------------------------------------------------------------------------------------------------------------------- ---- Reads HTTP Message from the given connection ---- ---- @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages ---- @see https://lunarmodules.github.io/luasocket/tcp.html#accept ---- @param client socket.client LUA Socket Lib Client ---- @return table, number Request table containing method, original_url, protocol, path, parameters, body, headers. Optionally returns a second item representing an error_code from the match_headers failing -local function receive_http(client) - local request = {} - __debug("receiving start-line") - local received, err = client:receive("*l") - - if (err) then - __error("Failed to get start-line due to " .. err) - return - end - - __debug("parsing start-line") - local method, original_url, protocol = string.match(received, "(%S+) (%S+) (%S+)") - request.method = method - request.original_url = original_url - request.protocol = protocol - - __debug("parsing url") - local path, parameters = parse_url(original_url) - request.path = path - request.parameters = parameters - - request.authOK = false - if FIDDLE.AUTH then - __debug("parsing headers for auth") - local headers = {} - while true do - local line = client:receive() - if not line or line == "" then break end - local name, value = line:match("^([^:]+):%s*(.*)$") - headers[name] = value - end - local auth_header = headers["Authorization"] - local host_header = headers['X-Forwarded-Host'] or headers["Host"] - if FIDDLE.BYPASS_LOCAL and (host_header == '127.0.0.1:12080' or host_header == '127.0.0.1:12081') then - request.authOK = true - elseif auth_header then - local encoded_username_password = auth_header:match("^Basic%s+(.*)$") - if encoded_username_password then - local decoded_username_password = base64.decode(encoded_username_password) - local username, password = decoded_username_password:match("^(.*):(.*)$") - -- __info(decoded_username_password) - if username == FIDDLE.USERNAME and password == FIDDLE.PASSWORD then - -- __info("Auth Passed") - request.authOK = true - end - end - end - else - request.authOK = true - end - - __debug("request completed") - return request -end - ------------------------------------------------------------------------------------------------------------------------- ---- HTTP Sender ------------------------------------------------------------------------------------------------------------------------- - -local EMPTY_LINE = "" -local CRLF = "\r\n" - -local status_text = { - [100] = "Continue", - [101] = "Switching protocols", - [102] = "Processing", - [103] = "Early Hints", - [200] = "OK", - [201] = "Created", - [202] = "Accepted", - [203] = "Non-Authoritative Information", - [204] = "No Content", - [205] = "Reset Content", - [206] = "Partial Content", - [207] = "Multi-Status", - [208] = "Already Reported", - [226] = "IM Used", - [300] = "Multiple Choices", - [301] = "Moved Permanently", - [302] = "Found (Previously \"Moved Temporarily\")", - [303] = "See Other", - [304] = "Not Modified", - [305] = "Use Proxy", - [306] = "Switch Proxy", - [307] = "Temporary Redirect", - [308] = "Permanent Redirect", - [400] = "Bad Request", - [401] = "Unauthorized", - [402] = "Payment Required", - [403] = "Forbidden", - [404] = "Not Found", - [405] = "Method Not Allowed", - [406] = "Not Acceptable", - [407] = "Proxy Authentication Required", - [408] = "Request Timeout", - [409] = "Conflict", - [410] = "Gone", - [411] = "Length Required", - [412] = "Precondition Failed", - [413] = "Payload Too Large", - [414] = "URI Too Long", - [415] = "Unsupported Media Type", - [416] = "Range Not Satisfiable", - [417] = "Expectation Failed", - [418] = "I'm a Teapot", - [421] = "Misdirected Request", - [422] = "Unprocessable Entity", - [423] = "Locked", - [424] = "Failed Dependency", - [425] = "Too Early", - [426] = "Upgrade Required", - [428] = "Precondition Required", - [429] = "Too Many Requests", - [431] = "Request Header Fields Too Large", - [451] = "Unavailable For Legal Reasons", - [500] = "Internal Server Error", - [501] = "Not Implemented", - [502] = "Bad Gateway", - [503] = "Service Unavailable", - [504] = "Gateway Timeout", - [505] = "HTTP Version Not Supported", - [506] = "Variant Also Negotiates", - [507] = "Insufficient Storage", - [508] = "Loop Detected", - [510] = "Not Extended", - [511] = "Network Authentication Required" -} - ------------------------------------------------------------------------------------------------------------------------- ---- Writes HTTP Message to the given connection using the given response object ---- ---- @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages ---- @param client client @see https://lunarmodules.github.io/luasocket/tcp.html ---- @param response table response table containing 'status' and 'body' -local function send_http(client, response) - local start_line = table.concat({ "HTTP/1.1", response.status, status_text[response.status] }, " ") - - local headers = { "Server: DCS Fiddle Server HTTP/1.1" } - - for name, value in pairs(response.headers) do - table.insert(headers, name .. ": " .. value) - end - - local response_string - if (response.body) then - response_string = table.concat({ start_line, table.concat(headers, CRLF), EMPTY_LINE, response.body }, CRLF) - else - response_string = table.concat({ start_line, table.concat(headers, CRLF), EMPTY_LINE, EMPTY_LINE }, CRLF) - end - - __info("Sending HTTP Response") - --__debug(">> " .. response_string) - local index, err = client:send(response_string) - if (err) then - __error("Failed to fully send due to: " .. err) - else - __info("Successfully sent response") - end -end - ------------------------------------------------------------------------------------------------------------------------- ---- HTTP Server ------------------------------------------------------------------------------------------------------------------------- -if (not require or not package) then - if (env and env.error) then - env.error("DCS Fiddle failed to inject into the mission scripting environment as require or package was not found.\n\nPlease follow the installation docs to de-sanitize the mission scripting environment\nhttps://dcsfiddle.pages.dev/docs", true) - return - end -end - -package.path = package.path .. ";.\\LuaSocket\\?.lua" -package.cpath = package.cpath .. ";.\\LuaSocket\\?.dll" - -local socket = require("socket") - -local clients = {} -local tcp_server - -local server_config = { cors = "*" } - -local client_id_seq = 1 - -local OK = 200 - -local BAD_REQUEST = 400 - -local INTERNAL_SERVER_ERROR = 500 -local METHOD_NOT_ALLOWED = 405 -local UNAUTHORIZED = 401 - ------------------------------------------------------------------------------------------------------------------------ ---- Gets and returns a client id incrementing the sequence -local function get_client_id() - local id = client_id_seq - client_id_seq = client_id_seq + 1 - return id -end - -local function handle_client_connection(client) - -- Dictionary of Headers that need to match, failure to match fails the read operation and returns the error code - local response = { status = INTERNAL_SERVER_ERROR, headers = { ["Content-Type"] = "application/json"} } - - local request = receive_http(client) - - if (request) then - if (request.method ~= "GET") then - response.status = METHOD_NOT_ALLOWED - elseif not request.authOK then - __info("Request Unauthorized") - response.status = UNAUTHORIZED - else - __info("Handling Request") - local success, res = pcall(base64.decode, string.sub(request.path, 2)) - if (not success) then - __error("Failed to read input due to " .. res) - response.status = BAD_REQUEST - else - local env = request.parameters and request.parameters.env - __info("Processing Command " .. res) - local success, res = pcall(handle_request, res, env) - if (not success) then - __error("Failed to handle request due to \n" .. res) - response.body = fiddlejson.encode({error=tostring(res)}) - response.status = INTERNAL_SERVER_ERROR - else - __info("Handled request") - response.body = fiddlejson.encode({result=res}) - response.status = OK - end - end - end - end - - if (server_config.cors) then - response.headers["Access-Control-Allow-Origin"] = server_config.cors - end - - send_http(client, response) - - __info("Connection Completed") - client:close() -end - - -local function create_server(address, port) - tcp_server = socket.bind(address, port) - tcp_server:settimeout(0) -- Make non blocking - - if not tcp_server then - __error("Could not bind socket.") - end - - local ip, port = tcp_server:getsockname() - - __info("HTTP Server running on " .. ip .. ":" .. port) - - --- Returns function which when called will perform 1 server loop - --- Note this impl only allows 1 request to be handled at a time - return function() - local client = tcp_server:accept() - if (client) then - local success, res = handle_client_connection(client) - if (not success) then - __error("Failed to run client handler " .. res) - else - clients[id].receive_patten = res - end - end - end -end - ------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------- MAIN --------------------------------------------------------------- - -__info("Checking the DCS environment...") - -local isMission = not DCS -local port = FIDDLE.PORT or 12080 -local bind_ip = FIDDLE.BIND_IP or '127.0.0.1' - -if (isMission) then - __info("Starting fiddle server in the mission scripting environment...") - local loop = create_server(bind_ip, port) - - timer.scheduleFunction(function(arg, time) - local success, err = pcall(loop) - if not success then - __info("loop() error: " .. tostring(err)) - end - return timer.getTime() + .1 - end, nil, timer.getTime() + .1) - - __info("DCS Fiddle server running") - env.info("DCS Fiddle successfully initialized.\n\nHappy Hacking!!", false) -elseif (not isMission) then - __info("Starting fiddle server in the Hooks environment...") - - local fiddleFile = lfs.writedir() .. 'Scripts\\Hooks\\dcs-fiddle-server.lua' - - local loop = create_server(bind_ip, port+1) - - local callbacks = {} - - function callbacks.onSimulationStart() - __info("Bootstrapping DCS Fiddle inside the mission using file " .. fiddleFile) - net.dostring_in("mission", string.format([[a_do_script("dofile('%s')")]], fiddleFile:gsub("\\","/"))) - end - - function callbacks.onSimulationFrame() - loop() - end - - DCS.setUserCallbacks(callbacks) - - __info("DCS Fiddle server running") -else - __info("Failed to start DCS fiddle, unknown environment") -end