diff options
author | James M Snell <jasnell@gmail.com> | 2018-01-01 11:13:29 -0800 |
---|---|---|
committer | James M Snell <jasnell@gmail.com> | 2018-01-03 11:29:43 -0800 |
commit | 060babd66524b6a3e4757bb2fe5b87ad567cdb40 (patch) | |
tree | 22a4d99a5f91fd9b976e8b2e293ecf7420f9a697 | |
parent | b25b1efa0a6801cdf3faea199a36e0e2cc489395 (diff) | |
download | node-new-060babd66524b6a3e4757bb2fe5b87ad567cdb40.tar.gz |
http2: add initial support for originSet
Add new properties to `Http2Session` to identify alpnProtocol,
and indicator about whether the session is TLS or not, and
initial support for origin set (preparinng for `ORIGIN` frame
support and the client-side `Pool` implementation.
The `originSet` is the set of origins for which an `Http2Session`
may be considered authoritative. Per the `ORIGIN` frame spec,
the originSet is only valid on TLS connections, so this is only
exposed when using a `TLSSocket`.
PR-URL: https://github.com/nodejs/node/pull/17935
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
Reviewed-By: Sebastiaan Deckers <sebdeckers83@gmail.com>
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
-rw-r--r-- | doc/api/http2.md | 35 | ||||
-rw-r--r-- | lib/internal/http2/core.js | 58 | ||||
-rw-r--r-- | test/parallel/test-http2-create-client-secure-session.js | 19 | ||||
-rw-r--r-- | test/parallel/test-http2-create-client-session.js | 8 |
4 files changed, 118 insertions, 2 deletions
diff --git a/doc/api/http2.md b/doc/api/http2.md index e9b06a427c..f613072324 100644 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -283,6 +283,18 @@ session.setTimeout(2000); session.on('timeout', () => { /** .. **/ }); ``` +#### http2session.alpnProtocol +<!-- YAML +added: REPLACEME +--> + +* Value: {string|undefined} + +Value will be `undefined` if the `Http2Session` is not yet connected to a +socket, `h2c` if the `Http2Session` is not connected to a `TLSSocket`, or +will return the value of the connected `TLSSocket`'s own `alpnProtocol` +property. + #### http2session.close([callback]) <!-- YAML added: REPLACEME @@ -340,6 +352,18 @@ added: v8.4.0 Will be `true` if this `Http2Session` instance has been destroyed and must no longer be used, otherwise `false`. +#### http2session.encrypted +<!-- YAML +added: REPLACEME +--> + +* Value: {boolean|undefined} + +Value is `undefined` if the `Http2Session` session socket has not yet been +connected, `true` if the `Http2Session` is connected with a `TLSSocket`, +and `false` if the `Http2Session` is connected to any other kind of socket +or stream. + #### http2session.goaway([code, [lastStreamID, [opaqueData]]]) <!-- YAML added: REPLACEME @@ -363,6 +387,17 @@ added: v8.4.0 A prototype-less object describing the current local settings of this `Http2Session`. The local settings are local to *this* `Http2Session` instance. +#### http2session.originSet +<!-- YAML +added: REPLACEME +--> + +* Value: {string[]|undefined} + +If the `Http2Session` is connected to a `TLSSocket`, the `originSet` property +will return an Array of origins for which the `Http2Session` may be +considered authoritative. + #### http2session.pendingSettingsAck <!-- YAML added: v8.4.0 diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index cad5fcfb10..a7f3045c3e 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -70,7 +70,9 @@ const TLSServer = tls.Server; const kInspect = require('internal/util').customInspectSymbol; +const kAlpnProtocol = Symbol('alpnProtocol'); const kAuthority = Symbol('authority'); +const kEncrypted = Symbol('encrypted'); const kHandle = Symbol('handle'); const kID = Symbol('id'); const kInit = Symbol('init'); @@ -731,6 +733,17 @@ function setupHandle(socket, type, options) { this[kHandle] = handle; + if (socket.encrypted) { + this[kAlpnProtocol] = socket.alpnProtocol; + this[kEncrypted] = true; + } else { + // 'h2c' is the protocol identifier for HTTP/2 over plain-text. We use + // it here to identify any session that is not explicitly using an + // encrypted socket. + this[kAlpnProtocol] = 'h2c'; + this[kEncrypted] = false; + } + const settings = typeof options.settings === 'object' ? options.settings : {}; @@ -805,9 +818,12 @@ class Http2Session extends EventEmitter { streams: new Map(), pendingStreams: new Set(), pendingAck: 0, - writeQueueSize: 0 + writeQueueSize: 0, + originSet: undefined }; + this[kEncrypted] = undefined; + this[kAlpnProtocol] = undefined; this[kType] = type; this[kProxySocket] = null; this[kSocket] = socket; @@ -833,6 +849,46 @@ class Http2Session extends EventEmitter { debug(`Http2Session ${sessionName(type)}: created`); } + // Returns undefined if the socket is not yet connected, true if the + // socket is a TLSSocket, and false if it is not. + get encrypted() { + return this[kEncrypted]; + } + + // Returns undefined if the socket is not yet connected, `h2` if the + // socket is a TLSSocket and the alpnProtocol is `h2`, or `h2c` if the + // socket is not a TLSSocket. + get alpnProtocol() { + return this[kAlpnProtocol]; + } + + // TODO(jasnell): originSet is being added in preparation for ORIGIN frame + // support. At the current time, the ORIGIN frame specification is awaiting + // publication as an RFC and is awaiting implementation in nghttp2. Once + // added, an ORIGIN frame will add to the origins included in the origin + // set. 421 responses will remove origins from the set. + get originSet() { + if (!this.encrypted || this.destroyed) + return undefined; + + let originSet = this[kState].originSet; + if (originSet === undefined) { + const socket = this[kSocket]; + this[kState].originSet = originSet = new Set(); + if (socket.servername != null) { + let originString = `https://${socket.servername}`; + if (socket.remotePort != null) + originString += `:${socket.remotePort}`; + // We have to ensure that it is a properly serialized + // ASCII origin string. The socket.servername might not + // be properly ASCII encoded. + originSet.add((new URL(originString)).origin); + } + } + + return Array.from(originSet); + } + // True if the Http2Session is still waiting for the socket to connect get connecting() { return (this[kState].flags & SESSION_FLAGS_READY) === 0; diff --git a/test/parallel/test-http2-create-client-secure-session.js b/test/parallel/test-http2-create-client-secure-session.js index 811ef772d5..6120a58602 100644 --- a/test/parallel/test-http2-create-client-secure-session.js +++ b/test/parallel/test-http2-create-client-secure-session.js @@ -19,6 +19,14 @@ function loadKey(keyname) { function onStream(stream, headers) { const socket = stream.session[kSocket]; + + assert(stream.session.encrypted); + assert(stream.session.alpnProtocol, 'h2'); + const originSet = stream.session.originSet; + assert(Array.isArray(originSet)); + assert.strictEqual(originSet[0], + `https://${socket.servername}:${socket.remotePort}`); + assert(headers[':authority'].startsWith(socket.servername)); stream.respond({ 'content-type': 'application/json' }); stream.end(JSON.stringify({ @@ -39,6 +47,17 @@ function verifySecureSession(key, cert, ca, opts) { assert.strictEqual(client.socket.listenerCount('secureConnect'), 1); const req = client.request(); + client.on('connect', common.mustCall(() => { + assert(client.encrypted); + assert.strictEqual(client.alpnProtocol, 'h2'); + const originSet = client.originSet; + assert(Array.isArray(originSet)); + assert.strictEqual(originSet.length, 1); + assert.strictEqual( + originSet[0], + `https://${opts.servername || 'localhost'}:${server.address().port}`); + })); + req.on('response', common.mustCall((headers) => { assert.strictEqual(headers[':status'], 200); assert.strictEqual(headers['content-type'], 'application/json'); diff --git a/test/parallel/test-http2-create-client-session.js b/test/parallel/test-http2-create-client-session.js index c5e9468f9b..b5be6bc858 100644 --- a/test/parallel/test-http2-create-client-session.js +++ b/test/parallel/test-http2-create-client-session.js @@ -34,10 +34,16 @@ server.listen(0); server.on('listening', common.mustCall(() => { const client = h2.connect(`http://localhost:${server.address().port}`); - client.setMaxListeners(100); + client.setMaxListeners(101); client.on('goaway', console.log); + client.on('connect', common.mustCall(() => { + assert(!client.encrypted); + assert(!client.originSet); + assert.strictEqual(client.alpnProtocol, 'h2c'); + })); + const countdown = new Countdown(count, () => { client.close(); server.close(); |