Initial commit

This commit is contained in:
monoadmin
2026-04-10 15:36:33 -07:00
commit d6d7338a39
134 changed files with 16232 additions and 0 deletions

88
electron-agent/README.md Normal file
View File

@@ -0,0 +1,88 @@
# RemoteLink Agent
The RemoteLink Agent is an Electron-based desktop application that enables remote desktop control. It runs in the system tray and allows users to generate session codes for remote access.
## Features
- System tray integration with quick access menu
- Session code generation (6-digit codes, 10-minute expiry)
- Screen capture using Electron's desktopCapturer
- WebRTC peer-to-peer connections
- Multi-monitor support
- Cross-platform (Windows, macOS, Linux)
## Development Setup
```bash
# Navigate to the agent directory
cd electron-agent
# Install dependencies
npm install
# Run in development mode
npm run dev
```
## Building
```bash
# Build for all platforms
npm run build
npm run package
# Platform-specific builds
npm run package:win # Windows
npm run package:mac # macOS
npm run package:linux # Linux
```
## Architecture
```
electron-agent/
├── src/
│ ├── main/ # Main process (Node.js)
│ │ └── index.ts # Tray, IPC handlers, WebRTC
│ ├── preload/ # Preload scripts (bridge)
│ │ └── index.ts # Exposes safe APIs to renderer
│ └── renderer/ # Renderer process (UI)
│ └── index.html # Agent UI
├── assets/ # Icons and images
└── package.json
```
## Configuration
The agent needs to be configured with an access key from the RemoteLink web dashboard:
1. Register/login at the web dashboard
2. Navigate to Machines > Add Machine
3. Copy the generated access key
4. Paste it in the agent's setup screen
## API Endpoints
The agent communicates with these server endpoints:
- `POST /api/agent/register` - Register machine with server
- `POST /api/agent/heartbeat` - Maintain online status
- `POST /api/agent/session-code` - Generate session codes
- `POST /api/signal` - WebRTC signaling
## Security
- Access keys are unique per machine
- Session codes expire after 10 minutes
- All WebRTC connections are encrypted
- No screen data passes through servers (P2P)
## Input Simulation
For full remote control, the agent uses native libraries:
- **Windows**: `robotjs` or `nut.js`
- **macOS**: Requires Accessibility permissions
- **Linux**: X11 or Wayland protocols
Note: Input simulation is not implemented in this demo version.

View File

@@ -0,0 +1,45 @@
{
"name": "remotelink-agent",
"version": "1.0.0",
"description": "RemoteLink Agent - Remote Desktop Control Agent",
"main": "dist/main.js",
"scripts": {
"dev": "electron-vite dev",
"build": "electron-vite build",
"preview": "electron-vite preview",
"package": "electron-builder",
"package:win": "electron-builder --win",
"package:mac": "electron-builder --mac",
"package:linux": "electron-builder --linux"
},
"dependencies": {
"@electron/remote": "^2.1.2"
},
"devDependencies": {
"@types/node": "^20.11.0",
"electron": "^29.0.0",
"electron-builder": "^24.9.1",
"electron-vite": "^2.0.0",
"typescript": "^5.3.3",
"vite": "^5.0.12"
},
"build": {
"appId": "com.remotelink.agent",
"productName": "RemoteLink Agent",
"directories": {
"output": "release"
},
"win": {
"target": ["nsis"],
"icon": "assets/icon.ico"
},
"mac": {
"target": ["dmg"],
"icon": "assets/icon.icns"
},
"linux": {
"target": ["AppImage"],
"icon": "assets/icon.png"
}
}
}

View File

