@walkhi/code-relax 0.1.0-beta.40 → 0.1.0-beta.41
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/dist/build-platform.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"layout":"npm-platform-packages","version":"0.1.0-beta.
|
|
1
|
+
{"layout":"npm-platform-packages","version":"0.1.0-beta.41"}
|
|
@@ -23,6 +23,6 @@
|
|
|
23
23
|
`;gathering=${known(this.gathering, ['new', 'in-progress', 'gathering', 'complete'])}` +
|
|
24
24
|
`;local=${counts(this.local)};remote=${counts(this.remote)}` +
|
|
25
25
|
`;open=${Number(this.open)};ping=${Number(this.ping)};pong=${Number(this.pong)}` +
|
|
26
|
-
`;error=${known(this.error, ['none', 'stun', 'description', 'candidate', 'channel', 'frame', 'peer', 'initialization', 'signal'])}`;
|
|
26
|
+
`;error=${known(this.error, ['none', 'stun', 'description', 'candidate', 'channel', 'frame', 'peer', 'initialization', 'signal', 'ice-policy'])}`;
|
|
27
27
|
}
|
|
28
28
|
});
|
|
@@ -29,10 +29,13 @@
|
|
|
29
29
|
const data = binary ? (value instanceof Uint8Array ? value : new Uint8Array(value)) : value;
|
|
30
30
|
const size = binary ? data.byteLength : new TextEncoder().encode(data).length;
|
|
31
31
|
if (!size || size > (binary ? MAX : PART) || this.bufferedAmount + size > LIMIT) throw new Error('文件直连分片或队列超限');
|
|
32
|
+
const negotiated = this.io.maxMessageSize?.();
|
|
33
|
+
const frameBytes = Number.isFinite(negotiated) && negotiated > 0 ? Math.min(PART, Math.max(1, Math.floor(negotiated))) : PART;
|
|
32
34
|
const frames = binary ? [JSON.stringify({ type: 'rtc-file-bytes', length: size })] : [];
|
|
33
|
-
if (binary) for (let i = 0; i < size; i +=
|
|
35
|
+
if (binary) for (let i = 0; i < size; i += frameBytes) frames.push(data.slice(i, i + frameBytes));
|
|
34
36
|
else frames.push(data);
|
|
35
37
|
const sizes = frames.map(frame => typeof frame === 'string' ? new TextEncoder().encode(frame).length : frame.byteLength);
|
|
38
|
+
if (sizes.some(length => length > frameBytes)) throw new Error('文件传输消息超过协商上限');
|
|
36
39
|
const framedSize = sizes.reduce((total, length) => total + length, 0);
|
|
37
40
|
if (this.bufferedAmount + framedSize > LIMIT) throw new Error('文件直连队列超限');
|
|
38
41
|
this.outbound.push({ frames, sizes, index: 0 }); this.queued += framedSize; this.pump();
|
|
@@ -66,7 +69,7 @@
|
|
|
66
69
|
this.waitForPartial(); return;
|
|
67
70
|
}
|
|
68
71
|
const bytes = value instanceof Uint8Array ? value : new Uint8Array(value), part = this.partial;
|
|
69
|
-
if (!part || bytes.length
|
|
72
|
+
if (!part || bytes.length < 1 || bytes.length > Math.min(PART, part.data.length - part.offset)) throw new Error();
|
|
70
73
|
this.waitForPartial(); this.io.received?.(); part.data.set(bytes, part.offset); part.offset += bytes.length;
|
|
71
74
|
if (part.offset === part.data.length) {
|
|
72
75
|
this.partial = null; clearTimeout(this.timer); this.timer = null;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
if (typeof module === 'object' && module.exports) module.exports = factory();
|
|
3
|
+
else root.CodeRelaxIcePolicy = factory();
|
|
4
|
+
})(globalThis, () => {
|
|
5
|
+
const directType = type => ['host', 'srflx', 'prflx'].includes(type);
|
|
6
|
+
const candidate = value => {
|
|
7
|
+
if (typeof value !== 'string' || value.length > 2048 || /[\r\n]/.test(value)) return false;
|
|
8
|
+
const fields = value.trim().split(/\s+/);
|
|
9
|
+
return /^(?:a=)?candidate:/.test(fields[0]) && fields[6] === 'typ' && directType(fields[7]);
|
|
10
|
+
};
|
|
11
|
+
return {
|
|
12
|
+
directType, candidate,
|
|
13
|
+
servers: values => Array.isArray(values) && values.length <= 4 && values.every(value => {
|
|
14
|
+
if (typeof value !== 'string' || !/^stun:[a-zA-Z0-9.-]+:[0-9]{1,5}$/.test(value)) return false;
|
|
15
|
+
const port = Number(value.split(':').at(-1));
|
|
16
|
+
return port > 0 && port <= 65535;
|
|
17
|
+
}),
|
|
18
|
+
sdp: value => typeof value === 'string' && value.length <= 65536 &&
|
|
19
|
+
value.split(/\r?\n/).filter(line => line.startsWith('a=candidate:')).every(candidate),
|
|
20
|
+
pair: (localType, remoteType) => directType(localType) && directType(remoteType),
|
|
21
|
+
};
|
|
22
|
+
});
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import P2pDiagnostics from '../../shared/p2p-diagnostics.cjs';
|
|
3
3
|
import P2pDataFramer from '../../shared/p2p-data-channel.cjs';
|
|
4
4
|
import FileChannel from '../../shared/p2p-file-channel.cjs';
|
|
5
|
+
import IcePolicy from '../../shared/p2p-ice-policy.cjs';
|
|
5
6
|
|
|
6
7
|
const PROBE_UDP_PORT_RANGE = Object.freeze({ begin: 42000, end: 42031 });
|
|
7
8
|
const HIGH_WATER = 1024 * 1024;
|
|
@@ -26,7 +27,7 @@ export async function createP2pProbe({ signal, report = () => {}, onData = () =>
|
|
|
26
27
|
const files = new Map();
|
|
27
28
|
const nonce = randomUUID();
|
|
28
29
|
const idle = new P2pDataFramer.IdleWatch(message => {
|
|
29
|
-
|
|
30
|
+
channel.sendMessage(JSON.stringify(message));
|
|
30
31
|
}, () => finish('idle-timeout'));
|
|
31
32
|
const timer = setTimeout(() => finish(verified ? 'closed' : 'timeout'), 15000);
|
|
32
33
|
function finish(state) {
|
|
@@ -40,6 +41,8 @@ export async function createP2pProbe({ signal, report = () => {}, onData = () =>
|
|
|
40
41
|
}
|
|
41
42
|
function verify() {
|
|
42
43
|
if (verified || !pingReceived || !pongReceived) return;
|
|
44
|
+
const pair = peer.getSelectedCandidatePair();
|
|
45
|
+
if (!pair || !IcePolicy.pair(pair.local.type, pair.remote.type)) { diagnostic.error = 'ice-policy'; finish('failed'); return; }
|
|
43
46
|
verified = true; clearTimeout(timer);
|
|
44
47
|
if (remoteDirect) idle.received();
|
|
45
48
|
report(remoteDirect ? 'direct' : 'verified', diagnostic.snapshot('verified'));
|
|
@@ -54,7 +57,7 @@ export async function createP2pProbe({ signal, report = () => {}, onData = () =>
|
|
|
54
57
|
}
|
|
55
58
|
peer.onDataChannel(raw => {
|
|
56
59
|
if (closed || purpose !== 'file' || !onFileChannel || raw.getLabel() !== 'code-relax-file-v1' || files.size >= 4) { raw.close(); return; }
|
|
57
|
-
const socket = new FileChannel({ buffered: () => raw.bufferedAmount(),
|
|
60
|
+
const socket = new FileChannel({ buffered: () => raw.bufferedAmount(), maxMessageSize: () => peer.maxMessageSize(),
|
|
58
61
|
send: value => typeof value === 'string' ? raw.sendMessage(value) : raw.sendMessageBinary(value), close: () => raw.close(), received: () => idle.received() });
|
|
59
62
|
files.set(raw, socket);
|
|
60
63
|
socket.addEventListener('close', () => files.delete(raw));
|
|
@@ -75,7 +78,8 @@ export async function createP2pProbe({ signal, report = () => {}, onData = () =>
|
|
|
75
78
|
try {
|
|
76
79
|
while (outbound.length && channel.bufferedAmount() < HIGH_WATER) {
|
|
77
80
|
const item = outbound[0], frame = framer.frame(item);
|
|
78
|
-
|
|
81
|
+
// false means accepted into libdatachannel's buffer, not rejected. Never replay it.
|
|
82
|
+
channel.sendMessage(frame);
|
|
79
83
|
if (framer.commit(item)) { outbound.shift(); queuedBytes -= item.bytes; }
|
|
80
84
|
}
|
|
81
85
|
} catch { diagnostic.error = 'channel'; finish('failed'); }
|
|
@@ -92,7 +96,11 @@ export async function createP2pProbe({ signal, report = () => {}, onData = () =>
|
|
|
92
96
|
peer.onGatheringStateChange(state => { if (!closed) diagnostic.gathering = state; });
|
|
93
97
|
peer.onStateChange(state => { if (!closed && (state === 'failed' || state === 'closed')) { diagnostic.error = 'peer'; finish('failed'); } });
|
|
94
98
|
peer.onLocalDescription((sdp, type) => { if (!closed) { diagnostic.offer = type === 'offer'; signal({ kind: type, sdp, iceServers, direct: 1 }); } });
|
|
95
|
-
peer.onLocalCandidate((candidate, mid) => {
|
|
99
|
+
peer.onLocalCandidate((candidate, mid) => {
|
|
100
|
+
if (closed) return;
|
|
101
|
+
if (!IcePolicy.candidate(candidate)) { diagnostic.error = 'ice-policy'; finish('failed'); return; }
|
|
102
|
+
diagnostic.candidate('local', candidate); signal({ kind: 'candidate', candidate, mid });
|
|
103
|
+
});
|
|
96
104
|
channel = peer.createDataChannel('code-relax-probe-v1');
|
|
97
105
|
channel.setBufferedAmountLowThreshold(LOW_WATER);
|
|
98
106
|
channel.onBufferedAmountLow(pump);
|
|
@@ -124,12 +132,14 @@ export async function createP2pProbe({ signal, report = () => {}, onData = () =>
|
|
|
124
132
|
receive(message) {
|
|
125
133
|
if (closed) return;
|
|
126
134
|
if (message.kind === 'answer' && !answered) {
|
|
135
|
+
if (!IcePolicy.sdp(message.sdp)) { diagnostic.error = 'ice-policy'; finish('failed'); return; }
|
|
127
136
|
try { peer.setRemoteDescription(message.sdp, 'answer'); }
|
|
128
137
|
catch { diagnostic.error = 'description'; finish('failed'); return; }
|
|
129
138
|
answered = true; remoteDirect = message.direct === 1; diagnostic.answer = true;
|
|
130
139
|
for (const item of candidates) peer.addRemoteCandidate(item.candidate, item.mid);
|
|
131
140
|
candidates = [];
|
|
132
141
|
} else if (message.kind === 'candidate') {
|
|
142
|
+
if (!IcePolicy.candidate(message.candidate)) { diagnostic.error = 'ice-policy'; finish('failed'); return; }
|
|
133
143
|
diagnostic.candidate('remote', message.candidate);
|
|
134
144
|
if (answered) peer.addRemoteCandidate(message.candidate, message.mid);
|
|
135
145
|
else if (candidates.length < 64) candidates.push(message);
|
|
@@ -145,15 +155,14 @@ export async function createP2pProbe({ signal, report = () => {}, onData = () =>
|
|
|
145
155
|
export function validProbeSignal(value) {
|
|
146
156
|
if (!value || value.version !== 1 || !/^[\w-]{16,128}$/.test(value.probeId || '')) return false;
|
|
147
157
|
if (['start', 'close'].includes(value.kind)) return true;
|
|
148
|
-
if (['offer', 'answer'].includes(value.kind)) return
|
|
158
|
+
if (['offer', 'answer'].includes(value.kind)) return IcePolicy.sdp(value.sdp) &&
|
|
149
159
|
(value.direct === undefined || value.direct === 0 || value.direct === 1) &&
|
|
150
160
|
(value.iceServers === undefined || validProbeIceServers(value.iceServers));
|
|
151
|
-
if (value.kind === 'candidate') return
|
|
161
|
+
if (value.kind === 'candidate') return IcePolicy.candidate(value.candidate) &&
|
|
152
162
|
typeof value.mid === 'string' && value.mid.length <= 64;
|
|
153
163
|
return false;
|
|
154
164
|
}
|
|
155
165
|
|
|
156
166
|
export function validProbeIceServers(value) {
|
|
157
|
-
return
|
|
158
|
-
typeof item === 'string' && /^stun:[a-zA-Z0-9.-]+:[0-9]{1,5}$/.test(item) && Number(item.split(':').at(-1)) <= 65535);
|
|
167
|
+
return IcePolicy.servers(value);
|
|
159
168
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walkhi/code-relax",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.41",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"tag": "beta"
|
|
42
42
|
},
|
|
43
43
|
"optionalDependencies": {
|
|
44
|
-
"@walkhi/code-relax-native-win32-x64": "0.1.0-beta.
|
|
45
|
-
"@walkhi/code-relax-native-darwin-arm64": "0.1.0-beta.
|
|
46
|
-
"@walkhi/code-relax-native-darwin-x64": "0.1.0-beta.
|
|
44
|
+
"@walkhi/code-relax-native-win32-x64": "0.1.0-beta.41",
|
|
45
|
+
"@walkhi/code-relax-native-darwin-arm64": "0.1.0-beta.41",
|
|
46
|
+
"@walkhi/code-relax-native-darwin-x64": "0.1.0-beta.41"
|
|
47
47
|
}
|
|
48
48
|
}
|