h2lean 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +265 -2
- package/index.js +72 -0
- package/lib/client-session.js +2068 -0
- package/lib/client-stream.js +166 -0
- package/lib/client.js +85 -0
- package/lib/compat.js +849 -0
- package/lib/fast-timer.js +436 -0
- package/lib/frames.js +127 -0
- package/lib/headers.js +463 -0
- package/lib/hpack-data.js +333 -0
- package/lib/hpack.js +1288 -0
- package/lib/pieces.js +134 -0
- package/lib/server.js +272 -0
- package/lib/session.js +2698 -0
- package/lib/stream.js +519 -0
- package/package.json +49 -4
package/lib/pieces.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// The octets of a frame that arrived in pieces.
|
|
2
|
+
//
|
|
3
|
+
// A frame whose bytes do not fall inside one socket read is assembled out of the
|
|
4
|
+
// views it arrived in rather than copied into a buffer of its own: a DATA
|
|
5
|
+
// payload reaches the application as the views it arrived in (node hands over
|
|
6
|
+
// the same shape -- tools/probe-chunking.js), and a header block that a read
|
|
7
|
+
// split is decoded out of the views. This file is the machinery that needs:
|
|
8
|
+
// how long the views are together, how to read an octet or a 32-bit value out
|
|
9
|
+
// of them, how to cut a range out of them, and how to compare them with a
|
|
10
|
+
// buffer.
|
|
11
|
+
//
|
|
12
|
+
// The views are of the chunks node read off the socket, and node never writes
|
|
13
|
+
// into a chunk again, so a view stays valid for as long as anybody holds it.
|
|
14
|
+
// That is the rule the slab this replaced broke: the slab was reused for the
|
|
15
|
+
// next frame that arrived in pieces, so the bytes an application kept moved
|
|
16
|
+
// under it (AGENTS.md 5: issued buffers stay valid).
|
|
17
|
+
//
|
|
18
|
+
// Every function here takes the views and an offset into their concatenation.
|
|
19
|
+
// The offsets are into the *payload* of a frame, so a caller that has trimmed
|
|
20
|
+
// the padding off a frame passes the trimmed offsets.
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
// How many octets the views hold together.
|
|
24
|
+
function piecesLength(pieces) {
|
|
25
|
+
let n = 0;
|
|
26
|
+
for (let i = 0; i < pieces.length; i++) {
|
|
27
|
+
n += pieces[i].length;
|
|
28
|
+
}
|
|
29
|
+
return n;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The octet at `off` in the concatenation of the views, or -1 past the end.
|
|
33
|
+
function pieceByteAt(pieces, off) {
|
|
34
|
+
for (let i = 0; i < pieces.length; i++) {
|
|
35
|
+
const p = pieces[i];
|
|
36
|
+
if (off < p.length) {
|
|
37
|
+
return p[off];
|
|
38
|
+
}
|
|
39
|
+
off -= p.length;
|
|
40
|
+
}
|
|
41
|
+
return -1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// A 32-bit value at `off`, big endian -- the frame header's stream id, the
|
|
45
|
+
// SETTINGS entries' values, the flow-control increments.
|
|
46
|
+
function pieceU32At(pieces, off) {
|
|
47
|
+
return ((pieceByteAt(pieces, off) * 0x1000000)
|
|
48
|
+
+ (pieceByteAt(pieces, off + 1) << 16)
|
|
49
|
+
+ (pieceByteAt(pieces, off + 2) << 8)
|
|
50
|
+
+ pieceByteAt(pieces, off + 3)) >>> 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The views that cover [from, to) of the concatenation. The parts that are not
|
|
54
|
+
// whole views are trimmed, so the result is the range and nothing else.
|
|
55
|
+
function pieceSlice(pieces, from, to) {
|
|
56
|
+
const out = [];
|
|
57
|
+
let off = 0;
|
|
58
|
+
for (let i = 0; i < pieces.length; i++) {
|
|
59
|
+
const p = pieces[i];
|
|
60
|
+
const end = off + p.length;
|
|
61
|
+
if (end > from && off < to) {
|
|
62
|
+
const a = from > off ? from - off : 0;
|
|
63
|
+
const b = to < end ? to - off : p.length;
|
|
64
|
+
out.push(p.subarray(a, b));
|
|
65
|
+
}
|
|
66
|
+
off = end;
|
|
67
|
+
if (off >= to) {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The octets of [from, to) copied into `dst` at `dstOff`. The copy a frame's
|
|
75
|
+
// first octets need when they are spread over several views: the pad length
|
|
76
|
+
// and the priority block of a HEADERS payload are at its front and are at most
|
|
77
|
+
// six octets.
|
|
78
|
+
function pieceCopyTo(dst, dstOff, pieces, from, to) {
|
|
79
|
+
let off = 0;
|
|
80
|
+
let written = 0;
|
|
81
|
+
for (let i = 0; i < pieces.length && written < to - from; i++) {
|
|
82
|
+
const p = pieces[i];
|
|
83
|
+
const end = off + p.length;
|
|
84
|
+
if (end > from && off < to) {
|
|
85
|
+
const a = from > off ? from - off : 0;
|
|
86
|
+
const b = to < end ? to - off : p.length;
|
|
87
|
+
p.copy(dst, dstOff + written, a, b);
|
|
88
|
+
written += b - a;
|
|
89
|
+
}
|
|
90
|
+
off = end;
|
|
91
|
+
}
|
|
92
|
+
return written;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// A fresh buffer with the octets of [from, to). This is the copy an application
|
|
96
|
+
// keeps, and the copy the shape cache stores -- the views themselves are not
|
|
97
|
+
// kept, because a view pins the socket chunk it came from.
|
|
98
|
+
function pieceConcat(pieces, from, to) {
|
|
99
|
+
const parts = pieceSlice(pieces, from, to);
|
|
100
|
+
if (parts.length === 1) {
|
|
101
|
+
return Buffer.from(parts[0]);
|
|
102
|
+
}
|
|
103
|
+
return Buffer.concat(parts, to - from);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Are the octets of [from, to) the same as the buffer's? The shape cache's
|
|
107
|
+
// answer to "was this exact block seen before".
|
|
108
|
+
function pieceEquals(buf, pieces, from, to) {
|
|
109
|
+
if (buf.length !== to - from) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
let off = 0;
|
|
113
|
+
for (let i = 0; i < pieces.length; i++) {
|
|
114
|
+
const p = pieces[i];
|
|
115
|
+
const end = off + p.length;
|
|
116
|
+
if (end > from && off < to) {
|
|
117
|
+
const a = from > off ? from - off : 0;
|
|
118
|
+
const b = to < end ? to - off : p.length;
|
|
119
|
+
if (buf.compare(p, a, b, off - from + a, off - from + b) !== 0) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
off = end;
|
|
124
|
+
if (off >= to) {
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = {
|
|
132
|
+
piecesLength, pieceByteAt, pieceU32At, pieceSlice, pieceCopyTo, pieceConcat,
|
|
133
|
+
pieceEquals
|
|
134
|
+
};
|
package/lib/server.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// Server front door, both transports: h2c on a net.Server and h2s on a tls.Server.
|
|
2
|
+
// This file owns the listening socket, one Http2Session per connection, and the
|
|
3
|
+
// decision of whether the compat (req, res) layer is instantiated at all.
|
|
4
|
+
//
|
|
5
|
+
// Connection-lifetime surface, measured against node 26 rather than against its
|
|
6
|
+
// documentation:
|
|
7
|
+
// * node's Http2Server has close, setTimeout and updateSettings -- and NOTHING else: no
|
|
8
|
+
// sessionTimeout, no closeIdleSessions, no sessions getter, no settings property. node keeps
|
|
9
|
+
// the settings updateSettings merges in a private options object; h2lean keeps them in the
|
|
10
|
+
// public `settings` field it also builds sessions from, which is a third name node does not
|
|
11
|
+
// have. The extras implemented here are additive, not substitutions:
|
|
12
|
+
// server.sessions -- the live session set, kept because h2lean tracks sessions
|
|
13
|
+
// in it and node keeps the same set under a symbol;
|
|
14
|
+
// server.closeIdleSessions() -- documented by node for years and gone in 26; the
|
|
15
|
+
// keep-alive policy it served is session.setTimeout() plus
|
|
16
|
+
// the application destroying the session, which both stacks
|
|
17
|
+
// do implement. Harmless to keep, surprising to drop.
|
|
18
|
+
// * what this server advertises on the wire matches node: the first SETTINGS frame carries only
|
|
19
|
+
// the values the application asked for -- an EMPTY payload when it asked for nothing -- and a
|
|
20
|
+
// later server.updateSettings() reaches the NEXT connection, not the live sessions. The local
|
|
21
|
+
// defaults still guard the inbound path; they are simply not advertised, because an advertised
|
|
22
|
+
// value is a promise to the peer rather than a local choice. Measured with
|
|
23
|
+
// bench/micro/probe-settings-frame.js: both stacks send an empty frame, then "3=100,5=32768" for
|
|
24
|
+
// settings {maxConcurrentStreams: 100, maxFrameSize: 32768}, then "3=100,4=65536" for the
|
|
25
|
+
// connection that follows server.updateSettings({initialWindowSize: 65536}).
|
|
26
|
+
// * closing a connection is not the same MOMENT on the two stacks, and one event depends on it.
|
|
27
|
+
// node's session survives close() until its graceful close completes (internal/http2/core.js:
|
|
28
|
+
// handle.ongracefulclosecomplete -> kMaybeDestroy), so it can still read -- and report -- the
|
|
29
|
+
// peer's answer to the GOAWAY we just sent. h2lean destroys the session as soon as that GOAWAY
|
|
30
|
+
// is written (lib/session.js: close() -> _maybeFinishClose), but its socket stays readable
|
|
31
|
+
// until the write is flushed, so the answer is read there too. The two therefore disagree
|
|
32
|
+
// about ONE event -- the 'goaway' the peer's answer produces -- and only inside a window:
|
|
33
|
+
// measured with tools/probe-goaway-echo.js, node's outcome moves with the load (40 of 40
|
|
34
|
+
// rounds on an idle machine, 26 of 40 and 29 of 30 in two loaded runs) while h2lean reports
|
|
35
|
+
// the echo in every round of every run. test/event-order.test.js drops the echo outside its
|
|
36
|
+
// 'goaway' scenario for that reason.
|
|
37
|
+
'use strict';
|
|
38
|
+
|
|
39
|
+
const net = require('node:net');
|
|
40
|
+
const tls = require('node:tls');
|
|
41
|
+
const { Http2Session, OUR_DEFAULTS } = require('./session');
|
|
42
|
+
const { onServerStream, Http2ServerRequest, Http2ServerResponse } = require('./compat');
|
|
43
|
+
|
|
44
|
+
const kCompat = Symbol('h2lean.compatListener');
|
|
45
|
+
// The transport's own close/on, captured before the shared API overrides them:
|
|
46
|
+
// net.Server and tls.Server are unrelated base classes and JS has only single
|
|
47
|
+
// inheritance, so the shared behaviour is mixed onto the prototype the same way
|
|
48
|
+
// node's own core does it, with the overridden methods kept reachable by symbol.
|
|
49
|
+
const kTransportClose = Symbol('h2lean.transportClose');
|
|
50
|
+
const kTransportOn = Symbol('h2lean.transportOn');
|
|
51
|
+
|
|
52
|
+
function notImplemented(what) {
|
|
53
|
+
const err = new Error(`h2lean v1 does not implement ${what}`);
|
|
54
|
+
err.code = 'ERR_H2LEAN_NOT_IMPLEMENTED';
|
|
55
|
+
return err;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function initHttp2Server(server, options) {
|
|
59
|
+
const opts = options || {};
|
|
60
|
+
server.settings = { ...OUR_DEFAULTS, ...opts.settings };
|
|
61
|
+
server._sessionOptions = {
|
|
62
|
+
settings: server.settings,
|
|
63
|
+
// Which values the application asked for. server.settings is what the session's guards read,
|
|
64
|
+
// this list is what the peer is told, and the two are deliberately not the same object: node
|
|
65
|
+
// advertises only what was set (created with no options, its SETTINGS frame is empty), and an
|
|
66
|
+
// advertised number decides how hard peers drive the connection. See Http2Session._advertised.
|
|
67
|
+
advertised: Object.keys(opts.settings || {}),
|
|
68
|
+
// There is no copy to opt out of: a DATA payload reaches the application
|
|
69
|
+
// either as a view of the buffer node read off the socket or as a buffer
|
|
70
|
+
// built for that frame alone, and both stay readable for as long as the
|
|
71
|
+
// application holds them.
|
|
72
|
+
shapeCache: opts.shapeCache !== false,
|
|
73
|
+
// Opt-in: req.headers is frozen and one object per distinct header block.
|
|
74
|
+
// Off by default, because node lets the application write into headers.
|
|
75
|
+
shareHeaders: opts.shareHeaders === true
|
|
76
|
+
};
|
|
77
|
+
server._sessions = new Set();
|
|
78
|
+
server[kCompat] = null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Turns an accepted socket -- plain or already decrypted -- into a session.
|
|
82
|
+
function attachSession(server, socket) {
|
|
83
|
+
socket.setNoDelay(true);
|
|
84
|
+
const session = new Http2Session(socket, server._sessionOptions);
|
|
85
|
+
session.server = server;
|
|
86
|
+
server._sessions.add(session);
|
|
87
|
+
session.on('stream', (stream, headers, options, rawHeaders) =>
|
|
88
|
+
server.emit('stream', stream, headers, options, rawHeaders));
|
|
89
|
+
session.on('error', (err) => {
|
|
90
|
+
if (server.listenerCount('sessionError') > 0) {
|
|
91
|
+
server.emit('sessionError', err);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
session.once('close', () => {
|
|
95
|
+
server._sessions.delete(session);
|
|
96
|
+
});
|
|
97
|
+
server.emit('session', session);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function applyHttp2ServerApi(proto) {
|
|
101
|
+
proto[kTransportClose] = proto.close;
|
|
102
|
+
proto[kTransportOn] = proto.on;
|
|
103
|
+
|
|
104
|
+
// node instantiates the compat req/res only when the application asks for
|
|
105
|
+
// them: a plain createServer() with a 'stream' handler never pays for two
|
|
106
|
+
// objects per request. Adding the first 'request' listener arms it.
|
|
107
|
+
proto._setupCompat = function () {
|
|
108
|
+
if (this[kCompat] !== null) {
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
this[kCompat] = onServerStream.bind(this);
|
|
112
|
+
this._sessionOptions.compat = true;
|
|
113
|
+
for (const session of this._sessions) {
|
|
114
|
+
session._wantRaw = true;
|
|
115
|
+
}
|
|
116
|
+
return proto[kTransportOn].call(this, 'stream', this[kCompat]);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
proto.on = function (event, listener) {
|
|
120
|
+
if (event === 'request') {
|
|
121
|
+
this._setupCompat();
|
|
122
|
+
}
|
|
123
|
+
return proto[kTransportOn].call(this, event, listener);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
proto.addListener = function (event, listener) {
|
|
127
|
+
return this.on(event, listener);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
proto.close = function (cb) {
|
|
131
|
+
// node's Http2Server.close() is `ReflectApply(NETServer.prototype.close, this, arguments)`
|
|
132
|
+
// followed by closeAllSessions(this) (internal/http2/core): the listening socket goes away
|
|
133
|
+
// right away and its callback is net.Server's own. Waiting for the session set to empty first
|
|
134
|
+
// was ours, and it made close(cb) a promise that a session which cannot drain never keeps --
|
|
135
|
+
// 'close' then depended on every connection finishing.
|
|
136
|
+
proto[kTransportClose].call(this, cb);
|
|
137
|
+
for (const session of [...this._sessions]) {
|
|
138
|
+
session.close();
|
|
139
|
+
}
|
|
140
|
+
return this;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
proto.closeIdleSessions = function () {
|
|
144
|
+
for (const session of this._sessions) {
|
|
145
|
+
if (session._liveStreams === 0) {
|
|
146
|
+
session.close();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return this;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
Object.defineProperty(proto, 'sessions', {
|
|
153
|
+
get() {
|
|
154
|
+
return this._sessions;
|
|
155
|
+
},
|
|
156
|
+
configurable: true,
|
|
157
|
+
enumerable: false
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// node's own Http2Server.setTimeout (internal/http2/core.js:3541) only records the value --
|
|
161
|
+
// `this.timeout = msecs`, the same field net.Server uses -- and attaches the callback. It
|
|
162
|
+
// does NOT walk the sessions. Measured against node with an idle connection and
|
|
163
|
+
// server.setTimeout(300): no 'timeout' on the session, no close, the connection simply
|
|
164
|
+
// stays. Arming a session idle timeout for every live session (what this used to do) both
|
|
165
|
+
// invented an event node does not emit and applied to connections that already existed,
|
|
166
|
+
// silently skipping the ones accepted later. The per-connection idle policy is
|
|
167
|
+
// session.setTimeout(), which is implemented on the session.
|
|
168
|
+
proto.setTimeout = function (msecs, cb) {
|
|
169
|
+
this.timeout = msecs;
|
|
170
|
+
if (cb !== undefined) {
|
|
171
|
+
if (typeof cb !== 'function') {
|
|
172
|
+
throw new TypeError('cb must be a function');
|
|
173
|
+
}
|
|
174
|
+
this.on('timeout', cb);
|
|
175
|
+
}
|
|
176
|
+
return this;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// node's Http2Server.updateSettings() (internal/http2/core.js, next to its setTimeout) only
|
|
180
|
+
// merges into the server's own settings object -- it does not walk the live sessions. That
|
|
181
|
+
// is exactly what the wire shows: after updateSettings({maxConcurrentStreams: 7}) the next
|
|
182
|
+
// connection is sent SETTINGS(3)=7 while an established session keeps the values it was
|
|
183
|
+
// opened with. `_sessionOptions.settings` is the object sessions are built from, so pointing
|
|
184
|
+
// it at the merged copy gives the same reach: new connections, no effect on old ones.
|
|
185
|
+
proto.updateSettings = function (settings) {
|
|
186
|
+
if (settings === null || typeof settings !== 'object') {
|
|
187
|
+
throw new TypeError('settings must be an object');
|
|
188
|
+
}
|
|
189
|
+
this.settings = { ...this.settings, ...settings };
|
|
190
|
+
this._sessionOptions.settings = this.settings;
|
|
191
|
+
// the values merged here become promises to the next peer, so they join the advertisement;
|
|
192
|
+
// first-seen order is kept, and it is the order the SETTINGS entries go out in
|
|
193
|
+
this._sessionOptions.advertised = [...new Set(
|
|
194
|
+
[...this._sessionOptions.advertised, ...Object.keys(settings)])];
|
|
195
|
+
return this;
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
class Http2Server extends net.Server {
|
|
200
|
+
constructor(options) {
|
|
201
|
+
super();
|
|
202
|
+
initHttp2Server(this, options);
|
|
203
|
+
this.on('connection', (socket) => attachSession(this, socket));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// node's h2s server insists on h2 and overwrites whatever ALPNProtocols the
|
|
208
|
+
// application passed: a peer offering only http/1.1 is refused by the handshake
|
|
209
|
+
// itself, with the TLS alert no_application_protocol, rather than after it. The
|
|
210
|
+
// list is therefore fixed here too, and the ALPN check in secureConnection below
|
|
211
|
+
// stays as the guard for contexts installed later through setSecureContext().
|
|
212
|
+
function alpnOptions(options) {
|
|
213
|
+
const opts = { ...(options || {}) };
|
|
214
|
+
opts.ALPNProtocols = ['h2'];
|
|
215
|
+
return opts;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
class Http2SecureServer extends tls.Server {
|
|
219
|
+
constructor(options) {
|
|
220
|
+
super(alpnOptions(options));
|
|
221
|
+
initHttp2Server(this, options);
|
|
222
|
+
this.on('secureConnection', (socket) => {
|
|
223
|
+
// node's rule: 'h2' negotiated, or no ALPN at all -- which is h2 with prior
|
|
224
|
+
// knowledge carried over a TLS connection. Anything else is dropped without
|
|
225
|
+
// ever becoming a session.
|
|
226
|
+
const alpn = socket.alpnProtocol;
|
|
227
|
+
if (alpn === 'h2' || alpn === false) {
|
|
228
|
+
attachSession(this, socket);
|
|
229
|
+
} else {
|
|
230
|
+
socket.destroy();
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
applyHttp2ServerApi(Http2Server.prototype);
|
|
237
|
+
applyHttp2ServerApi(Http2SecureServer.prototype);
|
|
238
|
+
|
|
239
|
+
function createServer(options, onRequestHandler) {
|
|
240
|
+
let opts = options;
|
|
241
|
+
let handler = onRequestHandler;
|
|
242
|
+
if (typeof opts === 'function') {
|
|
243
|
+
handler = opts; opts = undefined;
|
|
244
|
+
}
|
|
245
|
+
const server = new Http2Server(opts);
|
|
246
|
+
if (typeof handler === 'function') {
|
|
247
|
+
server._setupCompat();
|
|
248
|
+
server.on('request', handler);
|
|
249
|
+
}
|
|
250
|
+
return server;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// node's createSecureServer takes the tls.Server options (key, cert, ca, ...) and
|
|
254
|
+
// hands the rest to the same machinery as the cleartext server.
|
|
255
|
+
function createSecureServer(options, onRequestHandler) {
|
|
256
|
+
let opts = options;
|
|
257
|
+
let handler = onRequestHandler;
|
|
258
|
+
if (typeof opts === 'function') {
|
|
259
|
+
handler = opts; opts = undefined;
|
|
260
|
+
}
|
|
261
|
+
const server = new Http2SecureServer(opts);
|
|
262
|
+
if (typeof handler === 'function') {
|
|
263
|
+
server._setupCompat();
|
|
264
|
+
server.on('request', handler);
|
|
265
|
+
}
|
|
266
|
+
return server;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
module.exports = {
|
|
270
|
+
Http2Server, Http2SecureServer, createServer, createSecureServer, notImplemented,
|
|
271
|
+
Http2ServerRequest, Http2ServerResponse
|
|
272
|
+
};
|