Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http2: initial support for originSet #17935

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions doc/api/http2.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,18 @@ session.setTimeout(2000);
session.on('timeout', () => { /** .. **/ });
```

#### http2session.alpnProtocol
<!-- YAML
added: REPLACEME
-->

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a type annotation here, like

* {string|undefined}

Ditto below.

* 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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/_tls_wrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,7 @@ TLSSocket.prototype._finishInit = function() {
this.alpnProtocol = this._handle.getALPNNegotiatedProtocol();
}

if (process.features.tls_sni && this._tlsOptions.isServer) {
if (process.features.tls_sni) {
this.servername = this._handle.getServername();
}

Expand Down
58 changes: 57 additions & 1 deletion lib/internal/http2/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -731,6 +733,17 @@ function setupHandle(socket, type, options) {

this[kHandle] = handle;

if (socket.encrypted) {
this[kAlpnProtocol] = socket.alpnProtocol;
this[kEncrypted] = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we define these on the initial class (as undefined) to avoid thrashing of the hidden class? (Maybe originSet too, on kState.)

} 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 : {};

Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
19 changes: 19 additions & 0 deletions test/parallel/test-http2-create-client-secure-session.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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');
Expand Down
8 changes: 7 additions & 1 deletion test/parallel/test-http2-create-client-session.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down