-
Notifications
You must be signed in to change notification settings - Fork 2
/
index.bs
405 lines (309 loc) · 16.8 KB
/
index.bs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
<pre class=metadata>
Title: Sockets API
Shortname: common-web-platform-api
Group: wintercg
Status: w3c/CG-DRAFT
Level: none
URL: https://sockets-api.proposal.wintercg.org/
Repository: https://github.com/wintercg/proposal-sockets-api
Editor: Dominik Picheta, Cloudflare https://cloudflare.com/, dominik@cloudflare.com
Editor: Ethan Arrowood, ethan@arrowood.dev
Editor: James M Snell, Cloudflare https://cloudflare.com/, jsnell@cloudflare.com
Abstract: Sockets API for Non-Browser EcmaScript-based runtimes.
Markup Shorthands: markdown yes
Markup Shorthands: idl yes
</pre>
<pre class=link-defaults>
spec:url; type:interface; text:URL
spec:html; type:attribute; for:Window; text:navigator
spec:html; type:method; for:Connect; text:connect
</pre>
<h2 id="intro">Introduction</h2>
<div class="non-normative">
<em>This section is non-normative.</em>
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.
</div>
<h2 id="concepts">Concepts</h2>
<h3 id="socket-concept-section">Socket</h3>
A <dfn export id="socket-concept">socket</dfn> represents a TCP connection, from which you can read and write data. A socket begins in a <i>connected</i> state (if the socket fails to connect, an error is thrown). While in a <i>connected</i> state, the socket's {{ReadableStream}} and {{WritableStream}} can be read from and written to respectively.
A socket becomes <i>closed</i> 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.
<h3 id="connect-concept-section">Connect</h3>
<p class="note">The [=connect=] method here is defined in a `sockets` module only for initial implementation purposes. It is imagined that in a finalized standard definition, the [=connect=] would be exposed as a global or within a [=binding object=]</p>
A socket can be constructed using a <dfn export id="connect-concept">connect</dfn> 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 <i>pending</i> state (or an error should be thrown). The socket will asynchronously <i>connect</i> depending on the implementation.
<h3 id="binding-object-concept">Binding Object</h3>
<p class="note">A [=binding object=] in this context is essentially just an object that exposes a [=connect=] method conformant with this specification. It is anticipated that a runtime may have any number of such objects. This is an area where there is still active discussion on how this should be defined.</p>
The <dfn export>binding object</dfn> 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:
<ul>
<li>TLS settings</li>
<li>The HTTP proxy to use for the socket connection</li>
</ul>
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.
<pre highlight="js">
const tls_socket = new TLSSocket({ key: '...', cert: '...' });
tls_socket.connect("example.com:1234");
</pre>
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](https://developers.cloudflare.com/workers/configuration/bindings/).
<h2 id="socket-section">Socket</h2>
<h3 id="using-a-socket">Using a socket</h3>
<div class=example>
A basic example of using connect with an echo server.
<pre highlight="js">
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!
</pre>
</div>
<h3 id="socket-class">The {{Socket}} class</h3>
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.
<pre class="idl">
[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();
};
</pre>
The terms {{ReadableStream}} and {{WritableStream}} are defined in [[WHATWG-STREAMS]].
<h3 id="attributes">Attributes</h3>
<h4 id="readable-attribute">readable</h4>
The {{readable}} attribute is a {{ReadableStream}} which receives data from the server the socket is connected to.
<div class="example">
The below example shows typical {{ReadableStream}} usage to read data from a socket:
<pre highlight="js">
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();
</pre>
</div>
<p class="note">The ReadableStream currently is defined to operate in non-byte mode, that is the `type` parameter to the ReadableStream constructor is not set. This means the stream's controller is {{ReadableStreamDefaultController}}. This, however, should be discussed and may be made configurable. It is reasonable, for instance, to assume that sockets used for most TCP cases would be byte-oriented, while sockets used for messages (e.g. UDP) would not.</p>
<h4 id="writable-attribute">writable</h4>
The {{writable}} attribute is a {{WritableStream}} which sends data to the server the socket is connected to.
<div class="example">
The below example shows typical {{WritableStream}} usage to write data to a socket:
<pre highlight="js">
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"));
</pre>
</div>
<h4 id="opened-attribute">opened</h4>
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}}.
<h4 id="closed-attribute">closed</h4>
The {{closed}} attribute is a promise which can be used to keep track of the socket state. It gets resolved under the
following circumstances:
<ul>
<li>the {{close()}} method is called on the socket</li>
<li>the socket was constructed with the `allowHalfOpen` parameter set to `false`, the ReadableStream is being read from, and the remote connection sends a FIN packet (graceful closure) or a RST packet</li>
</ul>
<div class="note">
The current Cloudflare Workers implementation behaves as described above, specifically the
ReadableStream needs to be read until completion for the `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.
</div>
It can also be rejected with a [=SocketError=] when a socket connection could not be established under the following circumstances:
<ul>
<li>The address/port combo requested is blocked </li>
<li>A transient issue with the runtime</li>
</ul>
Cancelling the socket's ReadableStream and closing the socket's WritableStream does not resolve the `closed` promise.
<h3 id="methods">Methods</h3>
<h4 id="close-method">close(optional any reason)</h4>
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}}.
<h4 id="starttls-method">startTls()</h4>
The {{startTls()}} method enables opportunistic TLS (otherwise known as [StartTLS](https://en.wikipedia.org/wiki/Opportunistic_TLS)) 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:
<ul>
<li>the original socket is closed, though the original connection is kept alive</li>
<li>a secure TLS connection is established over that connection</li>
<li>a new socket is created and returned from the `startTls` call</li>
</ul>
<aside class="example">
Here is a simple code example showing usage of the {{startTls()}} method:
<pre highlight="js">
import { connect } from 'sockets';
let sock = connect("google.com:443", { secureTransport: "starttls" });
// ... some code here ...
// We want to StartTLS at this point.
let tlsSock = sock.startTls();
</pre>
</aside>
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:
<ul>
<li>called on an existing TLS socket</li>
<li>the `secureTransport` option defined on the {{Socket}} instance is not equal to `"starttls"`.</li>
</ul>
<h3 id="socket-error">SocketError</h3>
<p class="note">Arguably, this should be a type of {{DOMException}} rather than {{TypeError}}. More discussion is necessary on the form and structure of socket-related errors.</p>
<dfn export>SocketError</dfn> is an instance of {{TypeError}}. The error message should start with `"SocketError: "`.
<div class="example">
An `"connection failed"` SocketError.
<pre highlight="js">
throw new SocketError('connection failed');
</pre>
Should result in the following error: `Uncaught SocketError [TypeError]: SocketError: connection failed`.
</div>
<h2 id="connect-section">connect</h2>
<pre class="idl">
[Exposed=*]
dictionary SocketAddress {
DOMString hostname;
unsigned short port;
};
typedef (DOMString or SocketAddress) AnySocketAddress;
enum SecureTransportKind { "off", "on", "starttls" };
[Exposed=*]
dictionary SocketOptions {
SecureTransportKind secureTransport = "off";
boolean allowHalfOpen = false;
DOMString sni = null;
DOMString[] alpn = [];
};
[Exposed=*]
interface Connect {
Socket connect(AnySocketAddress address, optional SocketOptions opts);
};
</pre>
The {{connect()}} method performs the following steps:
<ol>
<li>New {{Socket}} instance is created with each of its attributes initialised immediately.</li>
<li>The socket's {{opened}} promise is set to [=a new promise=]. Set |opened|.\[[PromiseIsHandled]] to true.
<li>The socket's {{closed}} promise is set to [=a new promise=]. Set |closed|.\[[PromiseIsHandled]] to true.
<li>The created {{Socket}} instance is returned immediately in a <i>pending</i> state.</li>
<li>A connection is established to the specified {{SocketAddress}} asynchronously.</li>
<li>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.</li>
<li>If the connection fails for any reason, set |error| to a new [=SocketError=] and reject the socket's {{closed}} and {{opened}} promises with |error|. Also, the {{readable}} is canceled with |error| and the {{writable}} is aborted with |error|.</li>
<li>The instance's {{ReadableStream}} and {{WritableStream}} streams can be used immediately but may not actually transmit or receive data until the socket is fully opened.</li>
</ol>
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.
<div class="note">
The implementation may consider blocking connections to certain hostname/port combinations which can pose a threat of abuse or security vulnerability.
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).
</div>
<h3 id="socketoptions-dictionary">`SocketOptions` dictionary</h3>
<dl>
<dt>
{{secureTransport}} member
</dt>
<dd>
The secure transport mode to use.
<dl>
<dt>{{off}}</dt>
<dd>A connection is established in plain text.</dd>
<dt>{{on}}</dt>
<dd>A TLS connection is established using default CAs</dd>
<dt>{{starttls}}</dt>
<dd>Initially the same as the `off` option, the connection continues in plain text until the {{startTls()}} method is called</dd>
</dl>
</dd>
<dt>
{{alpn}} member
</dt>
<dd>
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 {{info}} property. May be specified if and only if {{secureTransport}} is `on` or `starttls`.
</dd>
<dt>
{{sni}} member
</dt>
<dd>
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 {{secureTransport}} is `on` or `starttls`.
</dd>
<dt>
{{allowHalfOpen}} member
</dt>
<dd>
This option is similar to that offered by the Node.js `net` module and allows interoperability with code which utilizes it.
<dl>
<dt>false</dt>
<dd>The WritableStream- and the socket instance- will be automatically closed when a
FIN packet is received from the remote connection.</dd>
<dt>true</dt>
<dd>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.</dd>
</dl>
</dd>
</dl>
<h3 id="socketinfo-dictionary">`SocketInfo` dictionary</h3>
<d1>
<dt>
{{remoteAddress}} member
</dt>
<dd>
Provides the hostname/port combo of the remote peer the {{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}}.
</dd>
<dt>
{{localAddress}} member
</dt>
<dd>
Optionally provides the hostname/port combo of the local network endpoint, for example `"localhost:12345"`.
</dd>
<dt>
{{alpn}} property
</dt>
<dd>
If the server agrees with one of the protocols specified in the `alpn` negotiation list, returns that protocol name as a string, otherwise `null`.
</dd>
</d1>
<h3 id="anysocketaddress-type">`AnySocketAddress` type</h3>
<dl>
<dt>
{{SocketAddress}} dictionary
</dt>
<dd>
The address to connect to. For example `{ hostname: "google.com", port: 443 }`.
<dl>
<dt>{{hostname}}</dt>
<dd>A connection is established in plain text.</dd>
<dt>{{port}}</dt>
<dd>A TLS connection is established using default CAs</dd>
</dl>
</dd>
<dt>
{{DOMString}}
</dt>
<dd>
A hostname/port combo separated by a colon. For example `"google.com:443"`.
</dd>
</dl>