Automatic update from web-platform-tests webrtc: throw on datachannel send exceeding max message size WebRTC CL: https://webrtc-review.googlesource.com/c/src/+/454580 Bug: chromium:490588131 Change-Id: I7a8ebc456c3951094df95a64382a35f109ed5473 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7645809 Reviewed-by: Henrik Boström <hbos@chromium.org> Commit-Queue: Philipp Hancke <philipp.hancke@googlemail.com> Reviewed-by: Guido Urdaneta <guidou@chromium.org> Cr-Commit-Position: refs/heads/main@{#1614459} -- wpt-commits: 5c9c1666ad29bf5e6ace00ae72b5f262cbba249d wpt-pr: 59215
522 lines
19 KiB
HTML
522 lines
19 KiB
HTML
<!doctype html>
|
|
<meta charset=utf-8>
|
|
<meta name="timeout" content="long">
|
|
<title>RTCDataChannel.prototype.send</title>
|
|
<script src="/resources/testharness.js"></script>
|
|
<script src="/resources/testharnessreport.js"></script>
|
|
<script src="RTCPeerConnection-helper.js"></script>
|
|
<script src="RTCDataChannel-helper.js"></script>
|
|
<script src="third_party/sdp/sdp.js"></script>
|
|
<script>
|
|
'use strict';
|
|
|
|
// Test is based on the following editor draft:
|
|
// https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
|
|
|
|
// The following helper functions are called from RTCPeerConnection-helper.js:
|
|
// createDataChannelPair
|
|
// awaitMessage
|
|
// blobToArrayBuffer
|
|
// assert_equals_typed_array
|
|
|
|
/*
|
|
6.2. RTCDataChannel
|
|
interface RTCDataChannel : EventTarget {
|
|
...
|
|
readonly attribute RTCDataChannelState readyState;
|
|
readonly attribute unsigned long bufferedAmount;
|
|
attribute EventHandler onmessage;
|
|
attribute DOMString binaryType;
|
|
|
|
void send(USVString data);
|
|
void send(Blob data);
|
|
void send(ArrayBuffer data);
|
|
void send(ArrayBufferView data);
|
|
};
|
|
*/
|
|
|
|
// Simple ASCII encoded string
|
|
const helloString = 'hello';
|
|
const emptyString = '';
|
|
// ASCII encoded buffer representation of the string
|
|
const helloBuffer = Uint8Array.of(0x68, 0x65, 0x6c, 0x6c, 0x6f);
|
|
const emptyBuffer = new Uint8Array();
|
|
const helloBlob = new Blob([helloBuffer]);
|
|
|
|
// Unicode string with multiple code units
|
|
const unicodeString = '世界你好';
|
|
// UTF-8 encoded buffer representation of the string
|
|
const unicodeBuffer = Uint8Array.of(
|
|
0xe4, 0xb8, 0x96, 0xe7, 0x95, 0x8c,
|
|
0xe4, 0xbd, 0xa0, 0xe5, 0xa5, 0xbd);
|
|
|
|
/*
|
|
6.2. send()
|
|
2. If channel's readyState attribute is connecting, throw an InvalidStateError.
|
|
*/
|
|
test(t => {
|
|
const pc = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc.close());
|
|
const channel = pc.createDataChannel('test');
|
|
assert_equals(channel.readyState, 'connecting');
|
|
assert_throws_dom('InvalidStateError', () => channel.send(helloString));
|
|
}, 'Calling send() when data channel is in connecting state should throw InvalidStateError');
|
|
|
|
for (const options of [{}, {negotiated: true, id: 0}]) {
|
|
const mode = `${options.negotiated? "Negotiated d" : "D"}atachannel`;
|
|
|
|
/*
|
|
6.2. send()
|
|
3. Execute the sub step that corresponds to the type of the methods argument:
|
|
|
|
string object
|
|
Let data be the object and increase the bufferedAmount attribute
|
|
by the number of bytes needed to express data as UTF-8.
|
|
|
|
[WebSocket]
|
|
5. Feedback from the protocol
|
|
When a WebSocket message has been received
|
|
4. If type indicates that the data is Text, then initialize event's data
|
|
attribute to data.
|
|
*/
|
|
promise_test(t => {
|
|
return createDataChannelPairWithLabel(t, 'string', options)
|
|
.then(([channel1, channel2]) => {
|
|
channel1.send(helloString);
|
|
return awaitMessage(channel2)
|
|
}).then(message => {
|
|
assert_equals(typeof message, 'string',
|
|
'Expect message to be a string');
|
|
|
|
assert_equals(message, helloString);
|
|
});
|
|
}, `${mode} should be able to send simple string and receive as string`);
|
|
|
|
promise_test(t => {
|
|
return createDataChannelPairWithLabel(t, 'unicode', options)
|
|
.then(([channel1, channel2]) => {
|
|
channel1.send(unicodeString);
|
|
return awaitMessage(channel2)
|
|
}).then(message => {
|
|
assert_equals(typeof message, 'string',
|
|
'Expect message to be a string');
|
|
|
|
assert_equals(message, unicodeString);
|
|
});
|
|
}, `${mode} should be able to send unicode string and receive as unicode string`);
|
|
promise_test(t => {
|
|
return createDataChannelPairWithLabel(t, 'recv_string', options)
|
|
.then(([channel1, channel2]) => {
|
|
channel2.binaryType = 'arraybuffer';
|
|
channel1.send(helloString);
|
|
return awaitMessage(channel2);
|
|
}).then(message => {
|
|
assert_equals(typeof message, 'string',
|
|
'Expect message to be a string');
|
|
|
|
assert_equals(message, helloString);
|
|
});
|
|
}, `${mode} should ignore binaryType and always receive string message as string`);
|
|
promise_test(t => {
|
|
return createDataChannelPairWithLabel(t, 'emptystring', options)
|
|
.then(([channel1, channel2]) => {
|
|
channel1.send(emptyString);
|
|
// Send a non-empty string in case the implementation ignores empty messages
|
|
channel1.send(helloString);
|
|
return awaitMessage(channel2)
|
|
}).then(message => {
|
|
assert_equals(typeof message, 'string',
|
|
'Expect message to be a string');
|
|
|
|
assert_equals(message, emptyString);
|
|
});
|
|
}, `${mode} should be able to send an empty string and receive an empty string`);
|
|
|
|
/*
|
|
6.2. send()
|
|
3. Execute the sub step that corresponds to the type of the methods argument:
|
|
ArrayBufferView object
|
|
Let data be the data stored in the section of the buffer described
|
|
by the ArrayBuffer object that the ArrayBufferView object references
|
|
and increase the bufferedAmount attribute by the length of the
|
|
ArrayBufferView in bytes.
|
|
|
|
[WebSocket]
|
|
5. Feedback from the protocol
|
|
When a WebSocket message has been received
|
|
4. If binaryType is set to "arraybuffer", then initialize event's data
|
|
attribute to a new read-only ArrayBuffer object whose contents are data.
|
|
|
|
[WebIDL]
|
|
4.1. ArrayBufferView
|
|
typedef (Int8Array or Int16Array or Int32Array or
|
|
Uint8Array or Uint16Array or Uint32Array or Uint8ClampedArray or
|
|
Float32Array or Float64Array or DataView) ArrayBufferView;
|
|
*/
|
|
promise_test(t => {
|
|
return createDataChannelPairWithLabel(t, 'array_to_arraybuffer', options)
|
|
.then(([channel1, channel2]) => {
|
|
channel2.binaryType = 'arraybuffer';
|
|
channel1.send(helloBuffer);
|
|
return awaitMessage(channel2)
|
|
}).then(messageBuffer => {
|
|
assert_true(messageBuffer instanceof ArrayBuffer,
|
|
'Expect messageBuffer to be an ArrayBuffer');
|
|
|
|
assert_equals_typed_array(messageBuffer, helloBuffer.buffer);
|
|
});
|
|
}, `${mode} should be able to send Uint8Array message and receive as ArrayBuffer`);
|
|
|
|
/*
|
|
6.2. send()
|
|
3. Execute the sub step that corresponds to the type of the methods argument:
|
|
ArrayBuffer object
|
|
Let data be the data stored in the buffer described by the ArrayBuffer
|
|
object and increase the bufferedAmount attribute by the length of the
|
|
ArrayBuffer in bytes.
|
|
*/
|
|
promise_test(t => {
|
|
return createDataChannelPairWithLabel(t, 'arraybuffer', options)
|
|
.then(([channel1, channel2]) => {
|
|
channel2.binaryType = 'arraybuffer';
|
|
channel1.send(helloBuffer.buffer);
|
|
return awaitMessage(channel2)
|
|
}).then(messageBuffer => {
|
|
assert_true(messageBuffer instanceof ArrayBuffer,
|
|
'Expect messageBuffer to be an ArrayBuffer');
|
|
|
|
assert_equals_typed_array(messageBuffer, helloBuffer.buffer);
|
|
});
|
|
}, `${mode} should be able to send ArrayBuffer message and receive as ArrayBuffer`);
|
|
|
|
promise_test(t => {
|
|
return createDataChannelPairWithLabel(t, 'empty_arraybuffer', options)
|
|
.then(([channel1, channel2]) => {
|
|
channel2.binaryType = 'arraybuffer';
|
|
channel1.send(emptyBuffer.buffer);
|
|
// Send a non-empty buffer in case the implementation ignores empty messages
|
|
channel1.send(helloBuffer.buffer);
|
|
return awaitMessage(channel2)
|
|
}).then(messageBuffer => {
|
|
assert_true(messageBuffer instanceof ArrayBuffer,
|
|
'Expect messageBuffer to be an ArrayBuffer');
|
|
|
|
assert_equals_typed_array(messageBuffer, emptyBuffer.buffer);
|
|
});
|
|
}, `${mode} should be able to send an empty ArrayBuffer message and receive as ArrayBuffer`);
|
|
|
|
/*
|
|
6.2. send()
|
|
3. Execute the sub step that corresponds to the type of the methods argument:
|
|
Blob object
|
|
Let data be the raw data represented by the Blob object and increase
|
|
the bufferedAmount attribute by the size of data, in bytes.
|
|
*/
|
|
promise_test(t => {
|
|
return createDataChannelPairWithLabel(t, 'blob_to_arraybuffer', options)
|
|
.then(([channel1, channel2]) => {
|
|
channel2.binaryType = 'arraybuffer';
|
|
channel1.send(helloBlob);
|
|
return awaitMessage(channel2);
|
|
}).then(messageBuffer => {
|
|
assert_true(messageBuffer instanceof ArrayBuffer,
|
|
'Expect messageBuffer to be an ArrayBuffer');
|
|
|
|
assert_equals_typed_array(messageBuffer, helloBuffer.buffer);
|
|
});
|
|
}, `${mode} should be able to send Blob message and receive as ArrayBuffer`);
|
|
|
|
/*
|
|
[WebSocket]
|
|
5. Feedback from the protocol
|
|
When a WebSocket message has been received
|
|
4. If binaryType is set to "blob", then initialize event's data attribute
|
|
to a new Blob object that represents data as its raw data.
|
|
*/
|
|
promise_test(t => {
|
|
return createDataChannelPairWithLabel(t, 'arraybuffer_to_blob', options)
|
|
.then(([channel1, channel2]) => {
|
|
channel2.binaryType = 'blob';
|
|
channel1.send(helloBuffer);
|
|
return awaitMessage(channel2);
|
|
})
|
|
.then(messageBlob => {
|
|
assert_true(messageBlob instanceof Blob,
|
|
'Expect received messageBlob to be a Blob');
|
|
|
|
return blobToArrayBuffer(messageBlob);
|
|
}).then(messageBuffer => {
|
|
assert_true(messageBuffer instanceof ArrayBuffer,
|
|
'Expect messageBuffer to be an ArrayBuffer');
|
|
|
|
assert_equals_typed_array(messageBuffer, helloBuffer.buffer);
|
|
});
|
|
}, `${mode} should be able to send ArrayBuffer message and receive as Blob`);
|
|
|
|
/*
|
|
6.2. RTCDataChannel
|
|
binaryType
|
|
The binaryType attribute must, on getting, return the value to which it was
|
|
last set. On setting, the user agent must set the IDL attribute to the new
|
|
value. When a RTCDataChannel object is created, the binaryType attribute must
|
|
be initialized to the string "blob".
|
|
*/
|
|
promise_test(t => {
|
|
return createDataChannelPairWithLabel(t, 'arraybuffer_recv', options)
|
|
.then(([channel1, channel2]) => {
|
|
assert_equals(channel2.binaryType, 'arraybuffer',
|
|
'Expect initial binaryType value to be arraybuffer');
|
|
|
|
channel1.send(helloBuffer);
|
|
return awaitMessage(channel2);
|
|
}).then(messageBuffer => {
|
|
assert_true(messageBuffer instanceof ArrayBuffer,
|
|
'Expect messageBuffer to be an ArrayBuffer');
|
|
|
|
assert_equals_typed_array(messageBuffer, helloBuffer.buffer);
|
|
});
|
|
}, `${mode} binaryType should receive message as ArrayBuffer by default`);
|
|
|
|
// Test sending 3 messages: helloBuffer, unicodeString, helloBlob
|
|
promise_test(t => {
|
|
const receivedMessages = [];
|
|
|
|
const onMessage = t.step_func(event => {
|
|
const { data } = event;
|
|
receivedMessages.push(data);
|
|
|
|
if(receivedMessages.length === 3) {
|
|
assert_equals_typed_array(receivedMessages[0], helloBuffer.buffer);
|
|
assert_equals(receivedMessages[1], unicodeString);
|
|
assert_equals_typed_array(receivedMessages[2], helloBuffer.buffer);
|
|
|
|
t.done();
|
|
}
|
|
});
|
|
|
|
return createDataChannelPairWithLabel(t, 'multitype', options)
|
|
.then(([channel1, channel2]) => {
|
|
channel2.binaryType = 'arraybuffer';
|
|
channel2.addEventListener('message', onMessage);
|
|
|
|
channel1.send(helloBuffer);
|
|
channel1.send(unicodeString);
|
|
channel1.send(helloBlob);
|
|
|
|
}).catch(t.step_func(err =>
|
|
assert_unreached(`Unexpected promise rejection: ${err}`)));
|
|
}, `${mode} sending multiple messages with different types should succeed and be received`);
|
|
|
|
/*
|
|
[Deferred]
|
|
6.2. RTCDataChannel
|
|
The send() method is being amended in w3c/webrtc-pc#1209 to throw error instead
|
|
of closing data channel when buffer is full
|
|
|
|
send()
|
|
4. If channel's underlying data transport is not established yet, or if the
|
|
closing procedure has started, then abort these steps.
|
|
5. Attempt to send data on channel's underlying data transport; if the data
|
|
cannot be sent, e.g. because it would need to be buffered but the buffer
|
|
is full, the user agent must abruptly close channel's underlying data
|
|
transport with an error.
|
|
|
|
test(t => {
|
|
const pc = new RTCPeerConnection();
|
|
const channel = pc.createDataChannel('test');
|
|
channel.close();
|
|
assert_equals(channel.readyState, 'closing');
|
|
channel.send(helloString);
|
|
}, 'Calling send() when data channel is in closing state should succeed');
|
|
*/
|
|
}
|
|
|
|
promise_test(async t => {
|
|
// This test uses some bundle shenanigans to cause packet loss
|
|
// The intent is to create a situation where a small message is received
|
|
// when there is a partially received large message, and check whether
|
|
// that small message is misinterpreted as part of the large message
|
|
// instead of its own thing.
|
|
const pc1 = new RTCPeerConnection({bundlePolicy: 'balanced'});
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
t.add_cleanup(() => pc2.close());
|
|
pc1.addTransceiver('audio');
|
|
pc1.addTransceiver('video');
|
|
const channel1 = pc1.createDataChannel('foo1', {id: 1, ordered: false, negotiated: true});
|
|
const channel2 = pc2.createDataChannel('foo2', {id: 1, ordered: false, negotiated: true});
|
|
const channel1Open = new Promise(r => channel1.onopen = r);
|
|
const channel2Open = new Promise(r => channel2.onopen = r);
|
|
exchangeIceCandidates(pc1, pc2);
|
|
|
|
const size = 1024*1024*5;
|
|
// TODO: crbug.com/490588131: this exceeds max-message-size which leads to unrelated failures.
|
|
// Setting this to max-message-size causes a crash however.
|
|
const bigMessage = 'a'.repeat(size);
|
|
const smallMessage = 'boom';
|
|
const numLittle = 1000;
|
|
|
|
const receivedMessages = new Promise((resolve, reject) => {
|
|
let littleReceived = 0;
|
|
let bigReceived = false;
|
|
channel2.onmessage = ({data}) => {
|
|
try {
|
|
if (data.length == size) {
|
|
assert_false(bigReceived, 'Only received big message once');
|
|
bigReceived = true;
|
|
assert_equals(data, bigMessage, 'Big message has correct contents');
|
|
} else if (data.length == smallMessage.length) {
|
|
littleReceived++;
|
|
assert_equals(data, smallMessage, 'Small message has correct contents');
|
|
assert_less_than_equal(littleReceived, numLittle, 'Got no more than expected small messages');
|
|
} else {
|
|
assert_true(false, `Message was an expected size (got ${data.length}, last bytes are ${data.slice(-20)})`);
|
|
}
|
|
|
|
if (littleReceived == 1000 && bigReceived) {
|
|
resolve();
|
|
}
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
};
|
|
});
|
|
|
|
function rebundleMids(offer, mids) {
|
|
// Disable any existing groups
|
|
let sdp = offer.sdp.replaceAll('a=group:BUNDLE','a=moop:BUNGLE');
|
|
sdp = sdp.replaceAll('a=msid-semantic:', 'a=msid-pedantic:');
|
|
// Create group attrs for the mids we want
|
|
const midsString = mids.join(' ');
|
|
sdp = sdp.replace('\r\nm=', `\r\na=group:BUNDLE ${midsString}\r\na=msid-semantic:WMS *\r\nm=`);
|
|
return {type: 'offer', sdp};
|
|
}
|
|
|
|
await pc1.setLocalDescription();
|
|
let offer = pc1.localDescription;
|
|
|
|
// offer should have three separate transports, one for each mid
|
|
const sections = SDPUtils.splitSections(offer.sdp);
|
|
assert_equals(sections.length, 4);
|
|
const mids = sections.slice(1).map(section => SDPUtils.getMid(section));
|
|
assert_equals(mids.length, 3);
|
|
|
|
// Cause the DataChannel msection to be bundled with the audio msection
|
|
await pc2.setRemoteDescription(rebundleMids(offer, [mids[0], mids[2]]));
|
|
await pc2.setLocalDescription();
|
|
await pc1.setRemoteDescription(pc2.localDescription);
|
|
await channel1Open;
|
|
await channel2Open;
|
|
|
|
channel1.send(bigMessage);
|
|
|
|
// Send until we're almost done with the first (big) message
|
|
channel1.bufferedAmountLowThreshold = 20000;
|
|
await new Promise(r => channel1.onbufferedamountlow = r);
|
|
|
|
// Ok, now for the shenanigans
|
|
let reoffer = await pc1.createOffer();
|
|
// Make pc2 _think_ the datachannel is now bundled with the video msection,
|
|
// so it will discard the packets
|
|
await pc2.setRemoteDescription(rebundleMids(reoffer, [mids[1], mids[2]]));
|
|
await pc2.setLocalDescription();
|
|
|
|
for (let i = 0; i < numLittle; i++) {
|
|
channel1.send('boom');
|
|
}
|
|
|
|
// Now, put it back the way it is supposed to be
|
|
await pc1.setLocalDescription();
|
|
reoffer = pc1.localDescription;
|
|
await pc2.setRemoteDescription(rebundleMids(reoffer, [mids[0], mids[2]]));
|
|
await pc2.setLocalDescription();
|
|
await pc1.setRemoteDescription(pc2.localDescription);
|
|
await receivedMessages;
|
|
}, `Sending multiple messages simultaneously in unordered mode works reliably`);
|
|
|
|
promise_test(async t => {
|
|
const offerer = new RTCPeerConnection();
|
|
const answerer = new RTCPeerConnection();
|
|
t.add_cleanup(() => offerer.close());
|
|
t.add_cleanup(() => answerer.close());
|
|
const channel1 = offerer.createDataChannel('foo');
|
|
|
|
const toSend = [];
|
|
for (let i = 0; i < 100; i++) {
|
|
toSend.push(`So anyway I started blastin' ${i}`);
|
|
}
|
|
|
|
function blastMessages(channel) {
|
|
assert_equals(channel.readyState, 'open', 'channel should already be open');
|
|
for (const message of toSend) {
|
|
channel.send(message);
|
|
}
|
|
}
|
|
|
|
// Set up both channels to send messages as soon as they are open
|
|
channel1.onopen = () => blastMessages(channel1);
|
|
|
|
const channel2Created = new Promise(r => {
|
|
answerer.ondatachannel = ({channel}) => {
|
|
blastMessages(channel);
|
|
r(channel);
|
|
}
|
|
});
|
|
|
|
async function receiveMessages(channel) {
|
|
const received = [];
|
|
while (received.length < toSend.length) {
|
|
const {data} = await new Promise(r => channel.onmessage = r);
|
|
received.push(data);
|
|
}
|
|
return received;
|
|
}
|
|
|
|
const received1 = receiveMessages(channel1);
|
|
|
|
await negotiate(offerer, answerer);
|
|
|
|
const channel2 = await channel2Created;
|
|
assert_array_equals(await receiveMessages(channel2), toSend);
|
|
assert_array_equals(await received1, toSend);
|
|
}, 'Sending before the other side is open should work');
|
|
|
|
promise_test(async t => {
|
|
const offerer = new RTCPeerConnection();
|
|
const answerer = new RTCPeerConnection();
|
|
t.add_cleanup(() => offerer.close());
|
|
t.add_cleanup(() => answerer.close());
|
|
|
|
const sender = offerer.createDataChannel('foo');
|
|
const data = 'something';
|
|
sender.onopen = () => {
|
|
sender.send(data);
|
|
};
|
|
const receiverPromise = new Promise(r => {
|
|
answerer.ondatachannel = ({channel}) => {
|
|
channel.onmessage = (e) => r(e.data);
|
|
}
|
|
});
|
|
await negotiate(offerer, answerer);
|
|
assert_equals(data, await receiverPromise);
|
|
}, 'Sending in onopen should work');
|
|
|
|
promise_test(async t => {
|
|
const offerer = new RTCPeerConnection();
|
|
const answerer = new RTCPeerConnection();
|
|
t.add_cleanup(() => offerer.close());
|
|
t.add_cleanup(() => answerer.close());
|
|
|
|
const data = 'something';
|
|
answerer.ondatachannel = ({channel}) => {
|
|
channel.send(data);onmessage = (e) => r(e.data);
|
|
}
|
|
const sender = offerer.createDataChannel('foo');
|
|
const senderPromise = new Promise(r => {
|
|
sender.onmessage = e => r(data);
|
|
});
|
|
await negotiate(offerer, answerer);
|
|
assert_equals(data, await senderPromise);
|
|
}, 'Sending in ondatachannel should work');
|
|
</script>
|