This Direct Sockets API proposal relates to the Discourse post Filling the remaining gap between WebSocket, WebRTC and WebTransport.
This API is currently planned as a part of the Isolated Web Apps proposal - check out Telnet Client Demo for a showcase of the API's capabilities.
The initial motivating use case is to support creating a web app that talks to servers and devices that have their own protocols incompatible with what’s available on the web. The web app should be able to talk to a legacy system, without requiring users to change or replace that system.
- Secure Shell
- Remote Desktop Protocol
- printer protocols
- IRC
- IOT smart devices
- Distributed Hash Tables for P2P systems
- Resilient collaboration using IPFS
- Virtual Desktop Infrastructure (VDI)
Web apps can already establish data communications using XMLHttpRequest, WebSocket and WebRTC.
These facilities are designed to be tightly constrained (example design requirements), and don't allow raw TCP or UDP communication. Such facilities were requested but not provided due to the potential for abuse.
If the web server has network access to the required server or device, it can act as relay. For example, a web mail application might use XMLHttpRequest for communication between the browser and the web server, and use SMTP and IMAP between the web server and a mail server.
In scenarios like the following, a vendor would require their customers to deploy a relay web server on each site where devices/servers are in use:
- The target devices might be behind firewalls, not accessible to a central server.
- Communication might be bandwidth-intensive or latency-sensitive.
- There might be legal constraints on where data can be sent.
- On-site communication may need to be resilient to ISP outages.
Developers have used browser plugins - such as Java applets, ActiveX, Adobe Flex or Microsoft Silverlight - to establish raw TCP or UDP communication from the browser, without relaying through a web server.
With the shift away from browser plugins, native apps now provide the main alternative. Widely used APIs include POSIX sockets, Winsock, java.net and System.Net.Sockets.
JavaScript APIs for socket communication have been developed for B2G OS (TCP, UDP) and Chrome Apps (TCP, UDP). An earlier proposed API for the web platform is the TCP and UDP Socket API, which the System Applications Working Group published as an informative Working Group Note and is no longer progressing.
This specification defines a policy-controlled permission identified by the string direct-sockets
. Its default allowlist is self
.
Permissions-Policy: direct-sockets=(self)
This Permissions-Policy
header determines whether a new TCPSocket(...)
, new UDPSocket(...)
or new TCPServerSocket(...)
call immediately rejects with a NotAllowedError
DOMException
.
The API is planned to be available only in Isolated Web Apps which themselves provide a decent level of security thanks to a transparent update model and strict Content Security Policy.
Third party iframes (such as ads) might initiate connections.
The direct-sockets
permissions policy will control access, preventing third party use by default. To further safeguard from potential third-party attacks, IWAs employ a strict Content Security Policy that makes using external resources (i.e. the ones not originating from the Web Bundle itself) difficult and enforce cross-origin-isolation.
Use of the API may violate organization policies, that control which protocols may be used.
User agents may restrict use of the API when enterprise software policies are in effect. For example, user agents might by default not allow use of this API unless the user has permission to install new binaries.
MITM attackers may hijack plaintext connections created using the API.
This API is supposed to be used in Isolated Web Apps which employ a strict Content Security Policy that makes using external resources (i.e. the ones not originating from the Web Bundle itself) difficult -- in particular, prohibit eval()
calls on the retrieved data thanks to script-src 'self'
in the CSP.
We should also facilitate use of TLS on TCP connections.
One option would be to allow TLS to be requested when opening a connection, like the TCP and UDP Socket API's useSecureTransport.
Another option would be to provide a method that upgrades an existing TCP connection to use TLS. Use cases would include SMTP STARTTLS, IMAP STARTTLS and POP STLS.
Applications will be able to request a TCP socket by creating a TCPSocket
class using the new
operator and then waiting for the connection to be established. Refer to the snippets below for a deeper dive.
enum SocketDnsQueryType { "ipv4", "ipv6" };
dictionary TCPSocketOptions {
boolean noDelay = false;
[EnforceRange] unsigned long keepAliveDelay;
[EnforceRange] unsigned long sendBufferSize;
[EnforceRange] unsigned long receiveBufferSize;
SocketDnsQueryType dnsQueryType;
};
dictionary TCPSocketOpenInfo {
ReadableStream readable;
WritableStream writable;
DOMString remoteAddress;
unsigned short remotePort;
DOMString localAddress;
unsigned short localPort;
};
interface TCPSocket {
constructor(
DOMString remoteAddress,
unsigned short remotePort,
optional TCPSocketOptions options = {});
readonly attribute Promise<TCPSocketOpenInfo> opened;
readonly attribute Promise<void> closed;
Promise<void> close();
};
Learn more about using TCPSocket.
const remoteAddress = 'example.com';
const remotePort = 7;
const options = {
noDelay: false,
keepAlive: true,
keepAliveDelay: 720_000
};
let tcpSocket = new TCPSocket(remoteAddress, remotePort, options);
// If rejected by permissions-policy...
if (!tcpSocket) {
return;
}
// Wait for the connection to be established...
let { readable, writable } = await tcpSocket.opened;
// do stuff with the socket
...
// Close the socket. Note that this operation will succeeed if and only if neither readable not writable streams are locked.
tcpSocket.close();
The TCP socket can be used for reading and writing.
- Writable stream accepts
BufferSource
- Readable stream returns
Uint8Array
See ReadableStream
spec for more examples.
TCPSocket
supports both ReadableStreamDefaultReader
and ReadableStreamBYOBReader
.
let tcpSocket = new TCPSocket(...);
let { readable } = await tcpSocket.opened;
let reader = readable.getReader();
let { value, done } = await reader.read();
if (done) {
// this happens either if the socket is exhausted (i.e. when the remote peer
// closes the connection gracefully), or after manual readable.releaseLock()/reader.cancel().
return;
}
const decoder = new TextDecoder();
let message = decoder.decode(value);
...
// Don't forget to call releaseLock() or cancel() on the reader once done.
See WritableStream
spec for more examples.
let tcpSocket = new TCPSocket(...);
let { writable } = await tcpSocket.opened;
let writer = writable.getWriter();
const encoder = new TextEncoder();
let message = "Some user-created tcp data";
await writer.ready;
writer.write(
encoder.encode(message)
).catch(err => console.log(err));
...
// Don't forget to call releaseLock() or cancel()/abort() on the writer once done.
Applications will be able to request a UDP socket by creating a UDPSocket
class using the new
operator and then waiting for the socket to be opened.
UDPSocket
operates in different modes depending on the provided set of options:
- In
bound
mode the socket is bound to a specific local IP endpoint (defined bylocalAddress
and optionallylocalPort
) and is capable of sending/receiving datagrams to/from arbitrary destinations.localAddress
must be a valid IP address.localPort
can be omitted to let the OS pick one on its own.remoteAddress
andremotePort
must be specified on a per-packet basis as part ofUDPMessage
.remoteAddress
(inUDPMessage
) can either be an IP address or a domain name when sending.
- In
connected
mode the socket is bound to an arbitrary local IP endpoint and sends/receives datagrams from/to a single destination (defined byremoteAddress
andremotePort
). *remoteAddress
can either be an IP address or a domain name.
remoteAddress
/remotePort
and localAddress
/localPort
pairs cannot be specified together.
enum SocketDnsQueryType { "ipv4", "ipv6" };
dictionary UDPSocketOptions {
DOMString remoteAddress;
[EnforceRange] unsigned short remotePort;
DOMString localAddress;
[EnforceRange] unsigned short localPort;
SocketDnsQueryType dnsQueryType;
[EnforceRange] unsigned long sendBufferSize;
[EnforceRange] unsigned long receiveBufferSize;
};
dictionary UDPSocketOpenInfo {
ReadableStream readable;
WritableStream writable;
DOMString remoteAddress;
unsigned short remotePort;
DOMString localAddress;
unsigned short localPort;
};
interface UDPSocket {
constructor(UDPSocketOptions options);
readonly attribute Promise<UDPSocketOpenInfo> opened;
readonly attribute Promise<void> closed;
Promise<void> close();
};
Learn more about using UDPSocket.
const remoteAddress = 'example.com'; // could be a raw IP address too
const remotePort = 7;
let udpSocket = new UDPSocket({ remoteAddress, remotePort });
// If rejected by permissions-policy...
if (!udpSocket) {
return;
}
// Wait for the connection to be established...
let { readable, writable } = await udpSocket.opened;
// do stuff with the socket
...
// Close the socket. Note that this operation will succeeed if and only if neither readable not writable streams are locked.
udpSocket.close();
const localAddress = '127.0.0.1';
// Omitting |localPort| allows the OS to pick one on its own.
let udpSocket = new UDPSocket({ localAddress });
// If rejected by permissions-policy...
if (!udpSocket) {
return;
}
// Wait for the connection to be established...
let { readable, writable } = await udpSocket.opened;
// do stuff with the socket
...
// Close the socket. Note that this operation will succeeed if and only if neither readable not writable streams are locked.
udpSocket.close();
The UDP socket can be used for reading and writing. Both streams operate on the UDPMessage
object which is defined as follows (idl):
dictionary UDPMessage {
BufferSource data;
DOMString remoteAddress;
unsigned short remotePort;
};
- Writable stream accepts
data
asBufferSource
- Readable stream returns
data
asUint8Array
See ReadableStream
spec for more examples.
let udpSocket = new UDPSocket({ remoteAddress, remotePort });
let { readable } = await udpSocket.opened;
let reader = readable.getReader();
let { value, done } = await reader.read();
if (done) {
// this happens only if readable.releaseLock() or reader.cancel() is called manually.
return;
}
const decoder = new TextDecoder();
// |value| is a UDPMessage object.
// |remoteAddress| and |remotePort| members of UDPMessage are guaranteed to be null.
let { data } = value;
let message = decoder.decode(data);
...
// Don't forget to call releaseLock() or cancel() on the reader once done.
let udpSocket = new UDPSocket({ localAddress });
let { readable } = await udpSocket.opened;
let reader = readable.getReader();
let { value, done } = await reader.read();
if (done) {
// this happens only if readable.releaseLock() or reader.cancel() is called manually.
return;
}
const decoder = new TextDecoder();
// |value| is a UDPMessage object.
// |remoteAddress| and |remotePort| members of UDPMessage indicate the remote host
// where the datagram came from.
let { data, remoteAddress, remotePort } = value;
let message = decoder.decode(data);
...
// Don't forget to call releaseLock() or cancel() on the reader once done.
See WritableStream
spec for more examples.
let udpSocket = new UDPSocket({ remoteAddress, remotePort });
let { writable } = await udpSocket.opened;
let writer = writable.getWriter();
const encoder = new TextEncoder();
let message = "Some user-created datagram";
// Sends a UDPMessage object where |data| is a Uint8Array.
// Note that |remoteAddress| and |remotePort| must not be specified.
await writer.ready;
writer.write({
data: encoder.encode(message)
}).catch(err => console.log(err));
// Sends a UDPMessage object where |data| is an ArrayBuffer.
// Note that |remoteAddress| and |remotePort| must not be specified.
await writer.ready;
writer.write({
data: encoder.encode(message).buffer
}).catch(err => console.log(err));
...
// Don't forget to call releaseLock() or cancel()/abort() on the writer once done.
let udpSocket = new UDPSocket({ localAddress });
let { writable } = await udpSocket.opened;
let writer = writable.getWriter();
const encoder = new TextEncoder();
let message = "Some user-created datagram";
// Sends a UDPMessage object where |data| is a Uint8Array.
// Note that both |remoteAddress| and |remotePort| must be specified in this case.
// Specifying a domain name as |remoteAddress| requires DNS lookups for each write()
// which is quite inefficient; applications should replace it with the IP address
// of the peer after receiving a response packet.
await writer.ready;
writer.write({
data: encoder.encode(message),
remoteAddress: 'example.com',
remotePort: 7
}).catch(err => console.log(err));
// Sends a UDPMessage object where |data| is an ArrayBuffer.
// Note that both |remoteAddress| and |remotePort| must be specified in this case.
await writer.ready;
writer.write({
data: encoder.encode(message).buffer,
remoteAddress: '98.76.54.32',
remotePort: 18
}).catch(err => console.log(err));
...
// Don't forget to call releaseLock() or cancel()/abort() on the writer once done.
Applications will be able to request a TCP server socket by creating a TCPServerSocket
class using the new
operator and then waiting for the socket to be opened.
localPort
can be omitted to let the OS pick one on its own.backlog
sets the size of the OS accept queue; if not specified, will be substituted by platform default.
dictionary TCPServerSocketOptions {
[EnforceRange] unsigned short localPort,
[EnforceRange] unsigned long backlog;
};
dictionary TCPServerSocketOpenInfo {
ReadableStream readable;
DOMString localAddress;
unsigned short localPort;
};
interface TCPServerSocket {
constructor(
DOMString localAddress,
optional TCPServerSocketOptions options = {});
readonly attribute Promise<TCPServerSocketOpenInfo> opened;
readonly attribute Promise<void> closed;
Promise<void> close();
};
Learn more about TCPServerSocket.
let tcpServerSocket = new TCPServerSocket('::');
// If rejected by permissions-policy...
if (!tcpServerSocket) {
return;
}
// Wait for the connection to be established...
let { readable } = await tcpServerSocket.opened;
// do stuff with the socket
...
// Close the socket. Note that this operation will succeeed if and only if readable stream is not locked.
tcpServerSocket.close();
Connection accepted by TCPServerSocket
are delivered via its ReadableStream
in the form of ready-to-use TCPSocket
objects.
See ReadableStream
spec for more examples.
let tcpServerSocket = new TCPServerSocket('::');
let { readable: tcpServerSocketReadable } = await udpSocket.opened;
let tcpServerSocketReader = tcpServerSocketReadable.getReader();
// |value| is an accepted TCPSocket.
let { value: tcpSocket, done } = await tcpServerSocketReader.read();
if (done) {
// this happens only if readable.releaseLock() or reader.cancel() is called manually.
return;
}
tcpServerSocketReader.releaseLock();
// Send a packet using the newly accepted socket.
const { writable: tcpSocketWritable } = await tcpSocket.opened;
const tcpSocketWriter = tcpSocketWritable.getWriter();
const encoder = new TextEncoder();
const message = "Some user-created tcp data";
await tcpSocketWriter.ready;
await tcpSocketWriter.write(encoder.encode(message));
// Don't forget to call releaseLock() or close()/abort() on the writer once done.