@@ -0,0 +1,253 @@
/**
* RemoteLink Agent - Main Process
*
* This is the main Electron process that:
* - Creates the system tray icon
* - Manages WebRTC connections
* - Captures screen using desktopCapturer
* - Handles input simulation
*/
import { app, BrowserWindow, Tray, Menu, desktopCapturer, ipcMain, screen } from 'electron'
import path from 'path'
let mainWindow: BrowserWindow | null = null
let tray: Tray | null = null
// Agent state
interface AgentState {
accessKey: string | null
isConnected: boolean
currentSessionCode: string | null
sessionCodeExpiry: Date | null
}
const agentState: AgentState = {
accessKey: null,
isConnected: false,
currentSessionCode: null,
sessionCodeExpiry: null,
}
const API_BASE_URL = process.env.API_BASE_URL || 'https://your-app.vercel.app'
function createWindow() {
mainWindow = new BrowserWindow({
width: 400,
height: 500,
resizable: false,
minimizable: true,
maximizable: false,
show: false,
frame: false,
skipTaskbar: true,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
contextIsolation: true,
nodeIntegration: false,
},
})
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'))
mainWindow.on('close', (event) => {
event.preventDefault()
mainWindow?.hide()
})
}
function createTray() {
const iconPath = path.join(__dirname, '../../assets/tray-icon.png')
tray = new Tray(iconPath)
const contextMenu = Menu.buildFromTemplate([
{
label: 'Open RemoteLink',
click: () => {
mainWindow?.show()
},
},
{
label: 'Generate Session Code',
click: async () => {
await generateSessionCode()
mainWindow?.show()
},
},
{ type: 'separator' },
{
label: 'Settings',
click: () => {
mainWindow?.show()
mainWindow?.webContents.send('navigate', 'settings')
},
},
{ type: 'separator' },
{
label: 'Quit',
click: () => {
app.quit()
},
},
])
tray.setToolTip('RemoteLink Agent')
tray.setContextMenu(contextMenu)
tray.on('click', () => {
mainWindow?.show()
})
}
// Generate session code via API
async function generateSessionCode(): Promise<string | null> {
if (!agentState.accessKey) {
console.error('No access key configured')
return null
}
try {
const response = await fetch(`${API_BASE_URL}/api/agent/session-code`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accessKey: agentState.accessKey }),
})
const data = await response.json()
if (data.success) {
agentState.currentSessionCode = data.code
agentState.sessionCodeExpiry = new Date(data.expiresAt)
// Notify renderer
mainWindow?.webContents.send('session-code-generated', {
code: data.code,
expiresAt: data.expiresAt,
expiresIn: data.expiresIn,
})
return data.code
}
} catch (error) {
console.error('Failed to generate session code:', error)
}
return null
}
// Send heartbeat to server
async function sendHeartbeat() {
if (!agentState.accessKey) return
try {
const response = await fetch(`${API_BASE_URL}/api/agent/heartbeat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accessKey: agentState.accessKey }),
})
const data = await response.json()
if (data.pendingConnection) {
// Handle incoming connection request
mainWindow?.webContents.send('incoming-connection', data.pendingConnection)
}
} catch (error) {
console.error('Heartbeat failed:', error)
}
}
// Get available screen sources
async function getScreenSources() {
const sources = await desktopCapturer.getSources({
types: ['screen', 'window'],
thumbnailSize: { width: 1920, height: 1080 },
})
return sources.map(source => ({
id: source.id,
name: source.name,
thumbnail: source.thumbnail.toDataURL(),
}))
}
// IPC Handlers
ipcMain.handle('get-screen-sources', getScreenSources)
ipcMain.handle('generate-session-code', generateSessionCode)
ipcMain.handle('get-agent-state', () => ({
isConnected: agentState.isConnected,
currentSessionCode: agentState.currentSessionCode,
sessionCodeExpiry: agentState.sessionCodeExpiry?.toISOString(),
}))
ipcMain.handle('set-access-key', (_event, accessKey: string) => {
agentState.accessKey = accessKey
// Register with server
registerAgent()
})
ipcMain.handle('get-monitors', () => {
return screen.getAllDisplays().map((display, index) => ({
id: display.id,
index,
label: `Display ${index + 1}`,
width: display.size.width,
height: display.size.height,
primary: display.id === screen.getPrimaryDisplay().id,
}))
})
// Register agent with server
async function registerAgent() {
if (!agentState.accessKey) return
try {
const response = await fetch(`${API_BASE_URL}/api/agent/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
accessKey: agentState.accessKey,
name: require('os').hostname(),
hostname: require('os').hostname(),
os: process.platform,
osVersion: require('os').release(),
agentVersion: app.getVersion(),
}),
})
const data = await response.json()
if (data.success) {
agentState.isConnected = true
mainWindow?.webContents.send('registered', data)
}
} catch (error) {
console.error('Registration failed:', error)
}
}
// App lifecycle
app.whenReady().then(() => {
createWindow()
createTray()
// Start heartbeat interval
setInterval(sendHeartbeat, 30000) // Every 30 seconds
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})
app.on('window-all-closed', () => {
// Don't quit on window close - keep running in tray
})
app.on('before-quit', () => {
// Clean up
tray?.destroy()
})

View File

