Skip to content

Commit a18467f

Browse files
feat: add WebRTC signaling server plugin
Lightweight signaling server for WebRTC peer-to-peer connections. Relays SDP offers/answers and ICE candidates between authenticated users. The actual media/data flows directly between peers. - New plugin: src/webrtc/index.js (~110 lines) - CLI: --webrtc, --webrtc-path - Env: JSS_WEBRTC, JSS_WEBRTC_PATH - WebSocket endpoint at /.webrtc (default) - WebID-based authentication required - Peer presence notifications (join/leave) Fixes JavaScriptSolidServer#207
1 parent 23d6d82 commit a18467f

4 files changed

Lines changed: 141 additions & 0 deletions

File tree

bin/jss.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ program
6565
.option('--no-nostr', 'Disable Nostr relay')
6666
.option('--nostr-path <path>', 'Nostr relay WebSocket path (default: /relay)')
6767
.option('--nostr-max-events <n>', 'Max events in relay memory (default: 1000)', parseInt)
68+
.option('--webrtc', 'Enable WebRTC signaling server')
69+
.option('--no-webrtc', 'Disable WebRTC signaling server')
70+
.option('--webrtc-path <path>', 'WebRTC signaling WebSocket path (default: /.webrtc)')
6871
.option('--activitypub', 'Enable ActivityPub federation')
6972
.option('--no-activitypub', 'Disable ActivityPub federation')
7073
.option('--ap-username <name>', 'ActivityPub username (default: me)')
@@ -142,6 +145,8 @@ program
142145
nostr: config.nostr,
143146
nostrPath: config.nostrPath,
144147
nostrMaxEvents: config.nostrMaxEvents,
148+
webrtc: config.webrtc,
149+
webrtcPath: config.webrtcPath,
145150
activitypub: config.activitypub,
146151
apUsername: config.apUsername,
147152
apDisplayName: config.apDisplayName,
@@ -186,6 +191,7 @@ program
186191
if (config.solidosUi) console.log(' SolidOS UI: enabled (modern interface)');
187192
if (config.git) console.log(' Git: enabled (clone/push support)');
188193
if (config.nostr) console.log(` Nostr: enabled (${config.nostrPath})`);
194+
if (config.webrtc) console.log(` WebRTC: enabled (${config.webrtcPath || '/.webrtc'})`);
189195
if (config.activitypub) console.log(` ActivityPub: enabled (@${config.apUsername || 'me'})`);
190196
if (config.singleUser) console.log(` Single-user: ${config.singleUserName || 'me'} (registration disabled)`);
191197
else if (config.inviteOnly) console.log(' Registration: invite-only');

