diff --git a/Applications/Access.app/Main.lua b/Applications/Access.app/Main.lua new file mode 100644 index 00000000..690f0fec --- /dev/null +++ b/Applications/Access.app/Main.lua @@ -0,0 +1,69 @@ +-- Access.app/Main.lua +-- Application MineOS — contrôle des portes typées + LOCKDOWN. +-- UI par type : simple/bunker/shelter -> Ouvrir/Fermer ; airlock -> battants Intérieur/Extérieur ; +-- silo -> armement (réservé admin côté serveur). Toutes les actions passent par le serveur (RPC signé). + +local ROOT = (os.getenv and os.getenv("SECSITE_ROOT")) or "/home/secsite" +package.path = ROOT .. "/?.lua;" .. ROOT .. "/?/init.lua;" .. package.path + +local GUI = require("GUI") +local system = require("System") +local net = require("mineos/lib/net") +local session = require("mineos/lib/session") +local protocol = require("shared/protocol") + +local workspace, window = system.addWindow(GUI.filledWindow(1, 1, 96, 30, 0x1E1E1E)) +window:addChild(GUI.panel(1, 1, window.width, 3, 0x2D2D2D)) +window:addChild(GUI.text(3, 2, 0xFFFFFF, "CONTRÔLE D'ACCÈS")) + +local list = window:addChild(GUI.layout(3, 5, window.width - 4, window.height - 8, 1, 1)) + +local function cmd(id, action) + local resp = net.request(protocol.request(protocol.REQ.DOOR_CMD, + { token = session.token(), id = id, action = action })) + if not (resp and resp.ok) then + GUI.alert("Échec: " .. tostring(resp and resp.error or "réseau")) + end +end + +local function refresh() + list:removeChildren() + local resp = net.request(protocol.request(protocol.REQ.DOOR_LIST, { token = session.token() })) + if not (resp and resp.ok) then + list:addChild(GUI.text(1, 1, 0xDD4444, "Serveur injoignable ou non autorisé")) + workspace:draw(); return + end + if resp.data.locked then + list:addChild(GUI.text(1, 1, 0xDD4444, "⚠ LOCKDOWN ACTIF")) + end + for _, d in ipairs(resp.data.doors) do + local row = list:addChild(GUI.container(1, 1, list.width, 3)) + row:addChild(GUI.text(1, 2, 0xCCCCCC, d.name .. " [" .. d.type .. "]")) + if d.type == "airlock" then + row:addChild(GUI.button(40, 1, 14, 3, 0x3C3C3C, 0xFFF, 0x2D2D2D, 0xFFF, "Intérieur")).onTouch = function() + cmd(d.id, "inner_open") + end + row:addChild(GUI.button(56, 1, 14, 3, 0x3C3C3C, 0xFFF, 0x2D2D2D, 0xFFF, "Extérieur")).onTouch = function() + cmd(d.id, "outer_open") + end + else + row:addChild(GUI.button(40, 1, 12, 3, 0x2E7D32, 0xFFF, 0x2D2D2D, 0xFFF, "Ouvrir")).onTouch = function() + cmd(d.id, "open") + end + row:addChild(GUI.button(54, 1, 12, 3, 0xB71C1C, 0xFFF, 0x2D2D2D, 0xFFF, "Fermer")).onTouch = function() + cmd(d.id, "close") + end + end + end + workspace:draw() +end + +window:addChild(GUI.button(3, window.height - 2, 18, 1, 0xB71C1C, 0xFFF, 0x2D2D2D, 0xFFF, "LOCKDOWN")).onTouch = function() + cmd(nil, "lockdown"); refresh() +end +window:addChild(GUI.button(23, window.height - 2, 18, 1, 0x2E7D32, 0xFFF, 0x2D2D2D, 0xFFF, "Lever lockdown")).onTouch = function() + cmd(nil, "release"); refresh() +end +window:addChild(GUI.button(43, window.height - 2, 14, 1, 0x3C3C3C, 0xFFF, 0x2D2D2D, 0xFFF, "Rafraîchir")).onTouch = refresh + +refresh() diff --git a/Applications/Accounts.app/Main.lua b/Applications/Accounts.app/Main.lua new file mode 100644 index 00000000..733e4726 --- /dev/null +++ b/Applications/Accounts.app/Main.lua @@ -0,0 +1,60 @@ +-- Accounts.app/Main.lua +-- Application MineOS (admin) — gestion des comptes : liste, création, rôle, suppression, +-- et sessions actives. Toutes les opérations passent par le serveur (permission manage_accounts). + +local ROOT = (os.getenv and os.getenv("SECSITE_ROOT")) or "/home/secsite" +package.path = ROOT .. "/?.lua;" .. ROOT .. "/?/init.lua;" .. package.path + +local GUI = require("GUI") +local system = require("System") +local net = require("mineos/lib/net") +local session = require("mineos/lib/session") +local protocol = require("shared/protocol") +local roles = require("shared/roles") + +local workspace, window = system.addWindow(GUI.filledWindow(1, 1, 104, 32, 0x1E1E1E)) +window:addChild(GUI.panel(1, 1, window.width, 3, 0x2D2D2D)) +window:addChild(GUI.text(3, 2, 0xFFFFFF, "COMPTES & RÔLES")) + +local function rpc(rtype, payload) + payload = payload or {} + payload.token = session.token() + return net.request(protocol.request(rtype, payload)) +end + +local list = window:addChild(GUI.layout(3, 9, window.width - 4, window.height - 11, 1, 1)) + +local function refresh() + list:removeChildren() + local resp = rpc(protocol.REQ.ACCOUNT_LIST) + if not (resp and resp.ok) then + list:addChild(GUI.text(1, 1, 0xDD4444, "Non autorisé ou serveur injoignable.")) + workspace:draw(); return + end + for _, a in ipairs(resp.data.accounts) do + local row = list:addChild(GUI.container(1, 1, list.width, 1)) + local flags = (a.hasCard and "🪪 " or "") .. (a.hasPassword and "🔑" or "") + row:addChild(GUI.text(1, 1, 0xCCCCCC, string.format("%-16s %-8s %s", a.name, a.role, flags))) + row:addChild(GUI.button(50, 1, 12, 1, 0xB71C1C, 0xFFF, 0x2D2D2D, 0xFFF, "Supprimer")).onTouch = function() + rpc(protocol.REQ.ACCOUNT_DELETE, { id = a.id }); refresh() + end + end + workspace:draw() +end + +-- Formulaire de création. +local nameInput = window:addChild(GUI.input(3, 5, 24, 1, 0x262626, 0x999, 0x262626, 0xFFF, 0xFFF, "", "nom")) +local roleCombo = window:addChild(GUI.comboBox(29, 5, 16, 1, 0x262626, 0xFFF, 0x333, 0x999)) +for _, r in ipairs(roles.LIST) do roleCombo:addItem(r) end +local pwInput = window:addChild(GUI.input(47, 5, 20, 1, 0x262626, 0x999, 0x262626, 0xFFF, 0xFFF, "", "mot de passe")) +window:addChild(GUI.button(69, 5, 14, 1, 0x2E7D32, 0xFFF, 0x2D2D2D, 0xFFF, "Créer")).onTouch = function() + local resp = rpc(protocol.REQ.ACCOUNT_CREATE, { + name = nameInput.text, role = roleCombo:getItem(roleCombo.selectedItem).text, + password = pwInput.text ~= "" and pwInput.text or nil, + }) + if not (resp and resp.ok) then GUI.alert("Échec: " .. tostring(resp and resp.error)) end + refresh() +end + +window:addChild(GUI.button(85, 5, 14, 1, 0x3C3C3C, 0xFFF, 0x2D2D2D, 0xFFF, "Rafraîchir")).onTouch = refresh +refresh() diff --git a/Applications/Badges.app/Main.lua b/Applications/Badges.app/Main.lua new file mode 100644 index 00000000..4461c7cb --- /dev/null +++ b/Applications/Badges.app/Main.lua @@ -0,0 +1,64 @@ +-- Badges.app/Main.lua +-- Application MineOS (admin) — émission/révocation de badges. +-- Émettre : génère un cardId, le GRAVE sur la carte physique (os_cardwriter) et l'associe au +-- compte côté serveur (ACCOUNT_SETCARD). Révoquer : détache la carte du compte. + +local ROOT = (os.getenv and os.getenv("SECSITE_ROOT")) or "/home/secsite" +package.path = ROOT .. "/?.lua;" .. ROOT .. "/?/init.lua;" .. package.path + +local GUI = require("GUI") +local system = require("System") +local net = require("mineos/lib/net") +local session = require("mineos/lib/session") +local writer = require("mineos/lib/writer") +local protocol = require("shared/protocol") + +local workspace, window = system.addWindow(GUI.filledWindow(1, 1, 96, 30, 0x1E1E1E)) +window:addChild(GUI.panel(1, 1, window.width, 3, 0x2D2D2D)) +window:addChild(GUI.text(3, 2, 0xFFFFFF, "ÉMISSION DE BADGES")) + +local status = window:addChild(GUI.text(3, 5, 0xAAAAAA, + writer.available() and "Graveur détecté." or "⚠ Aucun graveur (os_cardwriter).")) + +local function rpc(rtype, payload) + payload = payload or {} + payload.token = session.token() + return net.request(protocol.request(rtype, payload)) +end + +local list = window:addChild(GUI.layout(3, 7, window.width - 4, window.height - 9, 1, 1)) + +local function issue(acc) + local cardId = writer.newCardId() + local okWrite, wReason = writer.write(cardId, "SecSite: " .. acc.name) + if not okWrite then + GUI.alert("Gravure impossible: " .. tostring(wReason)); return + end + local resp = rpc(protocol.REQ.ACCOUNT_SETCARD, { id = acc.id, cardId = cardId }) + if resp and resp.ok then status.text = "Badge émis pour " .. acc.name + else GUI.alert("Enregistrement échoué: " .. tostring(resp and resp.error)) end + workspace:draw() +end + +local function revoke(acc) + local resp = rpc(protocol.REQ.ACCOUNT_SETCARD, { id = acc.id }) -- cardId nil = révocation + if resp and resp.ok then status.text = "Badge révoqué pour " .. acc.name end + workspace:draw() +end + +local function refresh() + list:removeChildren() + local resp = rpc(protocol.REQ.ACCOUNT_LIST) + if not (resp and resp.ok) then + list:addChild(GUI.text(1, 1, 0xDD4444, "Non autorisé.")); workspace:draw(); return + end + for _, a in ipairs(resp.data.accounts) do + local row = list:addChild(GUI.container(1, 1, list.width, 1)) + row:addChild(GUI.text(1, 1, 0xCCCCCC, string.format("%-16s %s", a.name, a.hasCard and "🪪 badge" or "—"))) + row:addChild(GUI.button(40, 1, 14, 1, 0x2E7D32, 0xFFF, 0x2D2D2D, 0xFFF, "Émettre")).onTouch = function() issue(a) end + row:addChild(GUI.button(56, 1, 14, 1, 0xB71C1C, 0xFFF, 0x2D2D2D, 0xFFF, "Révoquer")).onTouch = function() revoke(a) end + end + workspace:draw() +end + +refresh() diff --git a/Applications/Config.app/Main.lua b/Applications/Config.app/Main.lua new file mode 100644 index 00000000..f0add06f --- /dev/null +++ b/Applications/Config.app/Main.lua @@ -0,0 +1,46 @@ +-- Config.app/Main.lua +-- Application MineOS (admin) — édite en jeu les réglages runtime (site, radar, défense), +-- appliqués à chaud côté serveur, sans toucher aux fichiers de code. + +local ROOT = (os.getenv and os.getenv("SECSITE_ROOT")) or "/home/secsite" +package.path = ROOT .. "/?.lua;" .. ROOT .. "/?/init.lua;" .. package.path + +local GUI = require("GUI") +local system = require("System") +local net = require("mineos/lib/net") +local session = require("mineos/lib/session") +local protocol = require("shared/protocol") + +local workspace, window = system.addWindow(GUI.filledWindow(1, 1, 90, 28, 0x1E1E1E)) +window:addChild(GUI.panel(1, 1, window.width, 3, 0x2D2D2D)) +window:addChild(GUI.text(3, 2, 0xFFFFFF, "CONFIGURATION (runtime)")) + +local function rpc(rtype, payload) + payload = payload or {}; payload.token = session.token() + return net.request(protocol.request(rtype, payload)) +end + +local list = window:addChild(GUI.layout(3, 5, window.width - 4, window.height - 6, 1, 1)) + +local function refresh() + list:removeChildren() + local resp = rpc(protocol.REQ.SETTINGS_GET) + if not (resp and resp.ok) then + list:addChild(GUI.text(1, 1, 0xDD4444, "Rôle admin requis ou serveur injoignable.")) + workspace:draw(); return + end + for _, setting in ipairs(resp.data.settings) do + local row = list:addChild(GUI.container(1, 1, list.width, 1)) + row:addChild(GUI.text(1, 1, 0xCCCCCC, string.format("%-24s (%s)", setting.key, setting.type))) + local input = row:addChild(GUI.input(40, 1, 22, 1, 0x262626, 0x999, 0x262626, 0xFFF, 0xFFF, + setting.value ~= nil and tostring(setting.value) or "", "valeur")) + row:addChild(GUI.button(64, 1, 12, 1, 0x2E7D32, 0xFFF, 0x2D2D2D, 0xFFF, "Appliquer")).onTouch = function() + local r = rpc(protocol.REQ.SETTINGS_SET, { key = setting.key, value = input.text }) + if not (r and r.ok) then GUI.alert("Échec: " .. tostring(r and r.error)) else refresh() end + end + end + workspace:draw() +end + +window:addChild(GUI.button(3, window.height - 1, 14, 1, 0x3C3C3C, 0xFFF, 0x2D2D2D, 0xFFF, "Rafraîchir")).onTouch = refresh +refresh() diff --git a/Applications/Defense.app/Main.lua b/Applications/Defense.app/Main.lua new file mode 100644 index 00000000..56a73278 --- /dev/null +++ b/Applications/Defense.app/Main.lua @@ -0,0 +1,53 @@ +-- Defense.app/Main.lua +-- Application MineOS (admin) — contre-mesures anti-missile : mode off/manual/auto + tir manuel. +-- En mode auto, le serveur engage automatiquement les contre-mesures sur escalade DEFCON. + +local ROOT = (os.getenv and os.getenv("SECSITE_ROOT")) or "/home/secsite" +package.path = ROOT .. "/?.lua;" .. ROOT .. "/?/init.lua;" .. package.path + +local GUI = require("GUI") +local system = require("System") +local net = require("mineos/lib/net") +local session = require("mineos/lib/session") +local protocol = require("shared/protocol") + +local workspace, window = system.addWindow(GUI.filledWindow(1, 1, 80, 24, 0x1E1E1E)) +window:addChild(GUI.panel(1, 1, window.width, 3, 0x2D2D2D)) +window:addChild(GUI.text(3, 2, 0xFFFFFF, "CONTRE-MESURES ANTI-MISSILE")) + +local function rpc(rtype, payload) + payload = payload or {}; payload.token = session.token() + return net.request(protocol.request(rtype, payload)) +end + +local modeLabel = window:addChild(GUI.text(3, 5, 0xAAAAAA, "Mode : —")) + +local function refresh() + local resp = rpc(protocol.REQ.DEFENSE_STATE) + if resp and resp.ok then + modeLabel.text = "Mode : " .. tostring(resp.data.mode) .. + (resp.data.lastFire and (" dernier tir: x" .. resp.data.lastFire.count) or "") + else + modeLabel.text = "Mode : (non autorisé / injoignable)" + end + workspace:draw() +end + +local function setMode(m) + local r = rpc(protocol.REQ.DEFENSE_MODE, { mode = m }) + if not (r and r.ok) then GUI.alert("Échec: " .. tostring(r and r.error)) end + refresh() +end + +window:addChild(GUI.button(3, 7, 14, 3, 0x3C3C3C, 0xFFF, 0x2D2D2D, 0xFFF, "OFF")).onTouch = function() setMode("off") end +window:addChild(GUI.button(19, 7, 14, 3, 0x2E7D32, 0xFFF, 0x2D2D2D, 0xFFF, "MANUEL")).onTouch = function() setMode("manual") end +window:addChild(GUI.button(35, 7, 14, 3, 0xEF6C00, 0xFFF, 0x2D2D2D, 0xFFF, "AUTO")).onTouch = function() setMode("auto") end + +window:addChild(GUI.button(3, 12, 30, 3, 0xB71C1C, 0xFFF, 0x2D2D2D, 0xFFF, "TIR MANUEL")).onTouch = function() + local r = rpc(protocol.REQ.DEFENSE_FIRE) + GUI.alert((r and r.ok) and ("Tir: x" .. r.data.fired) or ("Échec: " .. tostring(r and r.error))) + refresh() +end + +window:addChild(GUI.button(3, window.height - 1, 14, 1, 0x3C3C3C, 0xFFF, 0x2D2D2D, 0xFFF, "Rafraîchir")).onTouch = refresh +refresh() diff --git a/Applications/Fleet.app/Main.lua b/Applications/Fleet.app/Main.lua new file mode 100644 index 00000000..9840e3e0 --- /dev/null +++ b/Applications/Fleet.app/Main.lua @@ -0,0 +1,48 @@ +-- Fleet.app/Main.lua +-- Application MineOS (admin) — gestion de la flotte : lister les machines et envoyer +-- reboot / shutdown / lock à distance. Commandes réservées au rôle Admin (vérifié serveur). + +local ROOT = (os.getenv and os.getenv("SECSITE_ROOT")) or "/home/secsite" +package.path = ROOT .. "/?.lua;" .. ROOT .. "/?/init.lua;" .. package.path + +local GUI = require("GUI") +local system = require("System") +local net = require("mineos/lib/net") +local session = require("mineos/lib/session") +local protocol = require("shared/protocol") + +local workspace, window = system.addWindow(GUI.filledWindow(1, 1, 100, 30, 0x1E1E1E)) +window:addChild(GUI.panel(1, 1, window.width, 3, 0x2D2D2D)) +window:addChild(GUI.text(3, 2, 0xFFFFFF, "FLOTTE — Gestion à distance")) + +local function rpc(rtype, payload) + payload = payload or {}; payload.token = session.token() + return net.request(protocol.request(rtype, payload)) +end + +local list = window:addChild(GUI.layout(3, 5, window.width - 4, window.height - 7, 1, 1)) + +local function send(address, command) + local r = rpc(protocol.REQ.NODE_CMD, { address = address, command = command }) + if not (r and r.ok) then GUI.alert("Échec: " .. tostring(r and r.error)) end +end + +local function refresh() + list:removeChildren() + local resp = rpc(protocol.REQ.NODE_LIST) + if not (resp and resp.ok) then + list:addChild(GUI.text(1, 1, 0xDD4444, "Rôle Admin requis ou serveur injoignable.")) + workspace:draw(); return + end + for _, n in ipairs(resp.data.nodes) do + local row = list:addChild(GUI.container(1, 1, list.width, 1)) + row:addChild(GUI.text(1, 1, 0xCCCCCC, string.format("%s %-10s %s", n.address:sub(1, 8), n.kind, n.status))) + row:addChild(GUI.button(40, 1, 12, 1, 0xEF6C00, 0xFFF, 0x2D2D2D, 0xFFF, "Reboot")).onTouch = function() send(n.address, "reboot") end + row:addChild(GUI.button(54, 1, 12, 1, 0xB71C1C, 0xFFF, 0x2D2D2D, 0xFFF, "Éteindre")).onTouch = function() send(n.address, "shutdown") end + row:addChild(GUI.button(68, 1, 12, 1, 0x3C3C3C, 0xFFF, 0x2D2D2D, 0xFFF, "Verrouiller")).onTouch = function() send(n.address, "lock") end + end + workspace:draw() +end + +window:addChild(GUI.button(3, window.height - 1, 14, 1, 0x3C3C3C, 0xFFF, 0x2D2D2D, 0xFFF, "Rafraîchir")).onTouch = refresh +refresh() diff --git a/Applications/Logs.app/Main.lua b/Applications/Logs.app/Main.lua new file mode 100644 index 00000000..f9455e30 --- /dev/null +++ b/Applications/Logs.app/Main.lua @@ -0,0 +1,34 @@ +-- Logs.app/Main.lua +-- Application MineOS — consultation du journal d'audit (connexions, portes, radar, sécurité). + +local ROOT = (os.getenv and os.getenv("SECSITE_ROOT")) or "/home/secsite" +package.path = ROOT .. "/?.lua;" .. ROOT .. "/?/init.lua;" .. package.path + +local GUI = require("GUI") +local system = require("System") +local net = require("mineos/lib/net") +local session = require("mineos/lib/session") +local protocol = require("shared/protocol") + +local workspace, window = system.addWindow(GUI.filledWindow(1, 1, 100, 30, 0x1E1E1E)) +window:addChild(GUI.panel(1, 1, window.width, 3, 0x2D2D2D)) +window:addChild(GUI.text(3, 2, 0xFFFFFF, "JOURNAL D'AUDIT")) + +local view = window:addChild(GUI.textBox(3, 5, window.width - 4, window.height - 7, 0x161616, 0xBBBBBB, {}, 1, 0, 0)) + +local function refresh() + local lines = {} + local resp = net.request(protocol.request(protocol.REQ.LOG_QUERY, { token = session.token(), limit = 100 })) + if resp and resp.ok then + for _, e in ipairs(resp.data.entries) do + lines[#lines + 1] = string.format("%s [%s] %s — %s", e.stamp or "", e.kind or "?", e.actor or "?", e.message or "") + end + else + lines = { "Serveur injoignable ou non autorisé." } + end + view.lines = lines + workspace:draw() +end + +window:addChild(GUI.button(3, window.height - 1, 14, 1, 0x3C3C3C, 0xFFF, 0x2D2D2D, 0xFFF, "Rafraîchir")).onTouch = refresh +refresh() diff --git a/Applications/Map.app/Main.lua b/Applications/Map.app/Main.lua new file mode 100644 index 00000000..d7ff6405 --- /dev/null +++ b/Applications/Map.app/Main.lua @@ -0,0 +1,78 @@ +-- Map.app/Main.lua +-- Application MineOS — carte 2D (vue du dessus) des contacts radar autour de l'installation. +-- Le centre/rayon viennent de shared/site.lua ; les contacts (x,z) de SITUATION_GET +-- (nécessite le radar OC HBM pour des positions ; en repli redstone, pas de blips). + +local ROOT = (os.getenv and os.getenv("SECSITE_ROOT")) or "/home/secsite" +package.path = ROOT .. "/?.lua;" .. ROOT .. "/?/init.lua;" .. package.path + +local GUI = require("GUI") +local system = require("System") +local net = require("mineos/lib/net") +local session = require("mineos/lib/session") +local protocol = require("shared/protocol") +local site = require("shared/site") + +local workspace, window = system.addWindow(GUI.filledWindow(1, 1, 92, 30, 0x0A0A0A)) +window:addChild(GUI.panel(1, 1, window.width, 3, 0x2D2D2D)) +window:addChild(GUI.text(3, 2, 0xFFFFFF, "CARTE TACTIQUE")) + +local MAP_X, MAP_Y, MAP_W, MAP_H = 3, 5, 60, 22 +local mapArea = window:addChild(GUI.panel(MAP_X, MAP_Y, MAP_W, MAP_H, 0x111111)) +local markers = window:addChild(GUI.container(MAP_X, MAP_Y, MAP_W, MAP_H)) +local sidebar = window:addChild(GUI.layout(MAP_X + MAP_W + 2, MAP_Y, window.width - MAP_W - MAP_X - 3, MAP_H, 1, 1)) + +local function rpc(rtype, payload) + payload = payload or {}; payload.token = session.token() + return net.request(protocol.request(rtype, payload)) +end + +local function refresh() + markers:removeChildren() + sidebar:removeChildren() + + local cx, cz = site.CONFIG.center.x, site.CONFIG.center.z + local range = math.max(16, (site.CONFIG.radius or 64) * 3) -- portée affichée + local mcx, mcy = math.floor(MAP_W / 2), math.floor(MAP_H / 2) + local sx, sy = mcx / range, mcy / range + + -- Centre du site. + markers:addChild(GUI.text(mcx, mcy, 0x2E7D32, "◉")) + + local resp = rpc(protocol.REQ.SITUATION_GET) + if not (resp and resp.ok) then + sidebar:addChild(GUI.text(1, 1, 0xDD4444, "Injoignable / non autorisé")) + workspace:draw(); return + end + local sit = resp.data + + sidebar:addChild(GUI.text(1, 1, 0xFFFFFF, "DEFCON " .. tostring(sit.defcon))) + sidebar:addChild(GUI.text(1, 1, sit.alert and 0xEF5350 or 0x9E9E9E, sit.alert and "ALERTE" or "calme")) + sidebar:addChild(GUI.text(1, 1, 0x9E9E9E, "Contacts: " .. tostring(sit.contacts or 0))) + if sit.impactETA then sidebar:addChild(GUI.text(1, 1, 0xF9A825, "ETA ~" .. math.floor(sit.impactETA) .. "s")) end + + -- Blips des contacts (positions relatives au centre). + local rc = resp.data and select(2, pcall(function() return rpc(protocol.REQ.RADAR_STATE) end)) + local radar = rc and rc.ok and rc.data or nil + for _, k in ipairs((radar and radar.contacts) or {}) do + if k.x and k.z then + local mx = math.floor(mcx + (k.x - cx) * sx) + local my = math.floor(mcy + (k.z - cz) * sy) + if mx >= 1 and mx <= MAP_W and my >= 1 and my <= MAP_H then + markers:addChild(GUI.text(mx, my, 0xB71C1C, "▲")) + end + end + end + + -- Portes importantes en légende. + sidebar:addChild(GUI.text(1, 1, 0x666666, "— Portes —")) + for _, d in ipairs(sit.importantDoors or {}) do + local st = d.state or {} + local open = st.open or st.inner or st.outer + sidebar:addChild(GUI.text(1, 1, open and 0xEF5350 or 0x66BB6A, (open and "▮ " or "▯ ") .. d.name)) + end + workspace:draw() +end + +window:addChild(GUI.button(3, window.height - 1, 14, 1, 0x3C3C3C, 0xFFF, 0x2D2D2D, 0xFFF, "Rafraîchir")).onTouch = refresh +refresh() diff --git a/Applications/Messages.app/Main.lua b/Applications/Messages.app/Main.lua new file mode 100644 index 00000000..e72a3890 --- /dev/null +++ b/Applications/Messages.app/Main.lua @@ -0,0 +1,57 @@ +-- Messages.app/Main.lua +-- Application MineOS — collaboration : bulletin d'annonces + boîte de réception. +-- Publier une annonce la diffuse à tous les terminaux et dans le chat du serveur (Computronics). + +local ROOT = (os.getenv and os.getenv("SECSITE_ROOT")) or "/home/secsite" +package.path = ROOT .. "/?.lua;" .. ROOT .. "/?/init.lua;" .. package.path + +local GUI = require("GUI") +local system = require("System") +local net = require("mineos/lib/net") +local session = require("mineos/lib/session") +local protocol = require("shared/protocol") + +local workspace, window = system.addWindow(GUI.filledWindow(1, 1, 100, 32, 0x1E1E1E)) +window:addChild(GUI.panel(1, 1, window.width, 3, 0x2D2D2D)) +window:addChild(GUI.text(3, 2, 0xFFFFFF, "MESSAGERIE & ANNONCES")) + +local function rpc(rtype, payload) + payload = payload or {}; payload.token = session.token() + return net.request(protocol.request(rtype, payload)) +end + +window:addChild(GUI.text(3, 5, 0xAAAAAA, "Tableau d'annonces")) +local board = window:addChild(GUI.textBox(3, 6, window.width - 4, 10, 0x161616, 0xBBBBBB, {}, 1, 0, 0)) + +window:addChild(GUI.text(3, 18, 0xAAAAAA, "Boîte de réception")) +local inbox = window:addChild(GUI.textBox(3, 19, window.width - 4, 8, 0x161616, 0xBBBBBB, {}, 1, 0, 0)) + +local function refresh() + local b = rpc(protocol.REQ.BOARD_GET, { limit = 20 }) + local blines = {} + if b and b.ok then + for _, a in ipairs(b.data.board) do blines[#blines + 1] = (a.stamp or "") .. " " .. a.actor .. ": " .. a.text end + end + board.lines = #blines > 0 and blines or { "(aucune annonce)" } + + local m = rpc(protocol.REQ.MSG_INBOX) + local mlines = {} + if m and m.ok then + for _, e in ipairs(m.data.messages) do mlines[#mlines + 1] = (e.stamp or "") .. " " .. e.from .. ": " .. e.text end + end + inbox.lines = #mlines > 0 and mlines or { "(aucun message)" } + workspace:draw() +end + +-- Zone de publication d'annonce. +local annInput = window:addChild(GUI.input(3, window.height - 2, 60, 1, 0x262626, 0x999, 0x262626, 0xFFF, 0xFFF, "", "annonce…")) +window:addChild(GUI.button(65, window.height - 2, 14, 1, 0x2E7D32, 0xFFF, 0x2D2D2D, 0xFFF, "Publier")).onTouch = function() + if annInput.text ~= "" then + rpc(protocol.REQ.ANNOUNCE_POST, { text = annInput.text }) + annInput.text = "" + refresh() + end +end +window:addChild(GUI.button(81, window.height - 2, 14, 1, 0x3C3C3C, 0xFFF, 0x2D2D2D, 0xFFF, "Rafraîchir")).onTouch = refresh + +refresh() diff --git a/Applications/Protocols.app/Main.lua b/Applications/Protocols.app/Main.lua new file mode 100644 index 00000000..a64fd736 --- /dev/null +++ b/Applications/Protocols.app/Main.lua @@ -0,0 +1,62 @@ +-- Protocols.app/Main.lua +-- Application MineOS — protocoles : lister, saisir un code et lancer en DRILL (simulation) ou RÉEL. +-- Réel = permission "protocol" (admin) ; drill = permission "drill" (agent+admin), vérifié serveur. + +local ROOT = (os.getenv and os.getenv("SECSITE_ROOT")) or "/home/secsite" +package.path = ROOT .. "/?.lua;" .. ROOT .. "/?/init.lua;" .. package.path + +local GUI = require("GUI") +local system = require("System") +local net = require("mineos/lib/net") +local session = require("mineos/lib/session") +local protocol = require("shared/protocol") + +local workspace, window = system.addWindow(GUI.filledWindow(1, 1, 96, 30, 0x1E1E1E)) +window:addChild(GUI.panel(1, 1, window.width, 3, 0x2D2D2D)) +window:addChild(GUI.text(3, 2, 0xFFFFFF, "PROTOCOLES & OVERRIDES")) + +local function rpc(rtype, payload) + payload = payload or {}; payload.token = session.token() + return net.request(protocol.request(rtype, payload)) +end + +local list = window:addChild(GUI.layout(3, 8, window.width - 4, window.height - 10, 1, 1)) + +local function run(code, drill) + local r = rpc(protocol.REQ.PROTOCOL_RUN, { code = code, drill = drill }) + if r and r.ok then + GUI.alert((drill and "DRILL lancé: " or "Protocole exécuté: ") .. r.data.name) + else + GUI.alert("Échec: " .. tostring(r and r.error)) + end +end + +local function refresh() + list:removeChildren() + local resp = rpc(protocol.REQ.PROTOCOL_LIST) + if not (resp and resp.ok) then + list:addChild(GUI.text(1, 1, 0xDD4444, "Non autorisé ou serveur injoignable.")) + workspace:draw(); return + end + for _, p in ipairs(resp.data.protocols) do + local row = list:addChild(GUI.container(1, 1, list.width, 1)) + row:addChild(GUI.text(1, 1, 0xCCCCCC, string.format("%-28s code:%-6s %s", p.name, p.code, p.role))) + if p.drillable then + row:addChild(GUI.button(48, 1, 10, 1, 0x9E9D24, 0xFFF, 0x2D2D2D, 0xFFF, "Drill")).onTouch = function() run(p.code, true) end + end + row:addChild(GUI.button(60, 1, 10, 1, 0xB71C1C, 0xFFF, 0x2D2D2D, 0xFFF, "RÉEL")).onTouch = function() run(p.code, false) end + end + workspace:draw() +end + +-- Saisie directe par code. +local codeInput = window:addChild(GUI.input(3, 5, 16, 1, 0x262626, 0x999, 0x262626, 0xFFF, 0xFFF, "", "code")) +window:addChild(GUI.button(21, 5, 12, 1, 0x9E9D24, 0xFFF, 0x2D2D2D, 0xFFF, "Drill")).onTouch = function() + if codeInput.text ~= "" then run(codeInput.text, true) end +end +window:addChild(GUI.button(35, 5, 12, 1, 0xB71C1C, 0xFFF, 0x2D2D2D, 0xFFF, "RÉEL")).onTouch = function() + if codeInput.text ~= "" then run(codeInput.text, false) end +end +window:addChild(GUI.button(49, 5, 14, 1, 0x3C3C3C, 0xFFF, 0x2D2D2D, 0xFFF, "Rafraîchir")).onTouch = refresh + +refresh() diff --git a/Applications/Reactor.app/Main.lua b/Applications/Reactor.app/Main.lua new file mode 100644 index 00000000..0fc83aab --- /dev/null +++ b/Applications/Reactor.app/Main.lua @@ -0,0 +1,52 @@ +-- Reactor.app/Main.lua +-- Application MineOS — supervision réacteurs / énergie (température, combustible, puissance), +-- avec arrêt d'urgence (SCRAM) réservé au rôle admin. + +local ROOT = (os.getenv and os.getenv("SECSITE_ROOT")) or "/home/secsite" +package.path = ROOT .. "/?.lua;" .. ROOT .. "/?/init.lua;" .. package.path + +local GUI = require("GUI") +local system = require("System") +local net = require("mineos/lib/net") +local session = require("mineos/lib/session") +local protocol = require("shared/protocol") + +local STATUS_COLOR = { ok = 0x66BB6A, warn = 0xF9A825, crit = 0xB71C1C, unknown = 0x757575 } + +local workspace, window = system.addWindow(GUI.filledWindow(1, 1, 96, 30, 0x1E1E1E)) +window:addChild(GUI.panel(1, 1, window.width, 3, 0x2D2D2D)) +window:addChild(GUI.text(3, 2, 0xFFFFFF, "SUPERVISION RÉACTEURS / ÉNERGIE")) + +local function rpc(rtype, payload) + payload = payload or {}; payload.token = session.token() + return net.request(protocol.request(rtype, payload)) +end + +local list = window:addChild(GUI.layout(3, 5, window.width - 4, window.height - 6, 1, 1)) + +local function refresh() + list:removeChildren() + local resp = rpc(protocol.REQ.POWER_STATE) + if not (resp and resp.ok) then + list:addChild(GUI.text(1, 1, 0xDD4444, "Non autorisé ou serveur injoignable.")) + workspace:draw(); return + end + for _, r in ipairs(resp.data.reactors) do + local row = list:addChild(GUI.container(1, 1, list.width, 2)) + row:addChild(GUI.text(1, 1, STATUS_COLOR[r.status] or 0xCCCCCC, "● " .. r.name .. " [" .. (r.status or "?") .. "]")) + local info = {} + if r.temp then info[#info + 1] = "T=" .. math.floor(r.temp) end + if r.fuel then info[#info + 1] = "Fuel=" .. math.floor(r.fuel) end + if r.power then info[#info + 1] = "P=" .. math.floor(r.power) end + if r.energy then info[#info + 1] = "E=" .. math.floor(r.energy) end + row:addChild(GUI.text(30, 1, 0x999999, table.concat(info, " "))) + row:addChild(GUI.button(70, 1, 12, 1, 0xB71C1C, 0xFFF, 0x2D2D2D, 0xFFF, "SCRAM")).onTouch = function() + local sc = rpc(protocol.REQ.REACTOR_SCRAM, { id = r.id }) + GUI.alert((sc and sc.ok) and "SCRAM envoyé" or ("Échec: " .. tostring(sc and sc.error))) + end + end + workspace:draw() +end + +window:addChild(GUI.button(3, window.height - 1, 14, 1, 0x3C3C3C, 0xFFF, 0x2D2D2D, 0xFFF, "Rafraîchir")).onTouch = refresh +refresh() diff --git a/Applications/SecurityConsole.app/Main.lua b/Applications/SecurityConsole.app/Main.lua new file mode 100644 index 00000000..cdba65fc --- /dev/null +++ b/Applications/SecurityConsole.app/Main.lua @@ -0,0 +1,64 @@ +-- SecurityConsole.app/Main.lua +-- Application MineOS — coquille du tableau de bord de sécurité (lot 1). +-- Utilise le framework GUI de MineOS (workspaces / conteneurs / widgets). +-- Les widgets DEFCON / portes / alertes seront ajoutés au lot 2. + +local ROOT = (os.getenv and os.getenv("SECSITE_ROOT")) or "/home/secsite" +package.path = ROOT .. "/?.lua;" .. ROOT .. "/?/init.lua;" .. package.path + +local GUI = require("GUI") +local system = require("System") +local net = require("mineos/lib/net") +local session = require("mineos/lib/session") +local protocol = require("shared/protocol") + +-- Couleur d'affichage selon le niveau DEFCON (5 calme -> 1 imminent). +local DEFCON_COLOR = { [5] = 0x2E7D32, [4] = 0x9E9D24, [3] = 0xF9A825, [2] = 0xEF6C00, [1] = 0xB71C1C } + +-- Fenêtre principale. +local workspace, window = system.addWindow(GUI.filledWindow(1, 1, 88, 26, 0x1E1E1E)) + +window:addChild(GUI.panel(1, 1, window.width, 3, 0x2D2D2D)) +window:addChild(GUI.text(3, 2, 0xFFFFFF, "SECURITY CONSOLE — Intranet")) + +local statusLabel = window:addChild(GUI.text(3, 5, 0xAAAAAA, "Serveur : vérification…")) +local defconPanel = window:addChild(GUI.panel(3, 7, 40, 3, 0x333333)) +local defconLabel = window:addChild(GUI.text(5, 8, 0xFFFFFF, "DEFCON : —")) + +-- Interroge serveur (ping) + état radar (DEFCON). +local function refresh() + local resp = net.ping() + if resp and resp.ok then + statusLabel.text = "Serveur : EN LIGNE (protocole v" .. tostring(resp.data.v) .. ")" + statusLabel.color = 0x44DD44 + else + statusLabel.text = "Serveur : INJOIGNABLE" + statusLabel.color = 0xDD4444 + end + + local rs = net.request(protocol.request(protocol.REQ.RADAR_STATE, { token = session.token() })) + if rs and rs.ok then + local d = rs.data.defcon or 5 + defconLabel.text = "DEFCON : " .. d .. (rs.data.alert and " ⚠ ALERTE" or "") + defconPanel.color = DEFCON_COLOR[d] or 0x333333 + end + workspace:draw() +end + +window:addChild(GUI.button(3, 11, 24, 3, 0x3C3C3C, 0xFFFFFF, 0x2D2D2D, 0xFFFFFF, "Rafraîchir")).onTouch = function() + refresh() +end + +-- Saisie rapide d'un code de protocole (drill/réel). +window:addChild(GUI.text(3, 16, 0xAAAAAA, "Code protocole :")) +local codeInput = window:addChild(GUI.input(19, 16, 12, 1, 0x262626, 0x999999, 0x262626, 0xFFFFFF, 0xFFFFFF, "", "code")) +local function runProto(drill) + if codeInput.text == "" then return end + local r = net.request(protocol.request(protocol.REQ.PROTOCOL_RUN, { token = session.token(), code = codeInput.text, drill = drill })) + GUI.alert((r and r.ok) and ((drill and "DRILL: " or "Exécuté: ") .. r.data.name) or ("Échec: " .. tostring(r and r.error))) +end +window:addChild(GUI.button(33, 16, 10, 1, 0x9E9D24, 0xFFF, 0x2D2D2D, 0xFFF, "Drill")).onTouch = function() runProto(true) end +window:addChild(GUI.button(45, 16, 10, 1, 0xB71C1C, 0xFFF, 0x2D2D2D, 0xFFF, "RÉEL")).onTouch = function() runProto(false) end + +refresh() +workspace:draw() diff --git a/Installer/Files.cfg b/Installer/Files.cfg index c63847f6..541c1176 100644 --- a/Installer/Files.cfg +++ b/Installer/Files.cfg @@ -45,6 +45,37 @@ "Localizations/Polish.lang" }, required = { + -- === SecSite OS : bibliothèques + applications préinstallées === + "Libraries/shared/protocol.lua", + "Libraries/shared/netsec.lua", + "Libraries/shared/sha2.lua", + "Libraries/shared/util.lua", + "Libraries/shared/roles.lua", + "Libraries/shared/doors.lua", + "Libraries/shared/site.lua", + "Libraries/shared/protocols.lua", + "Libraries/shared/reactors.lua", + "Libraries/shared/defense.lua", + "Libraries/shared/branding.lua", + "Libraries/mineos/lib/net.lua", + "Libraries/mineos/lib/card.lua", + "Libraries/mineos/lib/session.lua", + "Libraries/mineos/lib/writer.lua", + "Libraries/mineos/lib/blackout.lua", + "Libraries/mineos/login-fork/patch.lua", + "Libraries/mineos/login-fork/autorun.lua", + "Applications/SecurityConsole.app/Main.lua", + "Applications/Access.app/Main.lua", + "Applications/Accounts.app/Main.lua", + "Applications/Badges.app/Main.lua", + "Applications/Logs.app/Main.lua", + "Applications/Messages.app/Main.lua", + "Applications/Fleet.app/Main.lua", + "Applications/Protocols.app/Main.lua", + "Applications/Reactor.app/Main.lua", + "Applications/Defense.app/Main.lua", + "Applications/Config.app/Main.lua", + "Applications/Map.app/Main.lua", -- Libraries { path = "Libraries/Bit32.lua", diff --git a/Installer/Main.lua b/Installer/Main.lua index 7598d263..f6f0a8c4 100644 --- a/Installer/Main.lua +++ b/Installer/Main.lua @@ -13,7 +13,7 @@ local EEPROMAddress, internetAddress, GPUAddress = component.invoke(GPUAddress, "bind", getComponentAddress("screen")) local screenWidth, screenHeight = component.invoke(GPUAddress, "getResolution") -local repositoryURL = "https://raw.githubusercontent.com/IgorTimofeev/MineOS/master/" +local repositoryURL = "https://raw.githubusercontent.com/quentinthierry12/mineos/master/" local installerURL = "Installer/" local EFIURL = "EFI/Minified.lua" diff --git a/Installer/OpenOS.lua b/Installer/OpenOS.lua index ec93789e..aae18c15 100644 --- a/Installer/OpenOS.lua +++ b/Installer/OpenOS.lua @@ -58,7 +58,7 @@ end -- Checking if installer can be downloaded from GitHub, because of PKIX errors, server blacklists, etc do - local success, result = pcall(component.internet.request, "https://raw.githubusercontent.com/IgorTimofeev/MineOS/master/Installer/Main.lua") + local success, result = pcall(component.internet.request, "https://raw.githubusercontent.com/quentinthierry12/mineos/master/Installer/Main.lua") if not success then if result then @@ -102,7 +102,7 @@ end -- Flashing EEPROM with tiny script that will run installer itself after reboot. -- It's necessary, because we need clean computer without OpenOS hooks to computer.pullSignal() component.eeprom.set([[ - local connection, data, chunk = component.proxy(component.list("internet")()).request("https://raw.githubusercontent.com/IgorTimofeev/MineOS/master/Installer/Main.lua"), "" + local connection, data, chunk = component.proxy(component.list("internet")()).request("https://raw.githubusercontent.com/quentinthierry12/mineos/master/Installer/Main.lua"), "" while true do chunk = connection.read(math.huge) diff --git a/Libraries/System.lua b/Libraries/System.lua index 2111ee69..e5f03ba2 100755 --- a/Libraries/System.lua +++ b/Libraries/System.lua @@ -3263,6 +3263,23 @@ function system.authorize() end end + -- === SecSite : authentification par carte OpenSecurity (en plus du mot de passe) === + local secStop + local secOk, secPatch = pcall(require, "mineos/login-fork/patch") + if secOk then + secStop = secPatch.cardListener(function(userName) + for _, u in ipairs(userList) do + if u:sub(1, -2) == userName then + if secStop then secStop() end + container:remove() + updateUser(userName) + workspace:draw() + return + end + end + end) + end + selectUser() else updateUser(userList[1]:sub(1, -2)) diff --git a/Libraries/mineos/lib/blackout.lua b/Libraries/mineos/lib/blackout.lua new file mode 100644 index 00000000..08d965b4 --- /dev/null +++ b/Libraries/mineos/lib/blackout.lua @@ -0,0 +1,50 @@ +-- mineos/lib/blackout.lua +-- Watcher « poste inaccessible » : quand l'agent a reçu la commande `blackout`, un drapeau existe ; +-- ce module affiche alors une page plein écran bloquante jusqu'à la levée (`release`). +-- À appeler depuis le hook de login/kiosque du terminal (les écrans du mur en sont exemptés). + +local component = require("component") + +local blackout = {} + +local FLAG = "/tmp/secsite.blackout" + +function blackout.active() + local f = io.open(FLAG, "r") + if f then f:close(); return true end + return false +end + +function blackout.screen() + if not component.isAvailable("gpu") then return end + local gpu = component.gpu + local w, h = gpu.getResolution() + gpu.setBackground(0x330000) + gpu.setForeground(0xFFFFFF) + gpu.fill(1, 1, w, h, " ") + local msg = "ORDINATEUR INACCESSIBLE" + local sub = "Acces suspendu par la securite" + gpu.set(math.max(1, math.floor((w - #msg) / 2)), math.floor(h / 2), msg) + gpu.set(math.max(1, math.floor((w - #sub) / 2)), math.floor(h / 2) + 2, sub) +end + +-- Boucle bloquante tant que le poste est verrouillé. +function blackout.guard() + local event = require("event") + while blackout.active() do + blackout.screen() + event.pull(1) + end +end + +-- À appeler depuis la boucle de login/kiosque : prend l'écran si le poste est en blackout. +-- Renvoie true si le blackout a été appliqué (et vient d'être levé), false sinon. +function blackout.enforce() + if blackout.active() then + blackout.guard() + return true + end + return false +end + +return blackout diff --git a/Libraries/mineos/lib/card.lua b/Libraries/mineos/lib/card.lua new file mode 100644 index 00000000..73677dd5 --- /dev/null +++ b/Libraries/mineos/lib/card.lua @@ -0,0 +1,65 @@ +-- mineos/lib/card.lua +-- Adaptateur lecteur de carte OpenSecurity (magnétique ou RFID). +-- API confirmées (wiki PC-Logix/OpenSecurity) : +-- * Magreader : événement `magData` = (address, playerName, cardData, cardUniqueId, isCardLocked, side) +-- -> identifiant de badge = cardData (repli cardUniqueId). Passif (swipe joueur). +-- * RFID : `scan([1-64])` déclenche `rfidData` = (uuid, playerName, distance, data) +-- -> identifiant = data. ACTIF : il faut appeler scan(). + +local component = require("component") +local computer = require("computer") +local event = require("event") + +local card = {} + +card.EVENTS = { magData = true, rfidData = true } + +-- Détecte un lecteur disponible. Renvoie (proxy, type) ou nil. +function card.reader() + if component.isAvailable("os_magreader") then return component.os_magreader, "mag" end + if component.isAvailable("os_rfidreader") then return component.os_rfidreader, "rfid" end + return nil +end + +function card.available() + return card.reader() ~= nil +end + +-- Extrait l'identifiant de badge selon l'événement OpenSecurity. +-- ev[1] = nom de l'événement ; les champs suivants dépendent du type. +local function extract(ev) + local name = ev[1] + if name == "magData" then + -- magData: address, playerName, cardData, cardUniqueId, isCardLocked, side + return ev[4] or ev[5] + elseif name == "rfidData" then + -- rfidData: uuid, playerName, distance, data + return ev[5] + end + return nil +end + +-- Attend une carte. Renvoie le cardId (string) ou nil au timeout. +-- Pour un lecteur RFID, on déclenche activement un scan à chaque itération. +function card.await(timeout) + local reader, kind = card.reader() + local deadline = timeout and (computer.uptime() + timeout) or nil + while true do + local remaining = deadline and (deadline - computer.uptime()) or nil + if remaining and remaining <= 0 then return nil end + if kind == "rfid" and reader then pcall(reader.scan) end + -- RFID : scans rapprochés ; borne l'attente pour re-scanner régulièrement. + local wait = remaining + if kind == "rfid" then wait = math.min(remaining or 1, 1) end + local ev = { event.pull(wait) } + local name = ev[1] + if name and card.EVENTS[name] then + local id = extract(ev) + if id and id ~= "" then return id end + elseif name == nil and kind ~= "rfid" then + return nil + end + end +end + +return card diff --git a/Libraries/mineos/lib/net.lua b/Libraries/mineos/lib/net.lua new file mode 100644 index 00000000..93dd49e1 --- /dev/null +++ b/Libraries/mineos/lib/net.lua @@ -0,0 +1,56 @@ +-- mineos/lib/net.lua +-- Client RPC vers le serveur de sécurité. Utilisé par les apps MineOS, la tablette et l'agent. +-- Diffuse une requête signée sur le port privé et attend la réponse corrélée (par id). +-- Le serveur est découvert par broadcast : sur le réseau privé (HMAC + liste blanche), +-- seul le vrai serveur peut produire une réponse valide. + +local ROOT = (os.getenv and os.getenv("SECSITE_ROOT")) or "/home/secsite" +package.path = ROOT .. "/?.lua;" .. ROOT .. "/?/init.lua;" .. package.path + +local component = require("component") +local computer = require("computer") +local event = require("event") + +local netsec = require("shared/netsec") +local protocol = require("shared/protocol") +local util = require("shared/util") + +local net = {} + +local modem = component.modem +if not modem.isOpen(protocol.PORT) then modem.open(protocol.PORT) end + +-- net.request(table, timeout?) -> (réponse, nil) ou (nil, raison) +function net.request(tbl, timeout) + timeout = timeout or 3 + tbl.id = util.uuid() + modem.broadcast(protocol.PORT, netsec.encode(tbl)) + local deadline = computer.uptime() + timeout + while true do + local remaining = deadline - computer.uptime() + if remaining <= 0 then return nil, "timeout" end + local name, _, _, port, _, msg = event.pull(remaining, "modem_message") + if not name then return nil, "timeout" end + if port == protocol.PORT and msg then + local resp = netsec.decode(msg) + if resp and resp.id == tbl.id then + return resp + end + end + end +end + +-- Raccourcis d'authentification. +function net.loginCard(cardId) + return net.request(protocol.request(protocol.REQ.AUTH_CARD, { cardId = cardId })) +end + +function net.loginPassword(name, password) + return net.request(protocol.request(protocol.REQ.AUTH_PASSWORD, { name = name, password = password })) +end + +function net.ping() + return net.request(protocol.request(protocol.REQ.PING), 2) +end + +return net diff --git a/Libraries/mineos/lib/session.lua b/Libraries/mineos/lib/session.lua new file mode 100644 index 00000000..44c84bcd --- /dev/null +++ b/Libraries/mineos/lib/session.lua @@ -0,0 +1,33 @@ +-- mineos/lib/session.lua +-- Détient la session courante du terminal (token/rôle/nom), établie par le login forké +-- (mineos/login-fork) et lue par les applications de sécurité. + +local session = {} + +local current = nil + +function session.set(s) + current = s -- { token, name, role } +end + +function session.clear() + current = nil +end + +function session.get() + return current +end + +function session.token() + return current and current.token or nil +end + +function session.role() + return current and current.role or nil +end + +function session.name() + return current and current.name or nil +end + +return session diff --git a/Libraries/mineos/lib/writer.lua b/Libraries/mineos/lib/writer.lua new file mode 100644 index 00000000..640d9050 --- /dev/null +++ b/Libraries/mineos/lib/writer.lua @@ -0,0 +1,32 @@ +-- mineos/lib/writer.lua +-- Adaptateur du graveur de carte OpenSecurity (os_cardwriter) pour l'émission de badges. +-- La signature exacte de write(...) est À CONFIRMER en jeu ; isolée ici. + +local component = require("component") +local util = require("shared/util") + +local writer = {} + +function writer.available() + return component.isAvailable("os_cardwriter") +end + +-- Génère un identifiant de badge unique. +function writer.newCardId() + return "SEC-" .. util.uuid(24) +end + +-- Grave un cardId sur la carte physique posée dans le graveur. +-- Renvoie true, ou (nil, raison). +function writer.write(cardId, label) + if not writer.available() then return nil, "no_writer" end + local w = component.os_cardwriter + local ok = pcall(function() + -- Ordre des arguments probable : (data, label, isRewritable) — à ajuster une fois testé. + w.write(cardId, label or "SecSite Badge", true) + end) + if not ok then return nil, "write_failed" end + return true +end + +return writer diff --git a/Libraries/mineos/login-fork/autorun.lua b/Libraries/mineos/login-fork/autorun.lua new file mode 100644 index 00000000..38c70245 --- /dev/null +++ b/Libraries/mineos/login-fork/autorun.lua @@ -0,0 +1,70 @@ +-- mineos/login-fork/autorun.lua +-- Enrobage NON INVASIF du login MineOS (pas d'édition de System.lua) : +-- * ajoute l'auth par carte à l'écran de connexion (en plus du mot de passe MineOS) +-- * applique le branding SecSite (bannière) +-- * mode kiosque optionnel (lance SecurityConsole après login) +-- À exécuter au démarrage de MineOS (installé comme autorun par install/secsite_os.lua). +-- +-- ⚠️ S'appuie sur des fonctions publiques de l'API System de MineOS (setUser/updateDesktop) ; +-- les noms exacts sont À CONFIRMER en jeu. Tout est protégé par pcall : en cas d'échec, le +-- login mot de passe MineOS reste pleinement fonctionnel. + +local ROOT = (os.getenv and os.getenv("SECSITE_ROOT")) or "/home/secsite" +package.path = ROOT .. "/?.lua;" .. ROOT .. "/?/init.lua;" .. package.path + +local ok, system = pcall(require, "System") +if not ok then return end + +local patch = require("mineos/login-fork/patch") +local session = require("mineos/lib/session") +local branding = require("shared/branding") + +-- Lecture du drapeau kiosque depuis secsite.cfg. +local function kioskEnabled() + local f = io.open(ROOT .. "/secsite.cfg", "r") + if not f then return false end + local data = f:read("*a"); f:close() + local chunk = load("return " .. (data or ""), "=cfg", "t", {}) + if not chunk then return false end + local okc, cfg = pcall(chunk) + return okc and type(cfg) == "table" and cfg.kiosk == true +end + +-- Bascule vers le bureau pour un profil donné (via API publique MineOS). +local function loginAs(userName) + local done = false + if system.setUser then done = pcall(system.setUser, userName) end + if system.updateDesktop then pcall(system.updateDesktop) end + return done +end + +-- Après connexion : en mode kiosque, lance SecurityConsole en avant-plan. +local function postLogin() + if not kioskEnabled() then return end + pcall(function() + dofile(ROOT .. "/mineos/apps/SecurityConsole.app/Main.lua") + end) +end + +-- Enrobe system.authorize : démarre l'écoute carte pendant l'écran de login. +if not system.__secsitePatched and system.authorize then + system.__secsitePatched = true + local realAuthorize = system.authorize + function system.authorize(...) + local stop = patch.cardListener(function(userName, sess) + session.set(sess) + if loginAs(userName) then + if stop then stop() end + postLogin() + end + end) + local res = { pcall(realAuthorize, ...) } + if stop then stop() end + return table.unpack(res, 2) + end +end + +-- Bannière SecSite (visible si un terminal texte est disponible au boot). +pcall(function() + for _, line in ipairs(branding.BANNER) do print(line) end +end) diff --git a/Libraries/mineos/login-fork/patch.lua b/Libraries/mineos/login-fork/patch.lua new file mode 100644 index 00000000..8f7f9c7c --- /dev/null +++ b/Libraries/mineos/login-fork/patch.lua @@ -0,0 +1,87 @@ +-- mineos/login-fork/patch.lua +-- Voie d'authentification « carte » ajoutée à l'écran de connexion MineOS. +-- Conçu pour être appelé DEPUIS le login MineOS forké (voir install.lua) : il écoute un swipe, +-- résout la carte auprès du serveur, et si un compte correspond, autorise l'ouverture de session. +-- +-- La voie « login + mot de passe » native de MineOS reste inchangée : ce module ajoute la carte +-- À CÔTÉ, il ne remplace rien. + +local ROOT = (os.getenv and os.getenv("SECSITE_ROOT")) or "/home/secsite" +package.path = ROOT .. "/?.lua;" .. ROOT .. "/?/init.lua;" .. package.path + +local net = require("mineos/lib/net") +local card = require("mineos/lib/card") +local session = require("mineos/lib/session") +local blackout = require("mineos/lib/blackout") + +local patch = {} + +-- Tente une connexion par carte (bloquant jusqu'au swipe ou timeout). +-- Renvoie une session serveur { token, name, role } ou (nil, raison). +function patch.tryCardLogin(timeout) + if not card.available() then return nil, "no_reader" end + local cardId = card.await(timeout) + if not cardId then return nil, "timeout" end + local resp, err = net.loginCard(cardId) + if not resp then return nil, err or "no_server" end + if not resp.ok then return nil, resp.error end + return resp.data +end + +-- Boucle d'écoute non bloquante à brancher dans le workspace du login MineOS. +-- `onSuccess(session)` est appelé quand une carte valide est présentée. +-- Le login MineOS choisit ensuite le profil (même nom) et ouvre la session. +function patch.attach(onSuccess, onReject) + return function() + -- Override « poste inaccessible » : si un blackout est actif, on prend l'écran d'abord. + if blackout.enforce() then return end + local s, reason = patch.tryCardLogin(0.5) + if s then + session.set(s) -- rend le token disponible aux apps de sécurité + if onSuccess then onSuccess(s) end + elseif reason and reason ~= "timeout" and reason ~= "no_reader" then + if onReject then onReject(reason) end + end + end +end + +-- Écouteur NON bloquant pour l'écran de login MineOS forké (system.authorize). +-- Enregistre les événements OpenSecurity (magData/rfidData) — et déclenche les scans RFID — +-- puis, sur carte valide, appelle onUser(nomDeCompte, session). À insérer DANS system.authorize +-- (voir login-fork/install.lua) : le nom de compte doit correspondre à un profil MineOS. +-- Renvoie une fonction stop() à appeler quand on quitte l'écran de login. +function patch.cardListener(onUser, onReject) + local event = require("event") + local reader, kind = card.reader() + + local function handle(cardId) + if not cardId or cardId == "" then return end + local resp = net.loginCard(cardId) + if resp and resp.ok then + session.set(resp.data) + if onUser then onUser(resp.data.name, resp.data) end + elseif onReject then + onReject(resp and resp.error or "no_server") + end + end + + -- magData: (_, address, playerName, cardData, cardUniqueId, isCardLocked, side) + local onMag = function(_, _, _, cardData, cardUniqueId) handle(cardData or cardUniqueId) end + -- rfidData: (_, uuid, playerName, distance, data) + local onRfid = function(_, _, _, _, data) handle(data) end + event.listen("magData", onMag) + event.listen("rfidData", onRfid) + + local timer + if kind == "rfid" and reader then + timer = event.timer(1.5, function() pcall(reader.scan) end, math.huge) + end + + return function() + event.ignore("magData", onMag) + event.ignore("rfidData", onRfid) + if timer then event.cancel(timer) end + end +end + +return patch diff --git a/Libraries/shared/branding.lua b/Libraries/shared/branding.lua new file mode 100644 index 00000000..4813ea20 --- /dev/null +++ b/Libraries/shared/branding.lua @@ -0,0 +1,24 @@ +-- shared/branding.lua +-- Identité visuelle « SecSite OS » (nom, couleurs, bannière). Utilisée par l'enrobage de login, +-- le kiosque et (optionnellement) l'en-tête des apps. + +local branding = {} + +branding.NAME = "SecSite OS" +branding.TAGLINE = "Terminal de sécurité" +branding.COLORS = { + bg = 0x0E0E12, + panel = 0x1C1C24, + accent = 0x2E7D32, + alert = 0xB71C1C, + text = 0xFFFFFF, +} + +branding.BANNER = { + "==============================", + " S E C S I T E ", + " Terminal de sécurité OC ", + "==============================", +} + +return branding diff --git a/Libraries/shared/defense.lua b/Libraries/shared/defense.lua new file mode 100644 index 00000000..ee6388ea --- /dev/null +++ b/Libraries/shared/defense.lua @@ -0,0 +1,21 @@ +-- shared/defense.lua +-- Emplacements de contre-mesures anti-missile (CIWS, silo intercepteur…). +-- Actionnés en redstone (tourelle/CIWS) ou via composant (silo HBM launch). Mode par défaut au boot. + +local defense = {} + +defense.DEFAULT_MODE = "manual" -- "off" | "manual" | "auto" +defense.ENGAGE_LEVEL = 2 -- en mode auto : engage si DEFCON <= ce niveau + +defense.EMPLACEMENTS = { + { + id = "ciws_1", name = "CIWS Nord", kind = "redstone", + side = "up", channel = 6, -- impulsion redstone d'activation + }, + { + id = "interceptor_1", name = "Silo intercepteur", kind = "component", + address = "REPLACE_WITH_HBM_SILO_ADDRESS", -- composant hbm_silo (launch) + }, +} + +return defense diff --git a/Libraries/shared/doors.lua b/Libraries/shared/doors.lua new file mode 100644 index 00000000..29d458d7 --- /dev/null +++ b/Libraries/shared/doors.lua @@ -0,0 +1,81 @@ +-- shared/doors.lua +-- Configuration DÉCLARATIVE des portes de l'installation. +-- Ajouter une porte = ajouter une entrée ici (aucun code à écrire). +-- Le moteur de portes (server/services/doors.lua, lot 2) interprète `type` et `driver`. +-- +-- driver.kind : +-- "redstone" -> contrôleur HBM / porte blast / trappe silo, via redstone +-- (side + éventuellement channel bundled Project Red) +-- "os_secdoor"-> composant OpenSecurity adressé (address) +-- type : "simple" | "bunker" | "shelter" | "airlock" | "silo" + +local doors = {} + +doors.CONFIG = { + { + id = "lobby", + name = "Porte d'accueil", + type = "simple", + zone = "lobby", + roles = { "admin", "agent", "invite" }, + driver = { kind = "os_secdoor", address = "REPLACE_WITH_SECDOOR_ADDRESS" }, + }, + { + id = "bunker_main", + name = "Entrée bunker", + type = "bunker", + zone = "A", + roles = { "admin", "agent" }, + driver = { kind = "redstone", side = "north", channel = 1 }, + }, + { + id = "airlock_A", + name = "Sas arrivée/sortie A", + type = "airlock", + zone = "A", + roles = { "admin", "agent" }, + -- Deux battants interverrouillés : un canal par battant. + driver = { kind = "redstone", side = "north", channelInner = 2, channelOuter = 3 }, + cycleSeconds = 3, -- délai de sécurité entre battants + }, + { + id = "shelter_1", + name = "Porte abri", + type = "shelter", + zone = "B", + roles = { "admin", "agent" }, + driver = { kind = "redstone", side = "up", channel = 4 }, + }, + { + id = "silo_hatch", + name = "Trappe silo", + type = "silo", + zone = "silo", + roles = { "admin" }, -- + permission restreinte "door:silo" + driver = { kind = "redstone", side = "south", channel = 5 }, + }, + { + id = "bunker_oc", + name = "Porte bunker (via addon OC HBM)", + type = "bunker", + zone = "A", + roles = { "admin", "agent" }, + -- Pilotage direct par composant HBM exposé par hbm-oc-addon (au lieu du redstone). + driver = { kind = "hbm_oc", address = "REPLACE_WITH_HBM_DOOR_ADDRESS" }, + }, +} + +function doors.get(id) + for _, d in ipairs(doors.CONFIG) do + if d.id == id then return d end + end + return nil +end + +-- Permission associée à une porte (ex. "door:A", "door:silo"). +function doors.permission(door) + if door.type == "silo" then return "door:silo" end + return "door:" .. (door.zone or door.id) +end + +return doors diff --git a/Libraries/shared/netsec.lua b/Libraries/shared/netsec.lua new file mode 100644 index 00000000..9e47a225 --- /dev/null +++ b/Libraries/shared/netsec.lua @@ -0,0 +1,83 @@ +-- shared/netsec.lua +-- Sécurité du réseau privé de l'intranet : +-- * secret partagé (déployé uniquement sur les machines admises) +-- * signature HMAC-SHA256 de chaque message (authenticité + intégrité) +-- * nonce + horodatage -> fenêtre anti-rejeu +-- * liste blanche d'adresses de composants +-- Un ordinateur qui ne connaît pas le secret ne peut ni forger ni lire un message valide. + +local util = require("shared/util") +local sha2 = require("shared/sha2") + +local netsec = {} + +netsec.secret = "CHANGE_ME_DEFAULT_SECRET" -- à remplacer par un fichier de config déployé +netsec.window = 30 -- tolérance d'horloge / anti-rejeu (secondes) +netsec.whitelist = nil -- nil = pas de filtrage ; table[addr]=true sinon + +function netsec.setSecret(s) + assert(type(s) == "string" and #s >= 8, "netsec: secret trop court") + netsec.secret = s +end + +-- Liste blanche : table {address = true}. nil pour désactiver. +function netsec.setWhitelist(set) + netsec.whitelist = set +end + +function netsec.allow(addr) + netsec.whitelist = netsec.whitelist or {} + netsec.whitelist[addr] = true +end + +function netsec.isAllowed(addr) + if not netsec.whitelist then return true end + return netsec.whitelist[addr] == true +end + +function netsec.mac(data) + return sha2.hmac(netsec.secret, data) +end + +-- Emballe une charge utile (string déjà sérialisée) dans une enveloppe signée. +function netsec.wrap(payload) + assert(type(payload) == "string", "netsec.wrap attend une string") + local body = tostring(util.now()) .. "|" .. util.uuid(16) .. "|" .. payload + return { b = body, m = netsec.mac(body) } +end + +-- Vérifie et déballe. Renvoie (payload, nonce) ou (nil, raison). +function netsec.unwrap(env) + if type(env) ~= "table" or type(env.b) ~= "string" or type(env.m) ~= "string" then + return nil, "malformed" + end + if netsec.mac(env.b) ~= env.m then + return nil, "bad_mac" + end + local ts, nonce, payload = env.b:match("^(%d+)|([^|]+)|(.*)$") + if not ts then + return nil, "bad_body" + end + if netsec.window and math.abs(util.now() - tonumber(ts)) > netsec.window then + return nil, "expired" + end + return payload, nonce +end + +-- Format « fil » : table -> string signée prête à envoyer sur le modem. +function netsec.encode(tbl) + return util.serialize(netsec.wrap(util.serialize(tbl))) +end + +-- string reçue -> table, ou (nil, raison) si signature/format invalide. +function netsec.decode(str) + local env = util.deserialize(str) + if type(env) ~= "table" then return nil, "decode" end + local payload, reason = netsec.unwrap(env) + if not payload then return nil, reason end + local tbl = util.deserialize(payload) + if type(tbl) ~= "table" then return nil, "payload" end + return tbl +end + +return netsec diff --git a/Libraries/shared/protocol.lua b/Libraries/shared/protocol.lua new file mode 100644 index 00000000..cbffd549 --- /dev/null +++ b/Libraries/shared/protocol.lua @@ -0,0 +1,94 @@ +-- shared/protocol.lua +-- Contrat de communication terminal/agent/tablette <-> serveur. +-- Les messages sont des tables sérialisées (util.serialize) puis emballées (netsec.wrap). + +local protocol = {} + +protocol.VERSION = 1 +protocol.PORT = 2412 -- port modem dédié à l'intranet de sécurité + +-- Types de requêtes (client -> serveur). +protocol.REQ = { + PING = "ping", + AUTH_CARD = "auth.card", -- { cardId } -> résout un compte via badge + AUTH_PASSWORD = "auth.password", -- { name, password } -> login classique + SESSION_CHECK = "session.check", -- { token } + LOGOUT = "auth.logout", -- { token } + ACCOUNT_LIST = "account.list", -- { token } + ACCOUNT_CREATE = "account.create", -- { token, name, role, cardId?, password? } + ACCOUNT_DELETE = "account.delete", -- { token, id } + LOG_QUERY = "log.query", -- { token, limit? } + DOOR_LIST = "door.list", -- { token } + DOOR_CMD = "door.cmd", -- { token, id, action } + RADAR_STATE = "radar.state", -- { token } + SESSION_LIST = "session.list", -- { token } + ACCOUNT_SETROLE = "account.setrole", -- { token, id, role } + ACCOUNT_SETCARD = "account.setcard", -- { token, id, cardId? } + -- Flotte (lot 4) : + NODE_REGISTER = "node.register", -- { token?, address, kind } + NODE_CMD = "node.cmd", -- { token, address, command } + NODE_LIST = "node.list", -- { token } + -- Collaboration (lot 5) : + ANNOUNCE_POST = "announce.post", -- { token, text } + BOARD_GET = "board.get", -- { token } + MSG_SEND = "msg.send", -- { token, to, text } + MSG_INBOX = "msg.inbox", -- { token } + -- Salle de contrôle & protocoles (lot 7) : + SITUATION_GET = "situation.get", -- { token } + PROTOCOL_LIST = "protocol.list", -- { token } + PROTOCOL_RUN = "protocol.run", -- { token, code, drill } + -- Supervision réacteur & contre-mesures : + POWER_STATE = "power.state", -- { token } + REACTOR_SCRAM = "reactor.scram", -- { token, id } + DEFENSE_STATE = "defense.state", -- { token } + DEFENSE_MODE = "defense.mode", -- { token, mode } + DEFENSE_FIRE = "defense.fire", -- { token } + -- Kiosque public (sans token) & configuration : + KIOSK_GET = "kiosk.get", -- {} (aucun token requis : info publique) + SETTINGS_GET = "settings.get", -- { token } + SETTINGS_SET = "settings.set", -- { token, key, value } +} + +-- Actions de porte acceptées par DOOR_CMD. +protocol.DOOR_ACTIONS = { + open = true, close = true, + inner_open = true, inner_close = true, + outer_open = true, outer_close = true, + lockdown = true, release = true, +} + +-- Types d'événements diffusés (serveur -> clients). +protocol.EVT = { + ALERT = "evt.alert", -- montée DEFCON / missile + LOCKDOWN = "evt.lockdown", + ANNOUNCE = "evt.announce", -- annonce / bulletin +} + +-- Messages serveur -> agent de nœud (gestion à distance). +protocol.AGENT = { + EXEC = "agent.exec", -- { command } où command ∈ NODE_COMMANDS +} + +protocol.NODE_COMMANDS = { + reboot = true, shutdown = true, lock = true, status = true, + blackout = true, release = true, -- override « poste inaccessible » + levée +} + +-- Construit une requête. +function protocol.request(rtype, payload) + local req = payload or {} + req.v = protocol.VERSION + req.t = rtype + return req +end + +-- Réponses normalisées. +function protocol.ok(data) + return { ok = true, data = data } +end + +function protocol.err(reason) + return { ok = false, error = reason } +end + +return protocol diff --git a/Libraries/shared/protocols.lua b/Libraries/shared/protocols.lua new file mode 100644 index 00000000..05a17bd8 --- /dev/null +++ b/Libraries/shared/protocols.lua @@ -0,0 +1,58 @@ +-- shared/protocols.lua +-- Protocoles déclaratifs : séquences d'actions lançables par CODE (drill ou override réel). +-- Une step = { type=..., ... }. Types gérés par server/services/protocols.lua : +-- announce{text} · alarm{on} · lockdown · release · disable_nodes{exclude} · wait{s} +-- drillable=true : le protocole peut être joué en simulation (mode drill = non destructif). + +local protocols = {} + +protocols.LIST = { + { + id = "drill_evac", name = "Drill évacuation", code = "1111", role = "agent", drillable = true, + steps = { + { type = "announce", text = "DRILL: procédure d'évacuation" }, + { type = "alarm", on = true }, + { type = "wait", s = 3 }, + { type = "alarm", on = false }, + }, + }, + { + id = "lockdown_full", name = "Confinement total", code = "2222", role = "admin", drillable = true, + steps = { + { type = "announce", text = "CONFINEMENT TOTAL EN COURS" }, + { type = "alarm", on = true }, + { type = "lockdown" }, + }, + }, + { + id = "release_all", name = "Levée du confinement", code = "0000", role = "admin", drillable = false, + steps = { + { type = "release" }, + { type = "alarm", on = false }, + { type = "announce", text = "Fin de confinement" }, + }, + }, + { + id = "blackout", name = "Coupure des postes (override)", code = "9999", role = "admin", drillable = false, + steps = { + { type = "announce", text = "OVERRIDE: coupure des postes de travail" }, + { type = "disable_nodes", exclude = "display" }, -- épargne les écrans du mur + }, + }, + { + id = "restore_nodes", name = "Rétablir les postes", code = "9990", role = "admin", drillable = false, + steps = { + { type = "restore_nodes", exclude = "display" }, + { type = "announce", text = "Postes rétablis" }, + }, + }, +} + +function protocols.get(idOrCode) + for _, p in ipairs(protocols.LIST) do + if p.id == idOrCode or p.code == idOrCode then return p end + end + return nil +end + +return protocols diff --git a/Libraries/shared/reactors.lua b/Libraries/shared/reactors.lua new file mode 100644 index 00000000..05eb62ce --- /dev/null +++ b/Libraries/shared/reactors.lua @@ -0,0 +1,31 @@ +-- shared/reactors.lua +-- Machines HBM à superviser (réacteurs / gros consommateurs d'énergie). +-- Lues via l'addon OC HBM (composants hbm_reactor / hbm_machine). Ajouter une machine = une entrée. + +local reactors = {} + +reactors.CONFIG = { + { + id = "reactor_1", + name = "Réacteur principal", + address = "REPLACE_WITH_HBM_REACTOR_ADDRESS", -- composant hbm_reactor + tempWarn = 800, -- seuil d'avertissement + tempCrit = 1000, -- seuil critique (déclenche alarme + SCRAM auto si activé) + autoScram = true, -- arrêt d'urgence automatique au seuil critique + }, + { + id = "grid", + name = "Réseau énergie", + address = "REPLACE_WITH_HBM_MACHINE_ADDRESS", -- composant hbm_machine (getEnergy) + energyOnly = true, -- pas de température : supervision d'énergie seule + }, +} + +function reactors.get(id) + for _, r in ipairs(reactors.CONFIG) do + if r.id == id then return r end + end + return nil +end + +return reactors diff --git a/Libraries/shared/roles.lua b/Libraries/shared/roles.lua new file mode 100644 index 00000000..f4aad784 --- /dev/null +++ b/Libraries/shared/roles.lua @@ -0,0 +1,64 @@ +-- shared/roles.lua +-- Rôles et permissions. Vérifiés CÔTÉ SERVEUR (jamais faire confiance au client). +-- Une permission est une chaîne ; le suffixe "*" agit comme joker (ex. "door:*"). + +local roles = {} + +roles.LIST = { "admin", "agent", "invite" } + +roles.PERMISSIONS = { + admin = { "*" }, -- tout + agent = { + "view_dashboard", + "view_logs", + "ack_alarm", + "lockdown", + "announce", -- publier une annonce / bulletin + "drill", -- lancer un protocole en mode simulation + "door:*", -- toutes les portes de zone... + -- (le silo reste réservé à admin : "door:silo" n'est PAS couvert, voir ci-dessous) + }, + invite = { + "view_dashboard", + "door:lobby", + }, +} + +-- Permissions sensibles qui exigent un rôle explicite même si un joker pourrait matcher. +-- Ex : la trappe de silo ne doit jamais tomber sous "door:*". +roles.RESTRICTED = { + ["door:silo"] = { admin = true }, +} + +local function matches(pattern, perm) + if pattern == perm then return true end + local prefix = pattern:match("^(.-)%*$") + if prefix then + return perm:sub(1, #prefix) == prefix + end + return false +end + +-- roles.can(role, permission) -> booléen +function roles.can(role, permission) + if type(role) ~= "string" or type(permission) ~= "string" then return false end + + -- Restriction explicite : seul un rôle listé passe, joker ignoré. + local restricted = roles.RESTRICTED[permission] + if restricted then + return restricted[role] == true + end + + local perms = roles.PERMISSIONS[role] + if not perms then return false end + for _, p in ipairs(perms) do + if matches(p, permission) then return true end + end + return false +end + +function roles.isRole(role) + return roles.PERMISSIONS[role] ~= nil +end + +return roles diff --git a/Libraries/shared/sha2.lua b/Libraries/shared/sha2.lua new file mode 100644 index 00000000..916feea9 --- /dev/null +++ b/Libraries/shared/sha2.lua @@ -0,0 +1,99 @@ +-- shared/sha2.lua +-- SHA-256 + HMAC-SHA256 en Lua 5.3 pur (entiers 64 bits + opérateurs bit à bit + string.pack). +-- Aucune dépendance matérielle (pas besoin d'une Data Card OpenComputers). +-- Vérifié contre les vecteurs de test standard (voir tools/test/run.lua). + +local sha2 = {} + +local MASK = 0xFFFFFFFF + +local function rotr(x, n) + return ((x >> n) | (x << (32 - n))) & MASK +end + +local function shr(x, n) + return (x >> n) & MASK +end + +local function bnot32(x) + return (~x) & MASK +end + +local K = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, +} + +-- Renvoie les 8 mots d'état (h0..h7) après hachage de msg. +local function core(msg) + local h0, h1, h2, h3 = 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a + local h4, h5, h6, h7 = 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 + + local bitlen = #msg * 8 + msg = msg .. "\128" + while (#msg % 64) ~= 56 do msg = msg .. "\0" end + msg = msg .. string.pack(">I4", (bitlen >> 32) & MASK) .. string.pack(">I4", bitlen & MASK) + + local w = {} + for chunk = 1, #msg, 64 do + for j = 0, 15 do + w[j] = (string.unpack(">I4", msg, chunk + j * 4)) + end + for j = 16, 63 do + local a15, a2 = w[j - 15], w[j - 2] + local s0 = rotr(a15, 7) ~ rotr(a15, 18) ~ shr(a15, 3) + local s1 = rotr(a2, 17) ~ rotr(a2, 19) ~ shr(a2, 10) + w[j] = (w[j - 16] + s0 + w[j - 7] + s1) & MASK + end + + local a, b, c, d = h0, h1, h2, h3 + local e, f, g, h = h4, h5, h6, h7 + for j = 0, 63 do + local S1 = rotr(e, 6) ~ rotr(e, 11) ~ rotr(e, 25) + local ch = (e & f) ~ (bnot32(e) & g) + local t1 = (h + S1 + ch + K[j + 1] + w[j]) & MASK + local S0 = rotr(a, 2) ~ rotr(a, 13) ~ rotr(a, 22) + local maj = (a & b) ~ (a & c) ~ (b & c) + local t2 = (S0 + maj) & MASK + h = g; g = f; f = e; e = (d + t1) & MASK + d = c; c = b; b = a; a = (t1 + t2) & MASK + end + + h0 = (h0 + a) & MASK; h1 = (h1 + b) & MASK; h2 = (h2 + c) & MASK; h3 = (h3 + d) & MASK + h4 = (h4 + e) & MASK; h5 = (h5 + f) & MASK; h6 = (h6 + g) & MASK; h7 = (h7 + h) & MASK + end + + return h0, h1, h2, h3, h4, h5, h6, h7 +end + +-- Digest binaire (32 octets). +function sha2.digest(msg) + return string.pack(">I4I4I4I4I4I4I4I4", core(msg)) +end + +-- Digest hexadécimal (64 caractères). +function sha2.sha256(msg) + return (string.format("%08x%08x%08x%08x%08x%08x%08x%08x", core(msg))) +end + +-- HMAC-SHA256 -> hex. +function sha2.hmac(key, msg) + local B = 64 + if #key > B then key = sha2.digest(key) end + key = key .. string.rep("\0", B - #key) + local ipad, opad = {}, {} + for n = 1, B do + local kb = string.byte(key, n) + ipad[n] = string.char(kb ~ 0x36) + opad[n] = string.char(kb ~ 0x5c) + end + return sha2.sha256(table.concat(opad) .. sha2.digest(table.concat(ipad) .. msg)) +end + +return sha2 diff --git a/Libraries/shared/site.lua b/Libraries/shared/site.lua new file mode 100644 index 00000000..e2ff25ce --- /dev/null +++ b/Libraries/shared/site.lua @@ -0,0 +1,17 @@ +-- shared/site.lua +-- Paramètres physiques de l'installation, utilisés par l'agrégateur de situation +-- (ETA impact, risque, temps de lockdown complet). À ajuster aux coordonnées réelles en jeu. + +local site = {} + +site.CONFIG = { + name = "Installation Alpha", + center = { x = 0, y = 64, z = 0 }, -- centre du site (coordonnées monde) + radius = 64, -- rayon considéré « installation » (blocs) + lockdown = { + perDoorSeconds = 2, -- durée de fermeture d'une porte simple/bunker/shelter + airlockSeconds = 3, -- durée de cycle d'un sas + }, +} + +return site diff --git a/Libraries/shared/util.lua b/Libraries/shared/util.lua new file mode 100644 index 00000000..c558e14e --- /dev/null +++ b/Libraries/shared/util.lua @@ -0,0 +1,91 @@ +-- shared/util.lua +-- Petits utilitaires portables (OpenComputers / Lua 5.3 standard). +-- Pas de dépendance à un composant : utilisable côté serveur, terminal, agent et en test. + +local util = {} + +-- Horloge : uptime monotone si dispo (OC expose `computer`), sinon os.clock. +function util.uptime() + if type(_G.computer) == "table" and computer.uptime then + return computer.uptime() + end + return os.clock() +end + +-- Timestamp epoch (secondes). os.time existe sur OpenOS et en Lua standard. +function util.now() + return os.time() +end + +-- Horodatage lisible pour les logs. +function util.stamp(t) + return os.date("!%Y-%m-%d %H:%M:%S", t or util.now()) +end + +-- Identifiant court (8 hex). Suffisant pour des ids internes / nonces. +local seeded = false +local function seed() + if seeded then return end + seeded = true + local s = (util.now() * 1000) + math.floor((util.uptime() * 1000) % 1000000) + math.randomseed(s) +end + +function util.uuid(len) + seed() + len = len or 8 + local out = {} + for i = 1, len do + out[i] = string.format("%x", math.random(0, 15)) + end + return table.concat(out) +end + +-- Jeton de session plus long. +function util.token() + return util.uuid(32) +end + +-- Sérialisation Lua minimale (tables de string/number/boolean, imbriquées). +function util.serialize(v) + local t = type(v) + if t == "nil" then + return "nil" + elseif t == "number" or t == "boolean" then + return tostring(v) + elseif t == "string" then + return string.format("%q", v) + elseif t == "table" then + local parts = {} + for k, val in pairs(v) do + local key + if type(k) == "string" and k:match("^[%a_][%w_]*$") then + key = k + else + key = "[" .. util.serialize(k) .. "]" + end + parts[#parts + 1] = key .. "=" .. util.serialize(val) + end + return "{" .. table.concat(parts, ",") .. "}" + end + error("util.serialize: type non supporté: " .. t) +end + +-- Désérialisation en environnement vide (aucun accès aux globales -> pas d'exécution). +function util.deserialize(s) + if type(s) ~= "string" then return nil end + local f = load("return " .. s, "=data", "t", {}) + if not f then return nil end + local ok, res = pcall(f) + if ok then return res end + return nil +end + +-- Copie superficielle (pratique pour renvoyer un état sans exposer la table interne). +function util.shallow(t) + local out = {} + for k, v in pairs(t) do out[k] = v end + return out +end + +return util