@@ -0,0 +1,108 @@
/**
* RemoteLink Agent - Preload Script
* Exposes safe APIs to the renderer process
*/
import { contextBridge, ipcRenderer } from 'electron'
// Define types for exposed API
interface AgentState {
isConnected: boolean
currentSessionCode: string | null
sessionCodeExpiry: string | null
}
interface ScreenSource {
id: string
name: string
thumbnail: string
}
interface Monitor {
id: number
index: number
label: string
width: number
height: number
primary: boolean
}
interface SessionCodeEvent {
code: string
expiresAt: string
expiresIn: number
}
// Expose APIs to renderer
contextBridge.exposeInMainWorld('remotelink', {
// Get current agent state
getAgentState: (): Promise<AgentState> => {
return ipcRenderer.invoke('get-agent-state')
},
// Set access key for registration
setAccessKey: (accessKey: string): Promise<void> => {
return ipcRenderer.invoke('set-access-key', accessKey)
},
// Generate a new session code
generateSessionCode: (): Promise<string | null> => {
return ipcRenderer.invoke('generate-session-code')
},
// Get available screen sources for sharing
getScreenSources: (): Promise<ScreenSource[]> => {
return ipcRenderer.invoke('get-screen-sources')
},
// Get connected monitors
getMonitors: (): Promise<Monitor[]> => {
return ipcRenderer.invoke('get-monitors')
},
// Event listeners
onSessionCodeGenerated: (callback: (event: SessionCodeEvent) => void) => {
ipcRenderer.on('session-code-generated', (_event, data) => callback(data))
return () => {
ipcRenderer.removeAllListeners('session-code-generated')
}
},
onIncomingConnection: (callback: (data: { sessionCodeId: string; usedBy: string }) => void) => {
ipcRenderer.on('incoming-connection', (_event, data) => callback(data))
return () => {
ipcRenderer.removeAllListeners('incoming-connection')
}
},
onRegistered: (callback: (data: { success: boolean; machineId: string }) => void) => {
ipcRenderer.on('registered', (_event, data) => callback(data))
return () => {
ipcRenderer.removeAllListeners('registered')
}
},
onNavigate: (callback: (page: string) => void) => {
ipcRenderer.on('navigate', (_event, page) => callback(page))
return () => {
ipcRenderer.removeAllListeners('navigate')
}
},
})
// Type declaration for window object
declare global {
interface Window {
remotelink: {
getAgentState: () => Promise<AgentState>
setAccessKey: (accessKey: string) => Promise<void>
generateSessionCode: () => Promise<string | null>
getScreenSources: () => Promise<ScreenSource[]>
getMonitors: () => Promise<Monitor[]>
onSessionCodeGenerated: (callback: (event: SessionCodeEvent) => void) => () => void
onIncomingConnection: (callback: (data: { sessionCodeId: string; usedBy: string }) => void) => () => void
onRegistered: (callback: (data: { success: boolean; machineId: string }) => void) => () => void
onNavigate: (callback: (page: string) => void) => () => void
}
}
}

View File