src/config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ export const defaults = {
5454
nostrPath: '/relay',
5555
nostrMaxEvents: 1000,
5656

57+
// WebRTC signaling
58+
webrtc: false,
59+
webrtcPath: '/.webrtc',
60+
5761
// ActivityPub federation
5862
activitypub: false,
5963
apUsername: 'me',
@@ -134,6 +138,8 @@ const envMap = {
134138
JSS_NOSTR: 'nostr',
135139
JSS_NOSTR_PATH: 'nostrPath',
136140
JSS_NOSTR_MAX_EVENTS: 'nostrMaxEvents',
141+
JSS_WEBRTC: 'webrtc',
142+
JSS_WEBRTC_PATH: 'webrtcPath',
137143
JSS_ACTIVITYPUB: 'activitypub',
138144
JSS_AP_USERNAME: 'apUsername',
139145
JSS_AP_DISPLAY_NAME: 'apDisplayName',

src/server.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { createPayHandler, isPayRequest } from './handlers/pay.js';
1818
import { activityPubPlugin, getActorHandler } from './ap/index.js';
1919
import { remoteStoragePlugin } from './remotestorage.js';
2020
import { dbPlugin } from './db/index.js';
21+
import { webrtcPlugin } from './webrtc/index.js';
2122

2223
const __dirname = dirname(fileURLToPath(import.meta.url));
2324

@@ -74,6 +75,9 @@ export function createServer(options = {}) {
7475
const nostrEnabled = options.nostr ?? false;
7576
const nostrPath = options.nostrPath ?? '/relay';
7677
const nostrMaxEvents = options.nostrMaxEvents ?? 1000;
78+
// WebRTC signaling is OFF by default
79+
const webrtcEnabled = options.webrtc ?? false;
80+
const webrtcPath = options.webrtcPath ?? '/.webrtc';
7781
// ActivityPub federation is OFF by default
7882
const activitypubEnabled = options.activitypub ?? false;
7983
const apUsername = options.apUsername ?? 'me';
@@ -240,6 +244,11 @@ export function createServer(options = {}) {
240244
});
241245
}
242246

247+
// Register WebRTC signaling if enabled
248+
if (webrtcEnabled) {
249+
fastify.register(webrtcPlugin, { path: webrtcPath });
250+
}
251+
243252
// Register ActivityPub plugin if enabled
244253
if (activitypubEnabled) {
245254
fastify.register(activityPubPlugin, {

src/webrtc/index.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* WebRTC Signaling Server Plugin
3+
*
4+
* Lightweight signaling server for WebRTC peer-to-peer connections.
5+
* Relays SDP offers/answers and ICE candidates between authenticated users.
6+
* The actual media/data flows directly between peers — JSS just introduces them.
7+
*
8+
* Usage: jss start --webrtc
9+
* Endpoint: wss://your.pod/.webrtc
10+
*
11+
* Protocol (JSON over WebSocket):
12+
* → { type: "offer", to: "<webid>", sdp: "..." }
13+
* → { type: "answer", to: "<webid>", sdp: "..." }
14+
* → { type: "candidate", to: "<webid>", candidate: {...} }
15+
* → { type: "hangup", to: "<webid>" }
16+
* ← { type: "offer", from: "<webid>", sdp: "..." }
17+
* ← { type: "answer", from: "<webid>", sdp: "..." }
18+
* ← { type: "candidate", from: "<webid>", candidate: {...} }
19+
* ← { type: "hangup", from: "<webid>" }
20+
* ← { type: "error", message: "..." }
21+
* ← { type: "peers", peers: ["<webid>", ...] }
22+
*/
23+
24+
import websocket from '@fastify/websocket';
25+
import { getWebIdFromRequestAsync } from '../auth/token.js';
26+
27+
// Connected peers: webId → WebSocket
28+
const peers = new Map();
29+
30+
/**
31+
* Register WebRTC signaling routes on Fastify instance
32+
*
33+
* @param {object} fastify - Fastify instance
34+
* @param {object} options - Options
35+
* @param {string} options.path - WebSocket path (default: '/.webrtc')
36+
*/
37+
export async function webrtcPlugin(fastify, options = {}) {
38+
const path = options.path || '/.webrtc';
39+
40+
await fastify.register(websocket);
41+
42+
fastify.get(path, { websocket: true }, async (connection, request) => {
43+
const socket = connection.socket;
44+
45+
// Authenticate the connection
46+
const { webId } = await getWebIdFromRequestAsync(request);
47+
if (!webId) {
48+
socket.send(JSON.stringify({ type: 'error', message: 'Authentication required' }));
49+
socket.close();
50+
return;
51+
}
52+
53+
// Register this peer
54+
const existing = peers.get(webId);
55+
if (existing) {
56+
existing.close();
57+
}
58+
peers.set(webId, socket);
59+
socket.webId = webId;
60+
61+
// Notify the peer of their identity and online peers
62+
socket.send(JSON.stringify({
63+
type: 'peers',
64+
you: webId,
65+
peers: [...peers.keys()].filter(id => id !== webId)
66+
}));
67+
68+
// Notify other peers that someone came online
69+
broadcast(webId, { type: 'peer-joined', webId });
70+
71+
socket.on('message', (data) => {
72+
let msg;
73+
try {
74+
msg = JSON.parse(data.toString());
75+
} catch {
76+
socket.send(JSON.stringify({ type: 'error', message: 'Invalid JSON' }));
77+
return;
78+
}
79+
80+
if (!msg.to || !msg.type) {
81+
socket.send(JSON.stringify({ type: 'error', message: 'Missing "to" or "type" field' }));
82+
return;
83+
}
84+
85+
const target = peers.get(msg.to);
86+
if (!target || target.readyState !== 1) {
87+
socket.send(JSON.stringify({ type: 'error', message: 'Peer not online', peer: msg.to }));
88+
return;
89+
}
90+
91+
// Relay the message, replacing 'to' with 'from'
92+
const relay = { ...msg, from: webId };
93+
delete relay.to;
94+
target.send(JSON.stringify(relay));
95+
});
96+
97+
socket.on('close', () => {
98+
peers.delete(webId);
99+
broadcast(webId, { type: 'peer-left', webId });
100+
});
101+
102+
socket.on('error', () => {
103+
peers.delete(webId);
104+
});
105+
});
106+
}
107+
108+
/**
109+
* Send a message to all connected peers except the sender
110+
*/
111+
function broadcast(senderWebId, msg) {
112+
const data = JSON.stringify(msg);
113+
for (const [id, socket] of peers) {
114+
if (id !== senderWebId && socket.readyState === 1) {
115+
socket.send(data);
116+
}
117+
}
118+
}
119+
120+
export default webrtcPlugin;

0 commit comments

Comments
 (0)