minep2p 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.js +191 -0
- package/index.js +8 -0
- package/lib/client.js +196 -0
- package/lib/config.js +19 -0
- package/lib/ipv6.js +74 -0
- package/lib/peer.js +139 -0
- package/lib/signaling.js +133 -0
- package/minep2p-1.0.0.tgz +0 -0
- package/package.json +27 -0
package/cli.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const yargs = require('yargs');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const MineP2P = require('./lib/client');
|
|
5
|
+
|
|
6
|
+
const P2P_INSTANCE_FILE = '/tmp/minep2p_instance.json';
|
|
7
|
+
|
|
8
|
+
let client = null;
|
|
9
|
+
|
|
10
|
+
const printHeader = () => {
|
|
11
|
+
console.log(chalk.blue.bold('╔══════════════════════════════════════════╗'));
|
|
12
|
+
console.log(chalk.blue.bold('║ MineP2P - Vexify 2026 ║'));
|
|
13
|
+
console.log(chalk.blue.bold('╚══════════════════════════════════════════╝'));
|
|
14
|
+
console.log(chalk.gray('Copyright (c) Vexify 2026'));
|
|
15
|
+
console.log('');
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const handlePeerConnect = (peerId) => {
|
|
19
|
+
console.log(chalk.green(`✓ Peer connected: ${peerId.substring(0, 8)}...`));
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const handlePeerDisconnect = (peerId) => {
|
|
23
|
+
console.log(chalk.yellow(`✗ Peer disconnected: ${peerId.substring(0, 8)}...`));
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const handleMessage = (peerId, message) => {
|
|
27
|
+
console.log(chalk.cyan(`[${peerId.substring(0, 8)}]`) + ' ' + JSON.stringify(message));
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const handleError = (error) => {
|
|
31
|
+
console.log(chalk.red(`✗ Error: ${error.message}`));
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const startCommand = async (argv) => {
|
|
35
|
+
printHeader();
|
|
36
|
+
|
|
37
|
+
const room = argv.room || 'minep2p-default';
|
|
38
|
+
|
|
39
|
+
console.log(chalk.cyan(`Starting MineP2P client...`));
|
|
40
|
+
console.log(chalk.cyan(`Room: ${room}`));
|
|
41
|
+
|
|
42
|
+
client = new MineP2P();
|
|
43
|
+
|
|
44
|
+
client.on('connect', () => {
|
|
45
|
+
console.log(chalk.green('✓ Connected to signaling server'));
|
|
46
|
+
console.log(chalk.cyan(`Peer ID: ${client.getStatus().peerId.substring(0, 8)}...`));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
client.on('peerConnect', handlePeerConnect);
|
|
50
|
+
client.on('peerDisconnect', handlePeerDisconnect);
|
|
51
|
+
client.on('message', handleMessage);
|
|
52
|
+
client.on('error', handleError);
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
await client.start(room);
|
|
56
|
+
|
|
57
|
+
console.log(chalk.green('✓ MineP2P client started successfully!'));
|
|
58
|
+
console.log('');
|
|
59
|
+
console.log(chalk.yellow('Press Ctrl+C to stop'));
|
|
60
|
+
|
|
61
|
+
process.stdin.resume();
|
|
62
|
+
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.log(chalk.red(`✗ Failed to start: ${error.message}`));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const stopCommand = async () => {
|
|
70
|
+
printHeader();
|
|
71
|
+
|
|
72
|
+
console.log(chalk.cyan('Stopping MineP2P client...'));
|
|
73
|
+
|
|
74
|
+
if (client) {
|
|
75
|
+
try {
|
|
76
|
+
await client.stop();
|
|
77
|
+
console.log(chalk.green('✓ MineP2P client stopped successfully'));
|
|
78
|
+
process.exit(0);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.log(chalk.red(`✗ Failed to stop: ${error.message}`));
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
console.log(chalk.yellow('No running client found'));
|
|
85
|
+
process.exit(0);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const statusCommand = () => {
|
|
90
|
+
printHeader();
|
|
91
|
+
|
|
92
|
+
if (client) {
|
|
93
|
+
const status = client.getStatus();
|
|
94
|
+
console.log(chalk.cyan('Current Status:'));
|
|
95
|
+
console.log(` Status: ${chalk.green(status.status)}`);
|
|
96
|
+
console.log(` Peer ID: ${status.peerId}`);
|
|
97
|
+
console.log(` Room: ${status.room}`);
|
|
98
|
+
console.log(` Connected Peers: ${chalk.yellow(status.peerCount)}`);
|
|
99
|
+
|
|
100
|
+
if (status.peerCount > 0) {
|
|
101
|
+
console.log(' Peer List:');
|
|
102
|
+
client.getPeers().forEach(peerId => {
|
|
103
|
+
console.log(` - ${peerId.substring(0, 8)}...`);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
console.log(chalk.yellow('Client is not running'));
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const connectCommand = async (argv) => {
|
|
112
|
+
printHeader();
|
|
113
|
+
|
|
114
|
+
if (!client) {
|
|
115
|
+
console.log(chalk.yellow('Client is not running. Starting...'));
|
|
116
|
+
client = new MineP2P();
|
|
117
|
+
|
|
118
|
+
client.on('connect', () => {
|
|
119
|
+
console.log(chalk.green('✓ Connected'));
|
|
120
|
+
});
|
|
121
|
+
client.on('peerConnect', handlePeerConnect);
|
|
122
|
+
client.on('peerDisconnect', handlePeerDisconnect);
|
|
123
|
+
client.on('message', handleMessage);
|
|
124
|
+
client.on('error', handleError);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const room = argv.room || 'minep2p-default';
|
|
128
|
+
|
|
129
|
+
console.log(chalk.cyan(`Connecting to room: ${room}`));
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
await client.start(room);
|
|
133
|
+
console.log(chalk.green('✓ Connected to room successfully'));
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.log(chalk.red(`✗ Failed to connect: ${error.message}`));
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const sendCommand = async (argv) => {
|
|
141
|
+
printHeader();
|
|
142
|
+
|
|
143
|
+
if (!client) {
|
|
144
|
+
console.log(chalk.red('✗ Client is not running'));
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const message = argv.message || 'Hello from MineP2P';
|
|
149
|
+
|
|
150
|
+
console.log(chalk.cyan(`Broadcasting message: ${message}`));
|
|
151
|
+
client.broadcast({ type: 'chat', content: message });
|
|
152
|
+
console.log(chalk.green('✓ Message sent'));
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
yargs
|
|
156
|
+
.command('start', 'Start MineP2P client', (yargs) => {
|
|
157
|
+
return yargs.option('room', {
|
|
158
|
+
describe: 'Room name to join',
|
|
159
|
+
type: 'string',
|
|
160
|
+
default: 'minep2p-default'
|
|
161
|
+
});
|
|
162
|
+
}, startCommand)
|
|
163
|
+
.command('stop', 'Stop MineP2P client', stopCommand)
|
|
164
|
+
.command('status', 'Check client status', statusCommand)
|
|
165
|
+
.command('connect <room>', 'Connect to a specific room', (yargs) => {
|
|
166
|
+
return yargs.positional('room', {
|
|
167
|
+
describe: 'Room name',
|
|
168
|
+
type: 'string'
|
|
169
|
+
});
|
|
170
|
+
}, connectCommand)
|
|
171
|
+
.command('send <message>', 'Send a message to all peers', (yargs) => {
|
|
172
|
+
return yargs.positional('message', {
|
|
173
|
+
describe: 'Message to send',
|
|
174
|
+
type: 'string'
|
|
175
|
+
});
|
|
176
|
+
}, sendCommand)
|
|
177
|
+
.help()
|
|
178
|
+
.argv;
|
|
179
|
+
|
|
180
|
+
process.on('SIGINT', async () => {
|
|
181
|
+
console.log('');
|
|
182
|
+
console.log(chalk.cyan('Received interrupt signal, stopping...'));
|
|
183
|
+
if (client) {
|
|
184
|
+
try {
|
|
185
|
+
await client.stop();
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.log(chalk.red(`Error stopping: ${error.message}`));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
process.exit(0);
|
|
191
|
+
});
|
package/index.js
ADDED
package/lib/client.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
const { v4: uuidv4 } = require('uuid');
|
|
2
|
+
const Signaling = require('./signaling');
|
|
3
|
+
const Peer = require('./peer');
|
|
4
|
+
const IPv6 = require('./ipv6');
|
|
5
|
+
const config = require('./config');
|
|
6
|
+
|
|
7
|
+
class MineP2P {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.peerId = uuidv4();
|
|
10
|
+
this.signaling = null;
|
|
11
|
+
this.peers = new Map();
|
|
12
|
+
this.room = null;
|
|
13
|
+
this.status = 'disconnected';
|
|
14
|
+
this.callbacks = {
|
|
15
|
+
connect: [],
|
|
16
|
+
disconnect: [],
|
|
17
|
+
peerConnect: [],
|
|
18
|
+
peerDisconnect: [],
|
|
19
|
+
message: [],
|
|
20
|
+
statusChange: [],
|
|
21
|
+
error: []
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async start(room = config.defaultRoom) {
|
|
26
|
+
this.room = room;
|
|
27
|
+
this.signaling = new Signaling(this.peerId);
|
|
28
|
+
|
|
29
|
+
this.signaling.on('peerJoined', (peerId) => {
|
|
30
|
+
this._handlePeerJoined(peerId);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
this.signaling.on('peerLeft', (peerId) => {
|
|
34
|
+
this._handlePeerLeft(peerId);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
this.signaling.on('message', (message) => {
|
|
38
|
+
this._handleMessage(message);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await this.signaling.join(room);
|
|
43
|
+
this.status = 'connecting';
|
|
44
|
+
this._emitStatusChange();
|
|
45
|
+
this.signaling.startPolling();
|
|
46
|
+
|
|
47
|
+
const ipv6 = await IPv6.getIPv6WithRetry();
|
|
48
|
+
console.log(`IPv6 Address: ${ipv6 || 'Not available'}`);
|
|
49
|
+
|
|
50
|
+
this.status = 'connected';
|
|
51
|
+
this._emitStatusChange();
|
|
52
|
+
this.callbacks.connect.forEach(cb => cb());
|
|
53
|
+
|
|
54
|
+
} catch (error) {
|
|
55
|
+
this.status = 'error';
|
|
56
|
+
this._emitStatusChange();
|
|
57
|
+
this.callbacks.error.forEach(cb => cb(error));
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async stop() {
|
|
63
|
+
if (this.signaling) {
|
|
64
|
+
this.signaling.stopPolling();
|
|
65
|
+
await this.signaling.leave();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const peer of this.peers.values()) {
|
|
69
|
+
peer.close();
|
|
70
|
+
}
|
|
71
|
+
this.peers.clear();
|
|
72
|
+
|
|
73
|
+
this.status = 'disconnected';
|
|
74
|
+
this._emitStatusChange();
|
|
75
|
+
this.callbacks.disconnect.forEach(cb => cb());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async sendToPeer(peerId, message) {
|
|
79
|
+
const peer = this.peers.get(peerId);
|
|
80
|
+
if (peer) {
|
|
81
|
+
peer.send(message);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
broadcast(message) {
|
|
86
|
+
for (const peer of this.peers.values()) {
|
|
87
|
+
peer.send(message);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getPeers() {
|
|
92
|
+
return Array.from(this.peers.keys());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
getStatus() {
|
|
96
|
+
return {
|
|
97
|
+
status: this.status,
|
|
98
|
+
peerId: this.peerId,
|
|
99
|
+
room: this.room,
|
|
100
|
+
peerCount: this.peers.size
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
on(event, callback) {
|
|
105
|
+
if (this.callbacks[event]) {
|
|
106
|
+
this.callbacks[event].push(callback);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
off(event, callback) {
|
|
111
|
+
if (this.callbacks[event]) {
|
|
112
|
+
this.callbacks[event] = this.callbacks[event].filter(cb => cb !== callback);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async _handlePeerJoined(peerId) {
|
|
117
|
+
if (peerId === this.peerId) return;
|
|
118
|
+
|
|
119
|
+
const peer = new Peer(peerId, this.signaling);
|
|
120
|
+
this.peers.set(peerId, peer);
|
|
121
|
+
|
|
122
|
+
peer.on('connect', () => {
|
|
123
|
+
this.callbacks.peerConnect.forEach(cb => cb(peerId));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
peer.on('disconnect', () => {
|
|
127
|
+
this._handlePeerLeft(peerId);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
peer.on('message', (data) => {
|
|
131
|
+
this.callbacks.message.forEach(cb => cb(peerId, data));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
peer.on('error', (error) => {
|
|
135
|
+
this.callbacks.error.forEach(cb => cb(error));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await peer.connect(true);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
_handlePeerLeft(peerId) {
|
|
142
|
+
const peer = this.peers.get(peerId);
|
|
143
|
+
if (peer) {
|
|
144
|
+
peer.close();
|
|
145
|
+
this.peers.delete(peerId);
|
|
146
|
+
this.callbacks.peerDisconnect.forEach(cb => cb(peerId));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async _handleMessage(message) {
|
|
151
|
+
const { type, fromPeerId, sdp, candidate } = message;
|
|
152
|
+
|
|
153
|
+
if (!this.peers.has(fromPeerId)) {
|
|
154
|
+
const peer = new Peer(fromPeerId, this.signaling);
|
|
155
|
+
this.peers.set(fromPeerId, peer);
|
|
156
|
+
|
|
157
|
+
peer.on('connect', () => {
|
|
158
|
+
this.callbacks.peerConnect.forEach(cb => cb(fromPeerId));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
peer.on('disconnect', () => {
|
|
162
|
+
this._handlePeerLeft(fromPeerId);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
peer.on('message', (data) => {
|
|
166
|
+
this.callbacks.message.forEach(cb => cb(fromPeerId, data));
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
peer.on('error', (error) => {
|
|
170
|
+
this.callbacks.error.forEach(cb => cb(error));
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await peer.connect(false);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const peer = this.peers.get(fromPeerId);
|
|
177
|
+
|
|
178
|
+
switch (type) {
|
|
179
|
+
case 'offer':
|
|
180
|
+
await peer.handleOffer(sdp);
|
|
181
|
+
break;
|
|
182
|
+
case 'answer':
|
|
183
|
+
await peer.handleAnswer(sdp);
|
|
184
|
+
break;
|
|
185
|
+
case 'ice':
|
|
186
|
+
await peer.handleIceCandidate(candidate);
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
_emitStatusChange() {
|
|
192
|
+
this.callbacks.statusChange.forEach(cb => cb(this.status));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = MineP2P;
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
apiBaseUrl: 'https://api.vexify.top/mp/',
|
|
3
|
+
iceServers: [
|
|
4
|
+
{ urls: 'stun:stun.cloudflare.com:3478' },
|
|
5
|
+
{ urls: 'stun:stun1.cloudflare.com:3478' },
|
|
6
|
+
{ urls: 'stun:stun2.cloudflare.com:3478' },
|
|
7
|
+
{ urls: 'stun:stun3.cloudflare.com:3478' },
|
|
8
|
+
{ urls: 'stun:stun4.cloudflare.com:3478' }
|
|
9
|
+
],
|
|
10
|
+
ipv6Endpoints: [
|
|
11
|
+
'https://api6.ipify.org',
|
|
12
|
+
'https://ipv6.icanhazip.com',
|
|
13
|
+
'https://v6.ident.me'
|
|
14
|
+
],
|
|
15
|
+
defaultRoom: 'minep2p-default',
|
|
16
|
+
pollTimeout: 30,
|
|
17
|
+
reconnectDelay: 5000,
|
|
18
|
+
maxRetries: 3
|
|
19
|
+
};
|
package/lib/ipv6.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const fetch = require('node-fetch');
|
|
2
|
+
const { networkInterfaces } = require('os');
|
|
3
|
+
const config = require('./config');
|
|
4
|
+
|
|
5
|
+
class IPv6 {
|
|
6
|
+
static async getPublicIPv6() {
|
|
7
|
+
for (let i = 0; i < config.ipv6Endpoints.length; i++) {
|
|
8
|
+
try {
|
|
9
|
+
const response = await fetch(config.ipv6Endpoints[i], {
|
|
10
|
+
timeout: 10000
|
|
11
|
+
});
|
|
12
|
+
if (response.ok) {
|
|
13
|
+
const ip = await response.text();
|
|
14
|
+
if (this.isValidIPv6(ip.trim())) {
|
|
15
|
+
return ip.trim();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.log(`IPv6 attempt ${i + 1} failed:`, error.message);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static async getIPv6WithRetry(retries = config.maxRetries) {
|
|
26
|
+
let result = await this.getPublicIPv6();
|
|
27
|
+
if (result) return result;
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < retries; i++) {
|
|
30
|
+
console.log(`IPv6 retry attempt ${i + 1}/${retries}`);
|
|
31
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
32
|
+
result = await this.getPublicIPv6();
|
|
33
|
+
if (result) return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return this.getLocalIPv6();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static getLocalIPv6() {
|
|
40
|
+
const interfaces = networkInterfaces();
|
|
41
|
+
for (const interfaceName of Object.keys(interfaces)) {
|
|
42
|
+
for (const iface of interfaces[interfaceName]) {
|
|
43
|
+
if (iface.family === 'IPv6' && !iface.internal) {
|
|
44
|
+
return iface.address;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static isValidIPv6(ip) {
|
|
52
|
+
try {
|
|
53
|
+
const parts = ip.split(':');
|
|
54
|
+
if (parts.length > 8) return false;
|
|
55
|
+
|
|
56
|
+
const hasDoubleColon = ip.includes('::');
|
|
57
|
+
if (hasDoubleColon && parts.filter(p => p === '').length > 1) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const part of parts) {
|
|
62
|
+
if (part === '') continue;
|
|
63
|
+
if (!/^[0-9a-fA-F]{1,4}$/.test(part)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = IPv6;
|
package/lib/peer.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const wrtc = require('wrtc');
|
|
2
|
+
const config = require('./config');
|
|
3
|
+
|
|
4
|
+
class Peer {
|
|
5
|
+
constructor(peerId, signaling) {
|
|
6
|
+
this.peerId = peerId;
|
|
7
|
+
this.signaling = signaling;
|
|
8
|
+
this.peerConnection = null;
|
|
9
|
+
this.dataChannel = null;
|
|
10
|
+
this.isInitiator = false;
|
|
11
|
+
this.callbacks = {
|
|
12
|
+
connect: [],
|
|
13
|
+
disconnect: [],
|
|
14
|
+
message: [],
|
|
15
|
+
error: []
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async connect(isInitiator = false) {
|
|
20
|
+
this.isInitiator = isInitiator;
|
|
21
|
+
this.peerConnection = new wrtc.RTCPeerConnection({
|
|
22
|
+
iceServers: config.iceServers
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
this.peerConnection.onicecandidate = (event) => {
|
|
26
|
+
if (event.candidate) {
|
|
27
|
+
this.signaling.sendMessage({
|
|
28
|
+
type: 'ice',
|
|
29
|
+
targetPeerId: this.peerId,
|
|
30
|
+
candidate: event.candidate.toJSON()
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
this.peerConnection.ondatachannel = (event) => {
|
|
36
|
+
this.dataChannel = event.channel;
|
|
37
|
+
this._setupDataChannel();
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
this.peerConnection.onconnectionstatechange = () => {
|
|
41
|
+
if (this.peerConnection.connectionState === 'connected') {
|
|
42
|
+
this.callbacks.connect.forEach(cb => cb());
|
|
43
|
+
} else if (this.peerConnection.connectionState === 'disconnected') {
|
|
44
|
+
this.callbacks.disconnect.forEach(cb => cb());
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (this.isInitiator) {
|
|
49
|
+
this.dataChannel = this.peerConnection.createDataChannel('minep2p');
|
|
50
|
+
this._setupDataChannel();
|
|
51
|
+
|
|
52
|
+
const offer = await this.peerConnection.createOffer();
|
|
53
|
+
await this.peerConnection.setLocalDescription(offer);
|
|
54
|
+
|
|
55
|
+
this.signaling.sendMessage({
|
|
56
|
+
type: 'offer',
|
|
57
|
+
targetPeerId: this.peerId,
|
|
58
|
+
sdp: offer.toJSON()
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async handleOffer(sdp) {
|
|
64
|
+
await this.peerConnection.setRemoteDescription(new wrtc.RTCSessionDescription(sdp));
|
|
65
|
+
|
|
66
|
+
const answer = await this.peerConnection.createAnswer();
|
|
67
|
+
await this.peerConnection.setLocalDescription(answer);
|
|
68
|
+
|
|
69
|
+
this.signaling.sendMessage({
|
|
70
|
+
type: 'answer',
|
|
71
|
+
targetPeerId: this.peerId,
|
|
72
|
+
sdp: answer.toJSON()
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async handleAnswer(sdp) {
|
|
77
|
+
await this.peerConnection.setRemoteDescription(new wrtc.RTCSessionDescription(sdp));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async handleIceCandidate(candidate) {
|
|
81
|
+
try {
|
|
82
|
+
await this.peerConnection.addIceCandidate(new wrtc.RTCIceCandidate(candidate));
|
|
83
|
+
} catch (error) {
|
|
84
|
+
this.callbacks.error.forEach(cb => cb(error));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
send(data) {
|
|
89
|
+
if (this.dataChannel && this.dataChannel.readyState === 'open') {
|
|
90
|
+
this.dataChannel.send(JSON.stringify(data));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
close() {
|
|
95
|
+
if (this.dataChannel) {
|
|
96
|
+
this.dataChannel.close();
|
|
97
|
+
}
|
|
98
|
+
if (this.peerConnection) {
|
|
99
|
+
this.peerConnection.close();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
on(event, callback) {
|
|
104
|
+
if (this.callbacks[event]) {
|
|
105
|
+
this.callbacks[event].push(callback);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
off(event, callback) {
|
|
110
|
+
if (this.callbacks[event]) {
|
|
111
|
+
this.callbacks[event] = this.callbacks[event].filter(cb => cb !== callback);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
_setupDataChannel() {
|
|
116
|
+
this.dataChannel.onopen = () => {
|
|
117
|
+
this.callbacks.connect.forEach(cb => cb());
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
this.dataChannel.onclose = () => {
|
|
121
|
+
this.callbacks.disconnect.forEach(cb => cb());
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
this.dataChannel.onmessage = (event) => {
|
|
125
|
+
try {
|
|
126
|
+
const data = JSON.parse(event.data);
|
|
127
|
+
this.callbacks.message.forEach(cb => cb(data));
|
|
128
|
+
} catch (error) {
|
|
129
|
+
this.callbacks.message.forEach(cb => cb(event.data));
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
this.dataChannel.onerror = (error) => {
|
|
134
|
+
this.callbacks.error.forEach(cb => cb(error));
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = Peer;
|
package/lib/signaling.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
const fetch = require('node-fetch');
|
|
2
|
+
const config = require('./config');
|
|
3
|
+
|
|
4
|
+
class Signaling {
|
|
5
|
+
constructor(peerId) {
|
|
6
|
+
this.peerId = peerId;
|
|
7
|
+
this.room = null;
|
|
8
|
+
this.polling = false;
|
|
9
|
+
this.callbacks = {
|
|
10
|
+
message: [],
|
|
11
|
+
peerJoined: [],
|
|
12
|
+
peerLeft: []
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async join(room) {
|
|
17
|
+
this.room = room;
|
|
18
|
+
const response = await this._post('/join', {
|
|
19
|
+
room: room,
|
|
20
|
+
peerId: this.peerId
|
|
21
|
+
});
|
|
22
|
+
return response;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async leave() {
|
|
26
|
+
if (!this.room) return;
|
|
27
|
+
await this._post('/leave', {
|
|
28
|
+
room: this.room,
|
|
29
|
+
peerId: this.peerId
|
|
30
|
+
});
|
|
31
|
+
this.room = null;
|
|
32
|
+
this.polling = false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async sendMessage(message) {
|
|
36
|
+
if (!this.room) throw new Error('Not joined to any room');
|
|
37
|
+
|
|
38
|
+
return await this._post('/message', {
|
|
39
|
+
room: this.room,
|
|
40
|
+
peerId: this.peerId,
|
|
41
|
+
message: message
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async getRooms() {
|
|
46
|
+
const response = await this._get('/rooms');
|
|
47
|
+
return response;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
startPolling() {
|
|
51
|
+
if (this.polling || !this.room) return;
|
|
52
|
+
this.polling = true;
|
|
53
|
+
this._poll();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
stopPolling() {
|
|
57
|
+
this.polling = false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
on(event, callback) {
|
|
61
|
+
if (this.callbacks[event]) {
|
|
62
|
+
this.callbacks[event].push(callback);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
off(event, callback) {
|
|
67
|
+
if (this.callbacks[event]) {
|
|
68
|
+
this.callbacks[event] = this.callbacks[event].filter(cb => cb !== callback);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async _poll() {
|
|
73
|
+
if (!this.polling || !this.room) return;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const response = await this._get(`/poll?room=${encodeURIComponent(this.room)}&peerId=${encodeURIComponent(this.peerId)}&timeout=${config.pollTimeout}`);
|
|
77
|
+
|
|
78
|
+
if (response && response.messages) {
|
|
79
|
+
for (const msg of response.messages) {
|
|
80
|
+
this._handleMessage(msg);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.log('Poll error:', error.message);
|
|
85
|
+
} finally {
|
|
86
|
+
if (this.polling) {
|
|
87
|
+
setTimeout(() => this._poll(), 1000);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_handleMessage(msg) {
|
|
93
|
+
switch (msg.type) {
|
|
94
|
+
case 'peerJoined':
|
|
95
|
+
this.callbacks.peerJoined.forEach(cb => cb(msg.peerId));
|
|
96
|
+
break;
|
|
97
|
+
case 'peerLeft':
|
|
98
|
+
this.callbacks.peerLeft.forEach(cb => cb(msg.peerId));
|
|
99
|
+
break;
|
|
100
|
+
default:
|
|
101
|
+
this.callbacks.message.forEach(cb => cb(msg));
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async _post(endpoint, data) {
|
|
107
|
+
try {
|
|
108
|
+
const response = await fetch(config.apiBaseUrl + endpoint, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: {
|
|
111
|
+
'Content-Type': 'application/json'
|
|
112
|
+
},
|
|
113
|
+
body: JSON.stringify(data)
|
|
114
|
+
});
|
|
115
|
+
return await response.json();
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.log('POST error:', error.message);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async _get(endpoint) {
|
|
123
|
+
try {
|
|
124
|
+
const response = await fetch(config.apiBaseUrl + endpoint);
|
|
125
|
+
return await response.json();
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.log('GET error:', error.message);
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = Signaling;
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "minep2p",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MineP2P - A Node.js P2P connection library",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mp": "./cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node cli.js start",
|
|
11
|
+
"test": "node test.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["p2p", "webrtc", "networking"],
|
|
14
|
+
"author": "andrew-china",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"wrtc": "^0.4.7",
|
|
18
|
+
"node-fetch": "^3.3.2",
|
|
19
|
+
"uuid": "^9.0.1",
|
|
20
|
+
"yargs": "^17.7.2",
|
|
21
|
+
"chalk": "^5.3.0"
|
|
22
|
+
},
|
|
23
|
+
"publisher": {
|
|
24
|
+
"name": "Vexify",
|
|
25
|
+
"email": "support@vexify.top"
|
|
26
|
+
}
|
|
27
|
+
}
|