@@ -0,0 +1,350 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RemoteLink Agent</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f17;
color: #e4e4e7;
min-height: 100vh;
-webkit-app-region: drag;
user-select: none;
}
.container {
padding: 24px;
display: flex;
flex-direction: column;
min-height: 100vh;
}
.header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 32px;
}
.logo {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.logo svg {
width: 24px;
height: 24px;
color: white;
}
.title {
font-size: 20px;
font-weight: 600;
}
.status-card {
background: #1a1a24;
border: 1px solid #27272a;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
}
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #22c55e;
}
.status-dot.offline {
background: #71717a;
}
.status-text {
font-size: 14px;
color: #a1a1aa;
}
.session-code {
text-align: center;
padding: 24px;
background: #1f1f2a;
border-radius: 8px;
}
.session-code-label {
font-size: 12px;
text-transform: uppercase;
color: #71717a;
margin-bottom: 8px;
}
.session-code-value {
font-size: 36px;
font-weight: 700;
letter-spacing: 8px;
font-family: 'JetBrains Mono', monospace;
color: #fff;
}
.session-code-expiry {
font-size: 12px;
color: #71717a;
margin-top: 8px;
}
.generate-btn {
-webkit-app-region: no-drag;
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
border: none;
border-radius: 8px;
color: white;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s;
}
.generate-btn:hover {
opacity: 0.9;
}
.generate-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.setup-card {
background: #1a1a24;
border: 1px solid #27272a;
border-radius: 12px;
padding: 20px;
}
.setup-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 12px;
}
.input-group {
margin-bottom: 16px;
}
.input-label {
font-size: 12px;
color: #71717a;
margin-bottom: 6px;
display: block;
}
.input {
-webkit-app-region: no-drag;
width: 100%;
padding: 10px 12px;
background: #0f0f17;
border: 1px solid #27272a;
border-radius: 6px;
color: #e4e4e7;
font-size: 14px;
font-family: 'JetBrains Mono', monospace;
}
.input:focus {
outline: none;
border-color: #6366f1;
}
.footer {
margin-top: auto;
text-align: center;
font-size: 11px;
color: #52525b;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
<line x1="8" y1="21" x2="16" y2="21"></line>
<line x1="12" y1="17" x2="12" y2="21"></line>
</svg>
</div>
<span class="title">RemoteLink Agent</span>
</div>
<div class="status-card">
<div class="status-indicator">
<div class="status-dot" id="statusDot"></div>
<span class="status-text" id="statusText">Connecting...</span>
</div>
<div class="session-code" id="sessionCodeContainer" style="display: none;">
<div class="session-code-label">Session Code</div>
<div class="session-code-value" id="sessionCodeValue">---</div>
<div class="session-code-expiry" id="sessionCodeExpiry">Valid for 10 minutes</div>
</div>
</div>
<button class="generate-btn" id="generateBtn">Generate Session Code</button>
<div class="setup-card" id="setupCard" style="margin-top: 20px; display: none;">
<div class="setup-title">Setup Required</div>
<div class="input-group">
<label class="input-label">Access Key</label>
<input type="text" class="input" id="accessKeyInput" placeholder="Paste your access key here">
</div>
<button class="generate-btn" id="saveKeyBtn">Save & Connect</button>
</div>
<div class="footer">
RemoteLink Agent v1.0.0<br>
Secure remote desktop solution
</div>
</div>
<script>
// UI Elements
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
const sessionCodeContainer = document.getElementById('sessionCodeContainer');
const sessionCodeValue = document.getElementById('sessionCodeValue');
const sessionCodeExpiry = document.getElementById('sessionCodeExpiry');
const generateBtn = document.getElementById('generateBtn');
const setupCard = document.getElementById('setupCard');
const accessKeyInput = document.getElementById('accessKeyInput');
const saveKeyBtn = document.getElementById('saveKeyBtn');
// State
let countdownInterval = null;
// Initialize
async function init() {
const state = await window.remotelink.getAgentState();
if (state.isConnected) {
setConnected();
if (state.currentSessionCode && state.sessionCodeExpiry) {
showSessionCode(state.currentSessionCode, state.sessionCodeExpiry);
}
} else {
setDisconnected();
setupCard.style.display = 'block';
}
}
function setConnected() {
statusDot.classList.remove('offline');
statusText.textContent = 'Connected to server';
setupCard.style.display = 'none';
}
function setDisconnected() {
statusDot.classList.add('offline');
statusText.textContent = 'Not configured';
}
function showSessionCode(code, expiresAt) {
sessionCodeContainer.style.display = 'block';
sessionCodeValue.textContent = code.slice(0, 3) + ' ' + code.slice(3);
// Start countdown
if (countdownInterval) clearInterval(countdownInterval);
function updateCountdown() {
const now = new Date();
const expiry = new Date(expiresAt);
const remaining = Math.max(0, Math.floor((expiry - now) / 1000));
if (remaining <= 0) {
sessionCodeExpiry.textContent = 'Code expired';
sessionCodeContainer.style.display = 'none';
clearInterval(countdownInterval);
return;
}
const minutes = Math.floor(remaining / 60);
const seconds = remaining % 60;
sessionCodeExpiry.textContent = `Expires in ${minutes}:${seconds.toString().padStart(2, '0')}`;
}
updateCountdown();
countdownInterval = setInterval(updateCountdown, 1000);
}
// Event handlers
generateBtn.addEventListener('click', async () => {
generateBtn.disabled = true;
generateBtn.textContent = 'Generating...';
const code = await window.remotelink.generateSessionCode();
generateBtn.disabled = false;
generateBtn.textContent = 'Generate Session Code';
if (!code) {
alert('Failed to generate code. Please check your connection.');
}
});
saveKeyBtn.addEventListener('click', async () => {
const accessKey = accessKeyInput.value.trim();
if (!accessKey) return;
saveKeyBtn.disabled = true;
saveKeyBtn.textContent = 'Connecting...';
await window.remotelink.setAccessKey(accessKey);
});
// IPC event listeners
window.remotelink.onSessionCodeGenerated((event) => {
showSessionCode(event.code, event.expiresAt);
});
window.remotelink.onRegistered((data) => {
if (data.success) {
setConnected();
}
saveKeyBtn.disabled = false;
saveKeyBtn.textContent = 'Save & Connect';
});
window.remotelink.onIncomingConnection((data) => {
// Show notification that someone is connecting
new Notification('RemoteLink', {
body: 'Incoming connection request',
});
});
// Initialize on load
init();
</script>
</body>
</html>