1. Introduction
This section is non-normative.
This document defines an API for establishing TCP connections in Non-Browser JavaScript runtime
environments. Existing standard APIs are reused as much as possible, for example ReadableStream and WritableStream are used for reading and writing from a socket. Some options are inspired
by the existing Node.js net.Socket API.
2. Concepts
2.1. Socket
A socket represents a TCP connection, from which you can read and write data. A socket begins in a connected state (if the socket fails to connect, an error is thrown). While in a connected state, the socket’s ReadableStream and WritableStream can be read from and written to respectively.
A socket becomes closed when its close() method is called. A socket configured with allowHalfOpen: false will close itself when it receives a FIN or RST packet in its read stream.
2.2. Connect
A socket can be constructed using a connect method defined in a sockets module (early implementations may use vendor:sockets for the module name), or defined on a binding object.
The connect method is the primary mechanism for creating a socket instance. It instantiates a socket with a resource identifier and some configuration values. It should synchronously return a socket instance in a pending state (or an error should be thrown). The socket will asynchronously connect depending on the implementation.
2.3. Binding Object
The binding object defines extra socket connect options. The options it contains can modify the
behaviour of the connect invoked on it. Some of the options it can define:
- TLS settings
- The HTTP proxy to use for the socket connection
The binding object is the primary mechanism for runtimes to introduce unique behavior for the connect method. For example, in order to support more TLS settings, a runtime may introduce a TLSSocket interface that extends from Socket. Thus, the binded connect() method could then utilize additional properties and configuration values that are controlled by the new TLSSocket interface.
const tls_socket= new TLSSocket({ key: '...' , cert: '...' }); tls_socket. connect();
Additionally, the binding object does not necessarily have to be an instance of a class, nor does it even have to be JavaScript. It can be any mechanism that exposes the connect() method. Cloudflare achieves this through environment bindings.
3. Socket
3.1. Using a socket
const socket= connect({ hostname: "my-url.com" , port: 43 }); const writer= socket. writable. getWriter(); await writer. write( "Hello, World!\r\n" ); const reader= socket. readable. getReader(); const result= await reader. read(); console. log( Buffer. from ( result. value). toString()); // Hello, World!
3.2. The Socket class
The Socket class is an instance of the socket concept. It should not be instantiated directly (new Socket()), but instead created by calling connect(). A constructor for Socket is intentionally not specified, and is left to implementors to create.
[Exposed=*]dictionary {SocketInfo DOMString =remoteAddress null ;DOMString =localAddress null ;DOMString =alpn null ; }; [Exposed=*]interface {Socket readonly attribute ReadableStream ;readable readonly attribute WritableStream ;writable readonly attribute Promise <SocketInfo >;opened readonly attribute Promise <undefined >;closed Promise <undefined >(close optional any ); [reason NewObject ]Socket (); };startTls
The terms ReadableStream and WritableStream are defined in [WHATWG-STREAMS].
3.3. Attributes
3.3.1. info
The info attribute is a SocketInfo which contains information about the state of the socket.
3.3.2. readable
The readable attribute is a ReadableStream which receives data from the server the socket is connected to.
ReadableStream usage to read data from a socket:
import { connect} from 'sockets' ; const socket= connect( "google.com:80" ); const reader= socket. readable. getReader(); while ( true ) { const { value, done} = await reader. read(); if ( done) { // the ReadableStream has been closed or cancelled break ; } // In many protocols the `value` needs to be decoded to be used: const decoder= new TextDecoder(); console. log( decoder. decode( value)); } reader. releaseLock();
The ReadableStream operates in non-byte mode, that is the type parameter to the
ReadableStream constructor is not set.
This means the stream’s controller is ReadableStreamDefaultController.
3.3.3. writable
The writable attribute is a WritableStream which sends data to the server the socket is connected to.
WritableStream usage to write data to a socket:
import { connect} from 'sockets' ; const socket= connect( "google.com:80" ); const writer= socket. writable. getWriter(); const encoder= new TextEncoder(); writer. write( encoder. encode( "GET / HTTP/1.0\r\n\r\n" ));
3.3.4. opened
The opened attribute is a promise that is resolved when the socket connection has been
successfully established, or is rejected if the connection fails. For sockets which use secure-transport,
the resolution of the opened promise indicates the completion of the secure handshake.
The opened promise resolves a SocketInfo dictionary that optionally provides details
about the connection that has been established.
By default, the opened promise is marked as handled.
3.3.5. closed
The closed attribute is a promise which can be used to keep track of the socket state. It gets resolved under the
following circumstances:
- the
close()method is called on the socket - the socket was constructed with the
allowHalfOpenparameter set tofalse, the ReadableStream is being read from, and the remote connection sends a FIN packet (graceful closure) or a RST packet
closed promise to resolve, if the
ReadableStream is not read then even if the server closes the connection the closed promise
will not resolve.
Whether the promise should resolve without the ReadableStream being read is up for discussion.
It can also be rejected with a SocketError when a socket connection could not be established under the following circumstances:
- The address/port combo requested is blocked
- A transient issue with the runtime
Cancelling the socket’s ReadableStream and closing the socket’s WritableStream does not resolve the closed promise.
3.4. Methods
3.4.1. close(optional any reason)
The close() method closes the socket and its underlying connection. It returns the same promise as the closed attribute.
When called, the ReadableStream and WritableStream associated with the Socket will
be canceled and aborted, respectively. If the reason argument is specified, the reason will be passed on to both the ReadableStream and WritableStream.
If the opened promise is still pending, it will be rejected with the reason.
3.4.2. startTls()
The startTls() method enables opportunistic TLS (otherwise known as StartTLS) which is a requirement for some protocols (primarily postgres/mysql and other DB protocols).
In this secureTransport mode of operation the socket begins the connection in plain-text, with messages read and written without any encryption. Then once the startTls method is called on the socket, the following shall take place:
- the original socket is closed, though the original connection is kept alive
- a secure TLS connection is established over that connection
- a new socket is created and returned from the
startTlscall
The original readers and writers based off the original socket will no longer work. You must create
new readers and writers from the new socket returned by startTls.
The method must fail with an SocketError if:
- called on an existing TLS socket
- the
secureTransportoption defined on theSocketinstance is not equal to"starttls".
3.5. SocketError
SocketError is an instance of TypeError. The error message should start with "SocketError: ".
"connection failed" SocketError.
throw new SocketError( 'connection failed' );
Should result in the following error: Uncaught SocketError [TypeError]: SocketError: connection failed.
4. connect
[Exposed=*]dictionary {SocketAddress DOMString ;hostname unsigned short ; };port typedef (DOMString or SocketAddress );AnySocketAddress enum {SecureTransportKind ,"off" ,"on" }; [Exposed=*]"starttls" dictionary {SocketOptions SecureTransportKind = "off";secureTransport boolean =allowHalfOpen false ;DOMString =sni null ;DOMString []= []; }; [Exposed=*]alpn interface {Connect Socket (connect AnySocketAddress ,address optional SocketOptions ); };opts
The connect() method performs the following steps:
- New
Socketinstance is created with each of its attributes initialised immediately. - The socket’s
openedpromise is set to a new promise. Set opened.[[PromiseIsHandled]] to true. - The socket’s
closedpromise is set to a new promise. Set closed.[[PromiseIsHandled]] to true. - The created
Socketinstance is returned immediately in a pending state. - A connection is established to the specified
SocketAddressasynchronously. - Once the connection is established, set info to a new
SocketInfo, and Resolve opened with info. For a socket using secure transport, the connection is considered to be established once the secure handshake has been completed. - If the connection fails for any reason, the socket’s
closedandopenedpromises are rejected with a SocketError describing why the connection failed. - The instance’s
ReadableStreamandWritableStreamstreams can be used immediately.
At any point during the creation of the Socket instance, connect may throw a SocketError. One case where this can happen is if the input address is incorrectly formatted. Errors which occur asynchronously (after the socket instance has been returned) must reject the socket’s closed promise.
For example, port 25 may be blocked to prevent abuse of SMTP servers and private IPs can be blocked to avoid connecting to private services hosted locally (or on the server’s LAN).
4.1. SocketOptions dictionary
-
secureTransportmember -
The secure transport mode to use.
off- A connection is established in plain text.
on- A TLS connection is established using default CAs
starttls- Initially the same as the
offoption, the connection continues in plain text until thestartTls()method is called
-
alpnmember - The Application-Layer Protocol Negotiation list to send, as an array of strings. If the server agrees with one of the protocols specified in this list, it will return the matching protocol in the
infoproperty. May be specified if and only ifsecureTransportisonorstarttls. -
snimember - The Server Name Indication TLS option to send as part of the TLS handshake. If specified, requests that the server send a certificate with a matching common name. May be specified if and only if
secureTransportisonorstarttls. -
allowHalfOpenmember -
This option is similar to that offered by the Node.js
netmodule and allows interoperability with code which utilizes it.- false
- The WritableStream- and the socket instance- will be automatically closed when a FIN packet is received from the remote connection.
- true
- When a FIN packet is received, the socket will enter a "half-open" state where the ReadableStream is closed but the WritableStream can still be written to.
4.2. SocketInfo dictionary
remoteAddress member
Socket is connected to, for example "example.com:443".
This value may or may not be the same as the address provided to the connect() method used to create the Socket.
localAddress member
"localhost:12345".
alpn property
alpn negotiation list, returns that protocol name as a string, otherwise null.
4.3. AnySocketAddress type
-
SocketAddressdictionary -
The address to connect to. For example
{ hostname: "google.com", port: 443 }.hostname- A connection is established in plain text.
port- A TLS connection is established using default CAs
-
DOMString - A hostname/port combo separated by a colon. For example
"google.com:443".