Sin descripción
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

agent.js 4.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. // Agent renderer: connects to the signaling server, handles consent,
  2. // captures the screen, and streams it over WebRTC to the technician's viewer.
  3. // Remote input events arrive on the data channel and are handed to the main
  4. // process (via the preload bridge) for OS-level injection.
  5. const statusEl = document.getElementById('status');
  6. const consentBox = document.getElementById('consentBox');
  7. const indicator = document.getElementById('indicator');
  8. const logEl = document.getElementById('log');
  9. let cfg = null;
  10. let ws = null;
  11. let pc = null;
  12. let localStream = null;
  13. let currentSessionId = null;
  14. const log = (t) => { const d = document.createElement('div'); d.textContent = t; logEl.appendChild(d); logEl.scrollTop = logEl.scrollHeight; };
  15. const setStatus = (t, cls = '') => { statusEl.textContent = t; statusEl.className = 'status ' + cls; };
  16. window.agent.onConfig((c) => {
  17. cfg = c;
  18. if (!cfg.enrollToken) {
  19. setStatus('No enroll token. Set AGENT_ENROLL_TOKEN and restart.', 'warn');
  20. return;
  21. }
  22. connect();
  23. });
  24. function wsUrl() {
  25. const u = new URL(cfg.serverUrl);
  26. const proto = u.protocol === 'https:' ? 'wss:' : 'ws:';
  27. return `${proto}//${u.host}/ws`;
  28. }
  29. function connect() {
  30. setStatus('Connecting to server…');
  31. ws = new WebSocket(wsUrl());
  32. ws.onopen = () => {
  33. ws.send(JSON.stringify({ type: 'agent-hello', enrollToken: cfg.enrollToken }));
  34. };
  35. ws.onmessage = (e) => handle(JSON.parse(e.data));
  36. ws.onclose = () => { setStatus('Disconnected. Reconnecting in 3s…', 'warn'); setTimeout(connect, 3000); };
  37. }
  38. async function handle(m) {
  39. switch (m.type) {
  40. case 'agent-registered':
  41. setStatus(`Online as "${m.name}". Waiting for sessions.`, 'on');
  42. log(`registered: ${m.name}`);
  43. break;
  44. case 'session-request':
  45. if (m.unattended) { log(`unattended session ${m.sessionId} — auto-granting`); grant(m.sessionId); }
  46. else showConsent(m);
  47. break;
  48. case 'start-stream':
  49. currentSessionId = m.sessionId;
  50. await startStreaming();
  51. break;
  52. case 'answer':
  53. if (pc) await pc.setRemoteDescription(new RTCSessionDescription(m.sdp));
  54. break;
  55. case 'ice-candidate':
  56. if (m.candidate && pc) await pc.addIceCandidate(new RTCIceCandidate(m.candidate));
  57. break;
  58. case 'session-ended':
  59. teardown();
  60. break;
  61. case 'error':
  62. setStatus('Server: ' + m.message, 'warn');
  63. break;
  64. }
  65. }
  66. function showConsent(m) {
  67. consentBox.innerHTML = `
  68. <div class="consent">
  69. <h2>Allow remote support?</h2>
  70. <p class="muted"><b>${escapeHtml(m.technician)}</b> is requesting to view and control this PC.</p>
  71. <button class="grant" id="grantBtn">Allow</button>
  72. <button class="deny" id="denyBtn">Deny</button>
  73. </div>`;
  74. document.getElementById('grantBtn').onclick = () => { consentBox.innerHTML = ''; grant(m.sessionId); };
  75. document.getElementById('denyBtn').onclick = () => { consentBox.innerHTML = ''; deny(m.sessionId); };
  76. }
  77. const grant = (sessionId) => ws.send(JSON.stringify({ type: 'consent', sessionId, granted: true }));
  78. const deny = (sessionId) => ws.send(JSON.stringify({ type: 'consent', sessionId, granted: false }));
  79. async function startStreaming() {
  80. setStatus('Sharing screen with technician…', 'on');
  81. indicator.classList.add('show');
  82. try {
  83. localStream = await navigator.mediaDevices.getDisplayMedia({ video: { frameRate: { ideal: 30 } }, audio: false });
  84. } catch (err) {
  85. log('getDisplayMedia failed: ' + err.message);
  86. setStatus('Screen capture failed.', 'warn');
  87. return;
  88. }
  89. pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] });
  90. localStream.getTracks().forEach((t) => pc.addTrack(t, localStream));
  91. // Viewer creates the input data channel; we receive it here.
  92. pc.ondatachannel = (ev) => {
  93. const ch = ev.channel;
  94. ch.onmessage = (msg) => {
  95. let evt; try { evt = JSON.parse(msg.data); } catch { return; }
  96. window.agent.injectInput(evt); // -> main process -> OS injection
  97. };
  98. };
  99. pc.onicecandidate = (ev) => {
  100. if (ev.candidate) ws.send(JSON.stringify({ type: 'ice-candidate', sessionId: currentSessionId, candidate: ev.candidate }));
  101. };
  102. const offer = await pc.createOffer();
  103. await pc.setLocalDescription(offer);
  104. ws.send(JSON.stringify({ type: 'offer', sessionId: currentSessionId, sdp: pc.localDescription }));
  105. log('sent offer for session ' + currentSessionId);
  106. }
  107. function teardown() {
  108. indicator.classList.remove('show');
  109. window.agent.sessionEnded();
  110. if (localStream) { localStream.getTracks().forEach((t) => t.stop()); localStream = null; }
  111. if (pc) { pc.close(); pc = null; }
  112. currentSessionId = null;
  113. setStatus('Session ended. Waiting for sessions.', 'on');
  114. }
  115. function escapeHtml(s) { return String(s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])); }