diff --git a/routes.js b/routes.js new file mode 100644 index 0000000..14d99af --- /dev/null +++ b/routes.js @@ -0,0 +1,104 @@ +const express = require('express'); +const router = express.Router(); +const { connectDB } = require('./database/db'); + +module.exports = function(clients, approvedSessions, activeSessions) { + // Status endpoint + router.get("/status", (req, res) => { + res.json({ + status: "Remote Control Gateway is running", + connectedClients: clients.size + }); + }); + + // Get active devices + router.get("/devices/active", async (req, res) => { + try { + const db = await connectDB(); + const devicesCollection = db.collection('devices'); + const activeDevices = await devicesCollection.find({ status: "active" }).toArray(); + res.json(activeDevices); + } catch (error) { + console.error("Error fetching active devices:", error); + res.status(500).json({ error: "Failed to fetch active devices" }); + } + }); + + // Get session logs for device + router.get("/session/logs/:deviceId", async (req, res) => { + const { deviceId } = req.params; + try { + const db = await connectDB(); + const sessionLogsCollection = db.collection('sessionLogs'); + const logs = await sessionLogsCollection.find({ deviceId }).toArray(); + res.json(logs); + } catch (error) { + console.error("Error fetching session logs:", error); + res.status(500).json({ error: "Failed to fetch session logs" }); + } + }); + + // Deregister device + router.post("/devices/deregister", async (req, res) => { + const { deviceId, deregistrationKey } = req.body; + + if (!deviceId || !deregistrationKey) { + return res.status(400).json({ error: "Missing required fields: deviceId, deregistrationKey" }); + } + + try { + const db = await connectDB(); + const devicesCollection = db.collection('devices'); + const device = await devicesCollection.findOne({ deviceId }); + + if (!device) { + return res.status(404).json({ error: `Device with ID ${deviceId} not found.` }); + } + + if (device.deregistrationKey !== deregistrationKey) { + return res.status(400).json({ error: "Invalid deregistration key." }); + } + + await devicesCollection.deleteOne({ deviceId }); + + if (clients.has(deviceId)) { + const ws = clients.get(deviceId); + ws.close(); + clients.delete(deviceId); + } + + res.status(200).json({ + message: `Device ${deviceId} deregistered successfully`, + deviceRemoved: true, + wsDisconnected: clients.has(deviceId) + }); + } catch (error) { + console.error("Error during device deregistration:", error); + res.status(500).json({ error: "Internal server error" }); + } + }); + + // Remove test sessions + router.post('/removeSessions', (req, res) => { + const { token, deviceId } = req.body; + + if (!token) { + return res.status(400).json({ + success: false, + message: 'Token is required.' + }); + } + + const removedFromApproved = approvedSessions.delete(deviceId); + const removedFromActive = activeSessions.delete(token); + + res.json({ + success: true, + message: 'Session cleanup completed', + approvedSessionRemoved: removedFromApproved, + activeSessionRemoved: removedFromActive + }); + }); + + return router; +}; \ No newline at end of file diff --git a/server.js b/server.js index 701c5f5..242e4c8 100644 --- a/server.js +++ b/server.js @@ -1,17 +1,26 @@ const express = require("express"); -const fs = require("fs"); -//const https = require("https"); const http = require('http'); const WebSocket = require("ws"); -const jwt = require("jsonwebtoken"); -const { verifySessionToken } = require("./utils/authSession"); const cors = require("cors"); const dotenv = require('dotenv'); const { logSessionEvent } = require("./utils/sessionLogger"); dotenv.config(); -const activeSessions = new Map(); // Store sessionId with deviceId before admin approval -const approvedSessions = new Map(); // Store approved sessions +const { + initialize: initializeWebSocketHandlers, + handleDeviceRegistration, + handleDeviceDeregistration, + handleDeviceStatus, + handleDeviceSignal, + handleDeviceDisconnect, + handleSessionRequest, + handleSessionFinalConfirmation +} = require('/websocketHandlers'); + +const activeSessions = new Map(); +const approvedSessions = new Map(); +let clients = new Map(); +let lastHeartbeat = new Map(); const { connectDB } = require("./database/db"); const { status } = require("migrate-mongo"); @@ -19,273 +28,23 @@ const app = express(); app.use(cors()); app.use(express.json()); -let webAdminWs = new WebSocket('wss://backend-wf7e.onrender.com/ws/control/comm'); +const setupRoutes = require('./routes'); const HEARTBEAT_TIMEOUT = 600 * 1000; const HEARTBEAT_CHECK_INTERVAL = 30 * 1000; -let clients = new Map(); // Store connected devices with their WebSocket connections -let lastHeartbeat = new Map(); //---2.task -function sendToDevice(deviceId, payload) { - console.log("Broj klijenata:", clients.size, "->", Array.from(clients.keys())); - const ws = clients.get(deviceId); - if (ws && ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify(payload)); - } else { - console.warn(`Device ${deviceId} not connected.`); - } -} - - -async function connectToWebAdmin() { - console.log((`Connecting to Web Admin at ${webAdminWs.url}`)); - - webAdminWs = new WebSocket('wss://backend-wf7e.onrender.com/ws/control/comm'); - - webAdminWs.on('open', () => { - console.log('>>> COMM LAYER: Successfully connected to Web Admin WS (Backend)!'); - }); - - webAdminWs.on('message', (message) => { - try { - const data = JSON.parse(message); - console.log('\nCOMM LAYER: Received message from Web Admin WS:', data); - - switch (data.type) { - case "request_received": - console.log(`COMM LAYER: Backend acknowledged request for session ${data.sessionId}`); - logSessionEvent(data.sessionId, activeSessions.get(data.sessionId), data.type, "Backend acknowledged request for session."); - break; - - case "control_decision": - console.log("COMM LAYER: Processing control_decision from Backend."); - const { sessionId: decisionSessionId, decision, reason } = data; - const deviceId = activeSessions.get(decisionSessionId); - - if (!deviceId) { - console.error(`COMM LAYER: Received control_decision for session ${decisionSessionId}, but couldn't find deviceId in activeSessions.`); - logSessionEvent(decisionSessionId, 'unknown', data.type, `Failed: Could not find active device for session.`); - return; - } - - console.log(`COMM LAYER: Found deviceId ${deviceId} for session ${decisionSessionId}. Decision: ${decision}`); - - if (decision === "accepted") { - console.log(`COMM LAYER: Sending 'approved' message to device ${deviceId}`); - sendToDevice(deviceId, { - type: "approved", - sessionId: decisionSessionId, - message: "Admin approved the session request." - }); - logSessionEvent(decisionSessionId, deviceId, data.type, "Session approved by backend."); - - if (!approvedSessions.has(deviceId)) { - approvedSessions.set(deviceId, new Set()); - } - approvedSessions.get(deviceId).add("web-admin"); // Assuming "web-admin" is the peer identifier - - } else { - console.log(`COMM LAYER: Sending 'rejected' message to device ${deviceId}`); - sendToDevice(deviceId, { - type: "rejected", - sessionId: decisionSessionId, - message: `Admin rejected the session request. Reason: ${reason || 'N/A'}` - }); - logSessionEvent(decisionSessionId, deviceId, data.type, `Session rejected by backend. Reason: ${reason || 'N/A'}`); - activeSessions.delete(decisionSessionId); - const deviceApprovedPeers = approvedSessions.get(deviceId); - if (deviceApprovedPeers) { - deviceApprovedPeers.delete("web-admin"); // Remove potential peer added optimistically - if (deviceApprovedPeers.size === 0) { - approvedSessions.delete(deviceId); - } - } - } - break; - - // --- NEW CASE FOR SESSION TERMINATION --- - case "session_terminated": { - console.log(`COMM LAYER: Processing session_terminated for session ${data.sessionId}`); - const { sessionId: terminatedSessionId, reason } = data; - - - let deviceIdForTermination = activeSessions.get(terminatedSessionId); - - if (!deviceIdForTermination) { - console.warn(`COMM LAYER: Received termination for session ${terminatedSessionId}, but couldn't map it back to a deviceId in activeSessions. Ignoring notification to device, but cleaning up.`); - logSessionEvent(terminatedSessionId, 'unknown', data.type, `Termination received, but device mapping lost. Reason: ${reason || 'N/A'}`); - activeSessions.delete(terminatedSessionId); // Attempt cleanup anyway - return; - } - - console.log(`COMM LAYER: Found deviceId ${deviceIdForTermination} for terminated session ${terminatedSessionId}. Reason: ${reason}`); - logSessionEvent(terminatedSessionId, deviceIdForTermination, data.type, `Session terminated by backend. Reason: ${reason || 'N/A'}`); - - // --- Cleanup Logic --- - activeSessions.delete(terminatedSessionId); - console.log(`COMM LAYER: Deleted session ${terminatedSessionId} from activeSessions due to termination.`); - - const deviceApprovedPeers = approvedSessions.get(deviceIdForTermination); - if (deviceApprovedPeers) { - const deleted = deviceApprovedPeers.delete("web-admin"); // Peer identifier - if (deleted) console.log(`COMM LAYER: Removed 'web-admin' peer from approvedSessions for device ${deviceIdForTermination}.`); - - if (deviceApprovedPeers.size === 0) { - approvedSessions.delete(deviceIdForTermination); - console.log(`COMM LAYER: Removed device ${deviceIdForTermination} from approvedSessions as no peers remain.`); - } - } else { - console.log(`COMM LAYER: Device ${deviceIdForTermination} not found in approvedSessions during termination cleanup (might be normal).`); - } - - // --- Notify Device --- - console.log(`COMM LAYER: Sending 'session_ended' message to device ${deviceIdForTermination}`); - sendToDevice(deviceIdForTermination, { - type: "session_ended", - sessionId: terminatedSessionId, - reason: reason || 'terminated_by_admin' - }); - break; - } - - // --- WebRTC Signaling Cases --- - case "offer": - case "answer": - case "ice-candidate": { - const { fromId, toId, payload, type } = data; - - if (!fromId || !toId || !payload) { - console.warn(`COMM LAYER: Received invalid signaling message (${type}). Missing fields. Data:`, data); - logSessionEvent(data.sessionId || 'unknown', fromId || 'unknown', type, `Invalid signaling message received from backend.`); - break; - } - - console.log(`COMM LAYER: Relaying ${type} from backend peer (${fromId}) to device ${toId}`); - const target = clients.get(toId); - - if (target && target.readyState === WebSocket.OPEN) { - target.send(JSON.stringify({ type, fromId, toId, payload })); - logSessionEvent(data.sessionId || 'unknown', toId, type, `Relayed from backend peer ${fromId}.`); - } else { - console.warn(`COMM LAYER: Target device ${toId} for ${type} not found or not connected.`); - logSessionEvent(data.sessionId || 'unknown', toId, type, `Failed relay to device (not found/connected). From: ${fromId}`); - } - break; - } - - case "mouse_click": { - const { fromId, toId, sessionId, payload, type } = data; - - if (!fromId || !toId || !payload) { - console.warn(`COMM LAYER: Received invalid click message (${type}). Missing fields. Data:`, data); - logSessionEvent(data.sessionId || 'unknown', fromId || 'unknown', type, `Invalid signaling message received from backend.`); - break; - } - //const { sessionId, x, y, button } = data; - - /*const allowedPeers = approvedSessions.get(toId); - if (!allowedPeers || !allowedPeers.has(toId)) { - // ws.send(JSON.stringify({ type: "error", message: "Session not approved between devices." })); - logSessionEvent(sessionId, toId, "mouse_click", "Unauthorized attempt to send mouse input."); - return; - }*/ - console.log(`COMM LAYER: Relaying ${type} from backend peer (${fromId}) to device ${toId}`); - - const target = clients.get(toId); - if (target && target.readyState === WebSocket.OPEN) { - target.send(JSON.stringify({ - type: "click", - toId, - fromId, - payload - })); - logSessionEvent(sessionId, toId, "mouse_click", `Mouse clicks on cordinates (${payload.x}, ${payload.y}) sent to device.`); - } else { - //ws.send(JSON.stringify({ type: "error", message: "Target device not connected." })); - logSessionEvent(sessionId, toId, "mouse_click", "Failed to send mouse input: device not connected."); - } - - break; - } - - - case "keyboard": { - const { fromId, toId, sessionId, payload, type } = data; - - //const { sessionId, key, code, type } = data; - - if (!sessionId || !payload.key || !payload.code || !type) { - //ws.send(JSON.stringify({ type: "error", message: "Missing required fields for keyboard input." })); - console.warn(`COMM LAYER: Received invalid keyboard message (${type}). Missing fields. Data:`, data); - logSessionEvent(data.sessionId || 'unknown', fromId || 'unknown', type, `Invalid signaling message received from backend.`); - break; - } - - const allowedPeers = approvedSessions.get(sessionId); - if (!allowedPeers || !allowedPeers.has(sessionId)) { - ws.send(JSON.stringify({ type: "error", message: "Session not approved between devices." })); - logSessionEvent(sessionId, toId, "keyboard", "Unauthorized attempt to send keyboard input."); - break; - } - - const target = clients.get(toId); - if (target && target.readyState === WebSocket.OPEN) { - target.send(JSON.stringify({ - type: "keyboard", - toId, - fromId, - payload - })); - logSessionEvent(sessionId, toId, "keyboard", `Keyboard input (${type}) relayed.`); - } else { - //ws.send(JSON.stringify({ type: "error", message: "Target device not connected." })); - logSessionEvent(sessionId, toId, "keyboard", "Failed to send keyboard input: device not connected."); - } - - break; - } - default: - console.log(`COMM LAYER: Received unhandled message type from Web Admin WS: ${data.type}`); - logSessionEvent(data.sessionId || 'unknown', data.deviceId || 'unknown', data.type, "Unhandled message type from Web Admin WS."); - break; - } - - } catch (error) { - console.error('COMM LAYER: Error parsing message from Web Admin WS:', error); - console.error('COMM LAYER: Raw message was:', message.toString ? message.toString() : message); - } - - }); - - webAdminWs.on('close', () => { - // const reasonString = reason ? reason.toString() : 'N/A'; - //console.log(`!!! COMM LAYER: Web Admin WS Disconnected. Code: ${code}, Reason: ${reasonString}. Retrying in 5s...`); // Enhanced log - // Clear listeners to avoid duplicates on retry if needed, although creating a new object handles this. - setTimeout(connectToWebAdmin, 5000); - }); - - webAdminWs.on('error', (err) => { - console.error('Web Admin WS Error:', err.message); - }); -} +const connectToWebAdmin = require('./webAdminConnection'); async function startServer() { // Wait for DB connection before proceeding const db = await connectDB(); - const devicesCollection = db.collection('devices'); - - const server = http.createServer(app); - - //const server = https.createServer(app); - - /*const server = https.createServer({ - key: fs.readFileSync("./certs/private.key"), - cert: fs.readFileSync("./certs/certificate.crt") - }, app);*/ - + let devicesCollection = db.collection('devices'); + initializeWebSocketHandlers(devicesCollection); + const webAdminWs = connectToWebAdmin(activeSessions, approvedSessions, clients); + const server = http.createServer(app); const wss = new WebSocket.Server({ server }); wss.on("connection", (ws) => { @@ -297,316 +56,40 @@ async function startServer() { console.log('\nReceived from device:', data); switch (data.type) { - //prilikom registracije, generise se token koji se vraca uređaju za dalje interakcije - case "register": // Device registration - const { deviceId, registrationKey } = data; - - // Add device to the clients map - clients.set(deviceId, ws); - - // Validate request payload - if (!deviceId || !registrationKey) { - ws.send(JSON.stringify({ type: "error", message: "Missing required fields: deviceId and/or registrationKey" })); - // return; - } - - // Find device using this registrationKey - const existingDevice = await devicesCollection.findOne({ registrationKey }); - if (!existingDevice) { - // If that device doesn't exist in DB, given registrationKey is invalid - ws.send(JSON.stringify({ type: "error", message: `Device with registration key ${registrationKey} doesn't exist.` })); - // return; - } - - // If the registrationKey is used by another device, prevent hijack (switching devices) - if (existingDevice.deviceId && existingDevice.deviceId !== deviceId) { - ws.send(JSON.stringify({ type: "error", message: `Registration key ${registrationKey} is already assigned to another device.` })); - // return; - } - - // Create device data with mandatory fields - const deviceData = { - deviceId, - status: "active", - lastActiveTime: new Date(), - }; - - // Add optional fields dynamically - ["model", "osVersion", "networkType", "ipAddress", "deregistrationKey"].forEach(field => { - if (data[field]) deviceData[field] = data[field]; - }); - - lastHeartbeat.set(deviceId, new Date()); - - console.log(`Device ${deviceId} connected.`); - - // Update the device status to "active" in the database - await devicesCollection.findOneAndUpdate( - { registrationKey }, - { - $set: deviceData - }, - { - returnDocument: 'after', - } - ); - - const token = jwt.sign({ deviceId }, process.env.JWT_SECRET); - - console.log(`[REGISTRATION] Generated JWT for device ${deviceId}: ${token}`); - - console.log(`Device ${deviceId} registered.`); - ws.send(JSON.stringify({ type: "success", message: `Device registered successfully.`, token })); + case "register": + await handleDeviceRegistration(ws, data, clients, lastHeartbeat); break; - //kod ostaje isti - case "deregister": // Device deregistration - // Validate request payload - if (!data.deviceId || !data.deregistrationKey) { - ws.send(JSON.stringify({ type: "error", message: "Missing required fields: deviceId, deregistrationKey" })); - return; - } - - // Check if the device exists - const device = await devicesCollection.findOne({ deviceId: data.deviceId }); - if (!device) { - ws.send(JSON.stringify({ type: "error", message: `Device not found.` })); - return; - } - - // Validate the deregistration key - if (device.deregistrationKey !== data.deregistrationKey) { - ws.send(JSON.stringify({ type: "error", message: "Invalid deregistration key." })); - return; - } - - // Remove the device from the database - await devicesCollection.deleteOne({ deviceId: data.deviceId }); - - // Disconnect the device by closing the WebSocket connection - clients.delete(data.deviceId); // Remove from connected clients - ws.close(); // Close the WebSocket connection for this device - - console.log(`Device ${data.deviceId} deregistered and removed from database.`); - - // Send success message - ws.send(JSON.stringify({ type: "success", message: `Device deregistered successfully and removed from database.` })); + case "deregister": + await handleDeviceDeregistration(ws, data, clients); break; - //kod ostaje isti case "status": - console.log(`Device ${data.deviceId} is ${data.status}`); - lastHeartbeat.set(data.deviceId, new Date()); - - // Ensure device is in clients map - if (!clients.has(data.deviceId)) { - clients.set(data.deviceId, ws); - } - - await devicesCollection.findOneAndUpdate( - { deviceId: data.deviceId }, - { - $set: { - status: data.status, - lastActiveTime: new Date() - } - }, - { returnDocument: 'after' } - ); + await handleDeviceStatus(ws, data, clients, lastHeartbeat); break; - //provjerava se sesija prije slanja signala case "signal": - const { from: senderId, to: receiverId, payload } = data; - - const allowedPeers = approvedSessions.get(senderId); - if (!allowedPeers || !allowedPeers.has(receiverId)) { - ws.send(JSON.stringify({ type: "error", message: "Session not approved between devices." })); - return; - } - - const target = clients.get(receiverId); - if (target) { - target.send(JSON.stringify({ type: "signal", from: senderId, payload })); - } + await handleDeviceSignal(ws, data, clients, approvedSessions); break; - //zasad nista case "disconnect": - await devicesCollection.findOneAndUpdate( - { deviceId: data.deviceId }, - { - $set: { - status: "inactive", - lastActiveTime: new Date() - } - }, - { returnDocument: 'after' } - ); - clients.delete(data.deviceId); - console.log(`Device ${data.deviceId} disconnected.`); + await handleDeviceDisconnect(ws, data, clients); break; - //device salje request prema comm layeru koji to salje webu case "session_request": - const { from, token: tokenn } = data; - - clients.set(from, ws); - - console.log(`Session request from device ${from} with token ${tokenn}`); - - const reqDevice = await devicesCollection.findOne({ deviceId: from }); - if (!reqDevice) { - ws.send(JSON.stringify({ type: "error", message: "Device is not registered." })); - return; - } - - const sessionUser = verifySessionToken(tokenn); - if (!sessionUser || sessionUser.deviceId !== from) { - ws.send(JSON.stringify({ type: "error", message: "Invalid session token." })); - return; - } - - activeSessions.set(tokenn, from); - - console.log(`\n\nSession request from device ${from} with token ${tokenn}'\n\n`); - - logSessionEvent(tokenn, from, data.type, "Session request from android device."); - - if (webAdminWs && webAdminWs.readyState === WebSocket.OPEN) { - console.log("Ja posaljem webu request od androida"); - - webAdminWs.send(JSON.stringify({ - type: "request_control", - sessionId: tokenn, - deviceId: from, - ovosesalje: "glupost" - })); - - // Notify the device we forwarded the request - ws.send(JSON.stringify({ type: "info", message: "Session request forwarded to Web Admin.", sessionId: tokenn })); - } else { - ws.send(JSON.stringify({ type: "error", message: "Web Admin not connected." })); - logSessionEvent(tokenn, from, data.type, "Web Admin not connected."); - } - - console.log("ActiveSessions: \n\n", activeSessions); - console.log("ApprovedSessions: \n\n", approvedSessions); - + await handleSessionRequest(ws, data, clients, activeSessions, webAdminWs); break; - //Device finalno salje potvrdu da prihvata sesiju i comm layer opet obavjestava web i tad pocinje case "session_final_confirmation": - const { token: finalToken, from: finalFrom, decision } = data; - - console.log(`Session final confirmation from device ${finalFrom} with token ${finalToken} and decision ${decision}`); - - const reqDeviceFinal = await devicesCollection.findOne({ deviceId: finalFrom }); - if (!reqDeviceFinal) { - ws.send(JSON.stringify({ type: "error", message: "Device is not registered." })); - logSessionEvent(finalToken, finalFrom, data.type, "Device is not registered."); - return; - } - - const sessionUserFinal = verifySessionToken(finalToken); - if (!sessionUserFinal || sessionUserFinal.deviceId !== finalFrom) { - ws.send(JSON.stringify({ type: "error", message: "Invalid session token." })); - logSessionEvent(finalToken, finalFrom, data.type, "Invalid session token."); - return; - } - - if (decision === "accepted") { - webAdminWs.send(JSON.stringify({ type: "control_status", from: finalFrom, sessionId: finalToken, status: "connected" })); - logSessionEvent(finalToken, finalFrom, data.type, "Session accepted by android device"); - } - else if (decision === "rejected") { - webAdminWs.send(JSON.stringify({ type: "control_status", from: finalFrom, sessionId: finalToken, status: "failed" })); - logSessionEvent(finalToken, finalFrom, data.type, "Session rejected by android device"); - break; - } - - ws.send(JSON.stringify({ type: "session_confirmed", message: "Session successfully started between device and Web Admin." })); - logSessionEvent(finalToken, finalFrom, "session_start", "Session successfully started between device and Web Admin."); - - break; - - case "offer": - case "answer": - case "ice-candidate": { - const { fromId, toId, payload, type } = data; - - // Ako je cilj Web Admin, šaljemo Web Adminu - if (webAdminWs && webAdminWs.readyState === WebSocket.OPEN) { - webAdminWs.send(JSON.stringify({ type, fromId, toId, payload })); - } - - // Ako je cilj drugi uređaj, šaljemo preko clients WS - const target = clients.get(toId); - if (target && target.readyState === WebSocket.OPEN) { - target.send(JSON.stringify({ type, fromId, toId, payload })); - } else { - console.warn(`Target ${toId} not connected as device (maybe it's the frontend).`); - } + await handleSessionFinalConfirmation(ws, data, webAdminWs); break; - } - - - - case "offer": case "answer": - case "ice-candidate": { - const { fromId, toId, payload, type } = data; - - const isFromAndroid = clients.get(fromId); - - if (isFromAndroid && webAdminWs && webAdminWs.readyState === WebSocket.OPEN) { - webAdminWs.send(JSON.stringify({ type, fromId, toId, payload })); - break; - } - - const target = clients.get(toId); - - if (target && target.readyState === WebSocket.OPEN) { - target.send(JSON.stringify({ type, fromId, toId, payload })); - } else { - console.warn(`Target ${toId} not connected as device (maybe it's the frontend).`); - } + case "ice-candidate": + handleWebRTCSignaling(ws, data, webAdminWs, clients); break; - } - /*case "remote_command": { - const { fromId, toId, command } = data; - - const allowedPeers = approvedSessions.get(fromId); - if (!allowedPeers || !allowedPeers.has(toId)) { - ws.send(JSON.stringify({ type: "error", message: "Session not approved between devices." })); - logSessionEvent("unknown", fromId, "remote_command", "Unauthorized attempt to send remote command."); - return; - } - - const target = clients.get(toId); - if (target && target.readyState === WebSocket.OPEN) { - target.send(JSON.stringify({ - type: "remote_command", - fromId, - command - })); - logSessionEvent("unknown", toId, "remote_command", `Remote command relayed from ${fromId}.`); - } else { - ws.send(JSON.stringify({ type: "error", message: "Target Android device not connected." })); - logSessionEvent("unknown", toId, "remote_command", "Failed to send remote command: device not connected."); - } - - break; - }*/ } - }); - // Prethodni kod nije radio jer se koristio deviceId koji nije definisan u ovom scope-u ws.on("close", () => { console.log("Client disconnected"); console.log("Klijenti: ", clients); }); - /*server.listen(443, () => { - console.log("HTTPS server running on port 443"); - });*/ - }); // Periodically check for devices that are inactive for too long @@ -651,98 +134,9 @@ async function startServer() { checkInactiveDevices(); }, HEARTBEAT_CHECK_INTERVAL); - // Status endpoint to check server status - app.get("/status", (req, res) => { - res.json({ status: "Remote Control Gateway is running", connectedClients: clients.size }); - }); - - app.get("/devices/active", async (req, res) => { - try { - - const activeDevices = await devicesCollection.find({ status: "active" }).toArray(); - - res.json(activeDevices); - } catch (error) { - console.error("Error fetching active devices:", error); - res.status(500).json({ error: "Failed to fetch active devices" }); - } - }); - - // Endpoint to get session logs for a specific device - app.get("/session/logs/:deviceId", async (req, res) => { - const { deviceId } = req.params; - - try { - const db = await connectDB(); - const sessionLogsCollection = db.collection('sessionLogs'); - - const logs = await sessionLogsCollection.find({ deviceId }).toArray(); - res.json(logs); - } catch (error) { - console.error("Error fetching session logs:", error); - res.status(500).json({ error: "Failed to fetch session logs" }); - } - }) - - // Deregister device - app.post("/devices/deregister", async (req, res) => { - const { deviceId, deregistrationKey } = req.body; - - // Validate request payload - if (!deviceId || !deregistrationKey) { - return res.status(400).json({ error: "Missing required fields: deviceId, deregistrationKey" }); - } - - try { - // Check if the device exists - const device = await devicesCollection.findOne({ deviceId }); - if (!device) { - return res.status(404).json({ error: `Device with ID ${deviceId} not found.` }); - } - - // Validate the deregistration key - if (device.deregistrationKey !== deregistrationKey) { - return res.status(400).json({ error: "Invalid deregistration key." }); - } - - // Remove the device from the database - await devicesCollection.deleteOne({ deviceId }); - - // Optionally remove the device from the WebSocket clients map and disconnect if needed - if (clients.has(deviceId)) { - const ws = clients.get(deviceId); - ws.close(); - clients.delete(deviceId); // Remove from connected clients - } - - console.log(`Device ${deviceId} deregistered and removed from database.`); - res.status(200).json({ message: `Device ${deviceId} deregistered successfully and removed from the database.` }); - - } catch (error) { - console.error("Error during device deregistration:", error); - res.status(500).json({ error: "Internal server error. Please try again later." }); - } - }); - - //temporary for removing test sessions - app.post('/removeSessions', (req, res) => { - const { token, deviceId } = req.body; - - if (!token) { - return res.status(400).json({ success: false, message: 'Token is required.' }); - } - - const removedFromSessions = approvedSessions.delete(deviceId); - const removedFromActiveSessions = activeSessions.delete(token); - - if (removedFromSessions || removedFromActiveSessions) { - return res.status(200).json({ success: true, message: 'Session removed.' }); - } else { - return res.status(404).json({ success: false, message: 'Session not found.' }); - } - }); + app.use('/', setupRoutes(clients, approvedSessions, activeSessions)); - const PORT = process.env.PORT || 8080; + const PORT = process.env.PORT || 8081; server.listen(PORT, () => { console.log("Server listening on port", PORT); }); diff --git a/webAdminConnection.js b/webAdminConnection.js new file mode 100644 index 0000000..fabcbef --- /dev/null +++ b/webAdminConnection.js @@ -0,0 +1,68 @@ +const WebSocket = require('ws'); +const config = require('./config'); +const { + handleRequestReceived, + handleControlDecision, + handleSessionTerminated, + handleSignalingMessage, + handleMouseClick, + handleKeyboard +} = require('./webAdminMessageHandlers'); + +function connectToWebAdmin(activeSessions, approvedSessions, clients) { + let webAdminWs = new WebSocket('wss://backend-wf7e.onrender.com/ws/control/comm'); + + webAdminWs.on('open', () => { + console.log('>>> COMM LAYER: Successfully connected to Web Admin WS (Backend)!'); + }); + + webAdminWs.on('message', (message) => { + try { + const data = JSON.parse(message); + console.log('\nCOMM LAYER: Received message from Web Admin WS:', data); + + switch (data.type) { + case "request_received": + handleRequestReceived(data, activeSessions); + break; + case "control_decision": + handleControlDecision(data, activeSessions, approvedSessions, clients); + break; + case "session_terminated": + handleSessionTerminated(data, activeSessions, approvedSessions, clients); + break; + case "offer": + case "answer": + case "ice-candidate": + handleSignalingMessage(data, clients); + break; + case "mouse_click": + handleMouseClick(data, clients); + break; + case "keyboard": + handleKeyboard(data, clients); + break; + default: + console.log(`COMM LAYER: Received unhandled message type from Web Admin WS: ${data.type}`); + logSessionEvent(data.sessionId || 'unknown', data.deviceId || 'unknown', data.type, "Unhandled message type from Web Admin WS."); + break; + } + + } catch (error) { + console.error('COMM LAYER: Error parsing message from Web Admin WS:', error); + console.error('COMM LAYER: Raw message was:', message.toString()); + } + }); + + webAdminWs.on('close', () => { + setTimeout(() => connectToWebAdmin(activeSessions, approvedSessions, clients), 5000); + }); + + webAdminWs.on('error', (err) => { + console.error('Web Admin WS Error:', err.message); + }); + + return webAdminWs; +} + +module.exports = connectToWebAdmin; diff --git a/webAdminMessageHandlers.js b/webAdminMessageHandlers.js new file mode 100644 index 0000000..36131a4 --- /dev/null +++ b/webAdminMessageHandlers.js @@ -0,0 +1,150 @@ +const { logSessionEvent } = require("./utils/sessionLogger"); + +function sendToDevice(deviceId, payload, clients) { + console.log("Broj klijenata:", clients.size, "->", Array.from(clients.keys())); + const ws = clients.get(deviceId); + if (ws && ws.readyState === 1) { + ws.send(JSON.stringify(payload)); + } else { + console.warn(`Device ${deviceId} not connected.`); + } +} + +function handleRequestReceived(data, activeSessions) { + console.log(`COMM LAYER: Backend acknowledged request for session ${data.sessionId}`); + logSessionEvent(data.sessionId, activeSessions.get(data.sessionId), data.type, "Backend acknowledged request for session."); +} + +function handleControlDecision(data, activeSessions, approvedSessions, clients) { + const { sessionId: decisionSessionId, decision, reason } = data; + const deviceId = activeSessions.get(decisionSessionId); + + if (!deviceId) { + console.error(`COMM LAYER: Received control_decision for session ${decisionSessionId}, but couldn't find deviceId.`); + logSessionEvent(decisionSessionId, 'unknown', data.type, `Failed: Could not find active device for session.`); + return; + } + + console.log(`COMM LAYER: Found deviceId ${deviceId} for session ${decisionSessionId}. Decision: ${decision}`); + + if (decision === "accepted") { + sendToDevice(deviceId, { + type: "approved", + sessionId: decisionSessionId, + message: "Admin approved the session request." + }, clients); + + logSessionEvent(decisionSessionId, deviceId, data.type, "Session approved by backend."); + if (!approvedSessions.has(deviceId)) { + approvedSessions.set(deviceId, new Set()); + } + approvedSessions.get(deviceId).add("web-admin"); + } else { + sendToDevice(deviceId, { + type: "rejected", + sessionId: decisionSessionId, + message: `Admin rejected the session request. Reason: ${reason || 'N/A'}` + }, clients); + + logSessionEvent(decisionSessionId, deviceId, data.type, `Session rejected by backend. Reason: ${reason || 'N/A'}`); + activeSessions.delete(decisionSessionId); + + const deviceApprovedPeers = approvedSessions.get(deviceId); + if (deviceApprovedPeers) { + deviceApprovedPeers.delete("web-admin"); + if (deviceApprovedPeers.size === 0) { + approvedSessions.delete(deviceId); + } + } + } +} + +function handleSessionTerminated(data, activeSessions, approvedSessions, clients) { + const { sessionId, reason } = data; + const deviceId = activeSessions.get(sessionId); + + if (!deviceId) { + console.warn(`COMM LAYER: Received termination for session ${sessionId}, but couldn't find deviceId.`); + logSessionEvent(sessionId, 'unknown', data.type, `Termination received. Reason: ${reason || 'N/A'}`); + activeSessions.delete(sessionId); + return; + } + + logSessionEvent(sessionId, deviceId, data.type, `Session terminated by backend. Reason: ${reason || 'N/A'}`); + activeSessions.delete(sessionId); + + const peers = approvedSessions.get(deviceId); + if (peers) { + peers.delete("web-admin"); + if (peers.size === 0) approvedSessions.delete(deviceId); + } + + sendToDevice(deviceId, { + type: "session_ended", + sessionId, + reason: reason || 'terminated_by_admin' + }, clients); +} + +function handleSignalingMessage(data, clients) { + const { fromId, toId, payload, type, sessionId } = data; + + if (!fromId || !toId || !payload) { + console.warn(`COMM LAYER: Invalid signaling message (${type}).`, data); + logSessionEvent(sessionId || 'unknown', fromId || 'unknown', type, "Invalid signaling message."); + return; + } + + const target = clients.get(toId); + if (target && target.readyState === 1) { + target.send(JSON.stringify({ type, fromId, toId, payload })); + logSessionEvent(sessionId || 'unknown', toId, type, `Relayed from backend peer ${fromId}.`); + } else { + logSessionEvent(sessionId || 'unknown', toId, type, `Failed relay: device not connected.`); + } +} + +function handleMouseClick(data, clients) { + const { fromId, toId, sessionId, payload } = data; + + if (!fromId || !toId || !payload) { + console.warn(`COMM LAYER: Invalid mouse_click message.`, data); + logSessionEvent(sessionId || 'unknown', fromId || 'unknown', "mouse_click", "Invalid message structure."); + return; + } + + const target = clients.get(toId); + if (target && target.readyState === 1) { + target.send(JSON.stringify({ type: "click", toId, fromId, payload })); + logSessionEvent(sessionId, toId, "mouse_click", `Mouse click at (${payload.x}, ${payload.y}) sent.`); + } else { + logSessionEvent(sessionId, toId, "mouse_click", "Device not connected."); + } +} + +function handleKeyboard(data, clients) { + const { fromId, toId, sessionId, payload, type } = data; + + if (!sessionId || !payload.key || !payload.code) { + console.warn(`COMM LAYER: Invalid keyboard message.`, data); + logSessionEvent(sessionId || 'unknown', fromId || 'unknown', type, "Invalid keyboard message structure."); + return; + } + + const target = clients.get(toId); + if (target && target.readyState === 1) { + target.send(JSON.stringify({ type: "keyboard", toId, fromId, payload })); + logSessionEvent(sessionId, toId, "keyboard", `Keyboard input (${type}) relayed.`); + } else { + logSessionEvent(sessionId, toId, "keyboard", "Device not connected."); + } +} + +module.exports = { + handleRequestReceived, + handleControlDecision, + handleSessionTerminated, + handleSignalingMessage, + handleMouseClick, + handleKeyboard, +}; diff --git a/websocketHandlers.js b/websocketHandlers.js new file mode 100644 index 0000000..c457077 --- /dev/null +++ b/websocketHandlers.js @@ -0,0 +1,271 @@ +// websocketHandlers.js +const jwt = require('jsonwebtoken'); +const { verifySessionToken } = require("./utils/authSession"); +const config = require('./config'); + +let devicesCollection; + +function initialize(dbCollection) { + devicesCollection = dbCollection; +} + +function sendToDevice(deviceId, payload, clients) { + const ws = clients.get(deviceId); + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify(payload)); + } else { + console.warn(`Device ${deviceId} not connected.`); + } +} + +async function handleDeviceRegistration(ws, data, clients, lastHeartbeat) { + const { deviceId, registrationKey } = data; + clients.set(deviceId, ws); + + if (!deviceId || !registrationKey) { + return ws.send(JSON.stringify({ type: "error", message: "Missing required fields: deviceId and/or registrationKey" })); + } + + const existingDevice = await devicesCollection.findOne({ registrationKey }); + if (!existingDevice) { + return ws.send(JSON.stringify({ type: "error", message: `Device with registration key ${registrationKey} doesn't exist.` })); + } + + if (existingDevice.deviceId && existingDevice.deviceId !== deviceId) { + return ws.send(JSON.stringify({ type: "error", message: `Registration key ${registrationKey} is already assigned to another device.` })); + } + + const deviceData = { + deviceId, + status: "active", + lastActiveTime: new Date(), + }; + + ["model", "osVersion", "networkType", "ipAddress", "deregistrationKey"].forEach(field => { + if (data[field]) deviceData[field] = data[field]; + }); + + lastHeartbeat.set(deviceId, new Date()); + + await devicesCollection.findOneAndUpdate( + { registrationKey }, + { $set: deviceData }, + { returnDocument: 'after' } + ); + + const token = jwt.sign({ deviceId }, config.JWT_SECRET); + console.log(`[REGISTRATION] Generated JWT for device ${deviceId}: ${token}`); + ws.send(JSON.stringify({ type: "success", message: `Device registered successfully.`, token })); +} + +async function handleDeviceDeregistration(ws, data, clients) { + const { deviceId, deregistrationKey } = data; + + if (!deviceId || !deregistrationKey) { + return ws.send(JSON.stringify({ type: "error", message: "Missing required fields: deviceId, deregistrationKey" })); + } + + const device = await devicesCollection.findOne({ deviceId }); + if (!device) { + return ws.send(JSON.stringify({ type: "error", message: `Device not found.` })); + } + + if (device.deregistrationKey !== deregistrationKey) { + return ws.send(JSON.stringify({ type: "error", message: "Invalid deregistration key." })); + } + + await devicesCollection.deleteOne({ deviceId }); + clients.delete(deviceId); + ws.close(); + + console.log(`Device ${deviceId} deregistered.`); + ws.send(JSON.stringify({ type: "success", message: "Device deregistered successfully and removed from database." })); +} + +async function handleDeviceStatus(ws, data, clients, lastHeartbeat) { + lastHeartbeat.set(data.deviceId, new Date()); + + if (!clients.has(data.deviceId)) { + clients.set(data.deviceId, ws); + } + + await devicesCollection.findOneAndUpdate( + { deviceId: data.deviceId }, + { + $set: { + status: data.status, + lastActiveTime: new Date() + } + }, + { returnDocument: 'after' } + ); +} + +async function handleDeviceSignal(ws, data, clients, approvedSessions) { + const { from: senderId, to: receiverId, payload } = data; + const allowedPeers = approvedSessions.get(senderId); + + if (!allowedPeers || !allowedPeers.has(receiverId)) { + return ws.send(JSON.stringify({ type: "error", message: "Session not approved between devices." })); + } + + const target = clients.get(receiverId); + if (target) { + target.send(JSON.stringify({ type: "signal", from: senderId, payload })); + } +} + +async function handleDeviceDisconnect(ws, data, clients) { + await devicesCollection.findOneAndUpdate( + { deviceId: data.deviceId }, + { + $set: { + status: "inactive", + lastActiveTime: new Date() + } + }, + { returnDocument: 'after' } + ); + clients.delete(data.deviceId); + console.log(`Device ${data.deviceId} disconnected.`); +} + +async function handleSessionRequest(ws, data, clients, activeSessions, webAdminWs, logSessionEvent) { + const { from, token: tokenn } = data; + + clients.set(from, ws); + + console.log(`Session request from device ${from} with token ${tokenn}`); + + const reqDevice = await devicesCollection.findOne({ deviceId: from }); + if (!reqDevice) { + ws.send(JSON.stringify({ type: "error", message: "Device is not registered." })); + return; + } + + const sessionUser = verifySessionToken(tokenn); + if (!sessionUser || sessionUser.deviceId !== from) { + ws.send(JSON.stringify({ type: "error", message: "Invalid session token." })); + return; + } + + activeSessions.set(tokenn, from); + + console.log(`\n\nSession request from device ${from} with token ${tokenn}'\n\n`); + + logSessionEvent(tokenn, from, data.type, "Session request from android device."); + + if (webAdminWs && webAdminWs.readyState === WebSocket.OPEN) { + console.log("Ja posaljem webu request od androida"); + webAdminWs.send(JSON.stringify({ + type: "request_control", + sessionId: tokenn, + deviceId: from, + ovosesalje: "glupost" + })); + + // Notify the device we forwarded the request + ws.send(JSON.stringify({ type: "info", message: "Session request forwarded to Web Admin.", sessionId: tokenn })); + } else { + ws.send(JSON.stringify({ type: "error", message: "Web Admin not connected." })); + logSessionEvent(tokenn, from, data.type, "Web Admin not connected."); + } + + console.log("ActiveSessions: \n\n", activeSessions); + console.log("ApprovedSessions: \n\n", approvedSessions); +} + +async function handleSessionFinalConfirmation(ws, data, webAdminWs) { + const { token: finalToken, from: finalFrom, decision } = data; + + console.log(`Session final confirmation from device ${finalFrom} with token ${finalToken} and decision ${decision}`); + + const reqDeviceFinal = await devicesCollection.findOne({ deviceId: finalFrom }); + if (!reqDeviceFinal) { + ws.send(JSON.stringify({ type: "error", message: "Device is not registered." })); + logSessionEvent(finalToken, finalFrom, data.type, "Device is not registered."); + return; + } + + const sessionUserFinal = verifySessionToken(finalToken); + if (!sessionUserFinal || sessionUserFinal.deviceId !== finalFrom) { + ws.send(JSON.stringify({ type: "error", message: "Invalid session token." })); + logSessionEvent(finalToken, finalFrom, data.type, "Invalid session token."); + return; + } + + if (decision === "accepted") { + webAdminWs.send(JSON.stringify({ type: "control_status", from: finalFrom, sessionId: finalToken, status: "connected" })); + logSessionEvent(finalToken, finalFrom, data.type, "Session accepted by android device"); + } + else if (decision === "rejected") { + webAdminWs.send(JSON.stringify({ type: "control_status", from: finalFrom, sessionId: finalToken, status: "failed" })); + logSessionEvent(finalToken, finalFrom, data.type, "Session rejected by android device"); + } + + ws.send(JSON.stringify({ type: "session_confirmed", message: "Session successfully started between device and Web Admin." })); + logSessionEvent(finalToken, finalFrom, "session_start", "Session successfully started between device and Web Admin."); + +} + +function handleControlDecision(data, activeSessions, approvedSessions, clients) { + const { sessionId, decision, reason } = data; + const deviceId = activeSessions.get(sessionId); + + if (!deviceId) { + return console.error(`No active session found for session ID ${sessionId}.`); + } + + if (decision === "accepted") { + sendToDevice(deviceId, { + type: "approved", + sessionId, + message: "Admin approved the session request." + }, clients); + + if (!approvedSessions.has(deviceId)) { + approvedSessions.set(deviceId, new Set()); + } + approvedSessions.get(deviceId).add("web-admin"); + } else { + sendToDevice(deviceId, { + type: "rejected", + sessionId, + message: `Admin rejected the session request. Reason: ${reason || 'N/A'}` + }, clients); + activeSessions.delete(sessionId); + } +} + + +function handleWebRTCSignaling(ws, data, webAdminWs, clients) { + const { fromId, toId, payload, type } = data; + + // Ako je cilj Web Admin, šaljemo Web Adminu + if (toId === "web-admin" && webAdminWs && webAdminWs.readyState === WebSocket.OPEN) { + webAdminWs.send(JSON.stringify({ type, fromId, toId, payload })); + } + + // Ako je cilj drugi uređaj, šaljemo preko clients WS + const target = clients.get(toId); + if (target && target.readyState === WebSocket.OPEN) { + target.send(JSON.stringify({ type, fromId, toId, payload })); + } else { + console.warn(`Target device ${toId} not connected or unavailable.`); + } +} + + +module.exports = { + initialize, + sendToDevice, + handleDeviceRegistration, + handleDeviceDeregistration, + handleDeviceStatus, + handleDeviceSignal, + handleDeviceDisconnect, + handleSessionRequest, + handleSessionFinalConfirmation, + handleControlDecision, + handleWebRTCSignaling +};