├── .gitattributes
├── .github
└── workflows
│ └── deploy.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── index.bs
└── w3c.json
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 | *.bs diff=html linguist-language=HTML
3 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Deploy
2 | on: [push]
3 |
4 | jobs:
5 | deploy:
6 | name: Deploy
7 | runs-on: ubuntu-latest
8 | permissions:
9 | id-token: write
10 | contents: read
11 |
12 | steps:
13 | - name: Clone repository
14 | uses: actions/checkout@v2
15 |
16 | - name: Build spec
17 | run: |
18 | mkdir -p out
19 | curl --retry 2 --fail https://api.csswg.org/bikeshed/ --output out/index.html --header "Accept: text/plain, text/html" -F die-on=fatal -F file=@"index.bs"
20 | - name: Upload to Deno Deploy
21 | uses: denoland/deployctl@v1
22 | with:
23 | project: "proposal-sockets-api"
24 | entrypoint: "https://deno.land/std@0.134.0/http/file_server.ts"
25 | root: out/
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | index.html
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | All documentation, code and communication under this repository are covered by
4 | the
5 | [W3C Code of Ethics and Professional Conduct](https://www.w3.org/Consortium/cepc/).
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Sockets API proposal
2 |
3 | ## Building the spec
4 |
5 | To build the spec locally first install bikeshed:
6 |
7 | ```sh
8 | pip3 install bikeshed && bikeshed update
9 | ```
10 |
11 | Then to build the spec (index.bs) into HTML (index.html), run one of the below
12 | commands:
13 |
14 | ```sh
15 | bikeshed spec # build once
16 |
17 | # or
18 |
19 | bikeshed watch # rebuild on changes
20 | ```
21 |
22 | ## IPR policy
23 |
24 | This repository is being used for work in the W3C Web-Interoperable Runtimes
25 | Community Group, governed by the
26 | [W3C Community License Agreement (CLA)](http://www.w3.org/community/about/agreements/cla/).
27 | To make substantive contributions, you must join the CG.
28 |
29 | If you are not the sole contributor to a contribution (pull request), please
30 | identify all contributors in the pull request comment.
31 |
32 | To add a contributor (other than yourself, that's automatic), mark them one per
33 | line as follows:
34 |
35 | ```
36 | +@github_username
37 | ```
38 |
39 | If you added a contributor by mistake, you can remove them in a comment with:
40 |
41 | ```
42 | -@github_username
43 | ```
44 |
45 | If you are making a pull request on behalf of someone else but you had no part
46 | in designing the feature, you can remove yourself with the above syntax.
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | All Reports in this Repository are licensed by Contributors under the
2 | [W3C Software and Document License](http://www.w3.org/Consortium/Legal/2015/copyright-software-and-document).
3 |
4 | Contributions to Specifications are made under the
5 | [W3C CLA](https://www.w3.org/community/about/agreements/cla/).
6 |
7 | Contributions to Test Suites are made under the
8 | [W3C 3-clause BSD License](https://www.w3.org/Consortium/Legal/2008/03-bsd-license.html)
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # sockets-api
2 |
3 | This is a proposal for an API for establishing TCP connections in Non-Browser JavaScript runtime
4 | environments.
5 |
6 | To view the rendered document, visit https://sockets-api.proposal.wintercg.org/.
7 |
8 | ### Implementations
9 |
10 | - Node.js - https://github.com/Ethan-Arrowood/socket
11 | - Cloudflare Workers - https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/
12 |
--------------------------------------------------------------------------------
/index.bs:
--------------------------------------------------------------------------------
1 |
26 |
27 | This section is non-normative.
28 |
29 | This document defines an API for establishing TCP connections in Non-Browser JavaScript runtime
30 | environments. Existing standard APIs are reused as much as possible, for example {{ReadableStream}}
31 | and {{WritableStream}} are used for reading and writing from a [=socket=]. Some options are inspired
32 | by the existing Node.js `net.Socket` API.
33 |
34 |
35 |
36 |
Concepts
37 |
38 |
Socket
39 |
40 | 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.
41 |
42 | 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.
43 |
44 |
Connect
45 |
46 |
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=]
47 |
48 | 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=].
49 |
50 | 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.
51 |
52 |
Binding Object
53 |
54 |
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.
55 |
56 | The binding object defines extra socket `connect` options. The options it contains can modify the
57 | behaviour of the `connect` invoked on it. Some of the options it can define:
58 |
59 |
60 |
TLS settings
61 |
The HTTP proxy to use for the socket connection
62 |
63 |
64 | 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.
65 |
66 |
70 |
71 | 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/).
72 |
73 |
Socket
74 |
75 |
Using a socket
76 |
77 |
78 | A basic example of using connect with an echo server.
79 |
93 |
94 | 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.
95 |
96 |
119 |
120 | The terms {{ReadableStream}} and {{WritableStream}} are defined in [[WHATWG-STREAMS]].
121 |
122 |
Attributes
123 |
124 |
readable
125 |
126 | The {{readable}} attribute is a {{ReadableStream}} which receives data from the server the socket is connected to.
127 |
128 |
129 | The below example shows typical {{ReadableStream}} usage to read data from a socket:
130 |
131 |
132 | import { connect } from 'sockets';
133 | const socket = connect("google.com:80");
134 |
135 | const reader = socket.readable.getReader();
136 |
137 | while (true) {
138 | const { value, done } = await reader.read();
139 | if (done) {
140 | // the ReadableStream has been closed or cancelled
141 | break;
142 | }
143 | // In many protocols the \`value\` needs to be decoded to be used:
144 | const decoder = new TextDecoder();
145 | console.log(decoder.decode(value));
146 | }
147 |
148 | reader.releaseLock();
149 |
150 |
151 |
152 |
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.
153 |
154 |
writable
155 |
156 | The {{writable}} attribute is a {{WritableStream}} which sends data to the server the socket is connected to.
157 |
158 |
159 | The below example shows typical {{WritableStream}} usage to write data to a socket:
160 |
161 |
172 |
173 | The {{opened}} attribute is a promise that is resolved when the socket connection has been
174 | successfully established, or is rejected if the connection fails. For sockets which use secure-transport,
175 | the resolution of the {{opened}} promise indicates the completion of the secure handshake.
176 |
177 | The {{opened}} promise resolves a {{SocketInfo}} dictionary that optionally provides details
178 | about the connection that has been established.
179 |
180 | By default, the {{opened}} promise is {{marked as handled}}.
181 |
182 |
closed
183 |
184 | The {{closed}} attribute is a promise which can be used to keep track of the socket state. It gets resolved under the
185 | following circumstances:
186 |
187 |
188 |
the {{close()}} method is called on the socket
189 |
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
190 |
191 |
192 |
193 | The current Cloudflare Workers implementation behaves as described above, specifically the
194 | ReadableStream needs to be read until completion for the `closed` promise to resolve, if the
195 | ReadableStream is not read then even if the server closes the connection the `closed` promise
196 | will not resolve.
197 |
198 | Whether the promise should resolve without the ReadableStream being read is up for discussion.
199 |
200 |
201 | It can also be rejected with a [=SocketError=] when a socket connection could not be established under the following circumstances:
202 |
203 |
204 |
The address/port combo requested is blocked
205 |
A transient issue with the runtime
206 |
207 |
208 | Cancelling the socket's ReadableStream and closing the socket's WritableStream does not resolve the `closed` promise.
209 |
210 |
upgraded
211 |
212 | The {{upgraded}} attribute is a boolean flag that indicates whether the socket has been upgraded to a secure connection (using `startTLS()`).
213 |
214 |
Methods
215 |
216 |
close(optional any reason)
217 |
218 | The {{close()}} method closes the socket and its underlying connection. It returns the same promise as the {{closed}} attribute.
219 |
220 | When called, the {{ReadableStream}} and {{WritableStream}} associated with the {{Socket}} will
221 | be canceled and aborted, respectively. If the {{reason}} argument is specified, the {{reason}}
222 | will be passed on to both the {{ReadableStream}} and {{WritableStream}}.
223 |
224 | If the {{opened}} promise is still pending, it will be rejected with the {{reason}}.
225 |
226 |
startTls()
227 |
228 | 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).
229 |
230 | 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:
231 |
232 |
233 |
the original socket "upgraded" attribute is set to true
234 |
the original socket is closed, though the original connection is kept alive
235 |
a secure TLS connection is established over that connection
236 |
a new socket is created and returned from the `startTls` call
237 |
238 |
239 |
250 |
251 | The original readers and writers based off the original socket will no longer work. You must create
252 | new readers and writers from the new socket returned by `startTls`.
253 |
254 | The method must fail with an [=SocketError=] if:
255 |
256 |
257 |
called on an existing TLS socket
258 |
the `secureTransport` option defined on the {{Socket}} instance is not equal to `"starttls"`.
259 |
260 |
261 |
SocketError
262 |
263 |
Arguably, this should be a type of {{DOMException}} rather than {{TypeError}}. More discussion is necessary on the form and structure of socket-related errors.
264 |
265 | SocketError is an instance of {{TypeError}}. The error message should start with `"SocketError: "`.
266 |
267 |
268 | An `"connection failed"` SocketError.
269 |
270 | throw new SocketError('connection failed');
271 |
272 | Should result in the following error: `Uncaught SocketError [TypeError]: SocketError: connection failed`.
273 |
301 |
302 | The {{connect()}} method performs the following steps:
303 |
304 |
305 |
New {{Socket}} instance is created with each of its attributes initialised immediately.
306 |
The socket's {{opened}} promise is set to [=a new promise=]. Set |opened|.\[[PromiseIsHandled]] to true.
307 |
The socket's {{closed}} promise is set to [=a new promise=]. Set |closed|.\[[PromiseIsHandled]] to true.
308 |
The created {{Socket}} instance is returned immediately in a pending state.
309 |
A connection is established to the specified {{SocketAddress}} asynchronously.
310 |
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.
311 |
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|.
312 |
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.
313 |
314 |
315 | 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.
316 |
317 |
318 | The implementation may consider blocking connections to certain hostname/port combinations which can pose a threat of abuse or security vulnerability.
319 |
320 | 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).
321 |
322 |
323 |
`SocketOptions` dictionary
324 |
325 |
326 |
327 | {{secureTransport}} member
328 |
329 |
330 | The secure transport mode to use.
331 |
332 |
{{off}}
333 |
A connection is established in plain text.
334 |
{{on}}
335 |
A TLS connection is established using default CAs
336 |
{{starttls}}
337 |
Initially the same as the `off` option, the connection continues in plain text until the {{startTls()}} method is called
338 |
339 |
340 |
341 | {{alpn}} member
342 |
343 |
344 | 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`.
345 |
346 |
347 | {{sni}} member
348 |
349 |
350 | 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`.
351 |
352 |
353 | {{allowHalfOpen}} member
354 |
355 |
356 | This option is similar to that offered by the Node.js `net` module and allows interoperability with code which utilizes it.
357 |
358 |
false
359 |
The WritableStream- and the socket instance- will be automatically closed when a
360 | FIN packet is received from the remote connection.
361 |
true
362 |
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.
363 |
364 |
365 |
366 |
367 |
`SocketInfo` dictionary
368 |
369 |
370 |
371 | {{remoteAddress}} member
372 |
373 |
374 | Provides the hostname/port combo of the remote peer the {{Socket}} is connected to, for example `"example.com:443"`.
375 | This value may or may not be the same as the address provided to the {{connect()}} method used to create the {{Socket}}.
376 |
377 |
378 | {{localAddress}} member
379 |
380 |
381 | Optionally provides the hostname/port combo of the local network endpoint, for example `"localhost:12345"`.
382 |
383 |
384 | {{alpn}} property
385 |
386 |
387 | If the server agrees with one of the protocols specified in the `alpn` negotiation list, returns that protocol name as a string, otherwise `null`.
388 |
389 |
390 |
391 |
`AnySocketAddress` type
392 |
393 |
394 |
395 | {{SocketAddress}} dictionary
396 |
397 |
398 | The address to connect to. For example `{ hostname: "google.com", port: 443 }`.
399 |
400 |
{{hostname}}
401 |
A connection is established in plain text.
402 |
{{port}}
403 |
A TLS connection is established using default CAs
404 |
405 |
406 |
407 | {{DOMString}}
408 |
409 |
410 | A hostname/port combo separated by a colon. For example `"google.com:443"`.
411 |