Skip to content

Commit 79d3198

Browse files
jasnellMylesBorins
authored andcommitted
http2: add aligned padding strategy
Add a padding strategy option that makes a best attempt to ensure that total frame length for DATA and HEADERS frames are aligned on multiples of 8-bytes. Backport-PR-URL: #18050 PR-URL: #17938 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent 2b6a5d9 commit 79d3198

File tree

4 files changed

+129
-5
lines changed

4 files changed

+129
-5
lines changed

doc/api/http2.md

+21
Original file line numberDiff line numberDiff line change
@@ -1652,6 +1652,13 @@ changes:
16521652
* `http2.constants.PADDING_STRATEGY_CALLBACK` - Specifies that the user
16531653
provided `options.selectPadding` callback is to be used to determine the
16541654
amount of padding.
1655+
* `http2.constants.PADDING_STRATEGY_ALIGNED` - Will *attempt* to apply
1656+
enough padding to ensure that the total frame length, including the
1657+
9-byte header, is a multiple of 8. For each frame, however, there is a
1658+
maxmimum allowed number of padding bytes that is determined by current
1659+
flow control state and settings. If this maximum is less than the
1660+
calculated amount needed to ensure alignment, the maximum will be used
1661+
and the total frame length will *not* necessarily be aligned at 8 bytes.
16551662
* `peerMaxConcurrentStreams` {number} Sets the maximum number of concurrent
16561663
streams for the remote peer as if a SETTINGS frame had been received. Will
16571664
be overridden if the remote peer sets its own value for.
@@ -1723,6 +1730,13 @@ changes:
17231730
* `http2.constants.PADDING_STRATEGY_CALLBACK` - Specifies that the user
17241731
provided `options.selectPadding` callback is to be used to determine the
17251732
amount of padding.
1733+
* `http2.constants.PADDING_STRATEGY_ALIGNED` - Will *attempt* to apply
1734+
enough padding to ensure that the total frame length, including the
1735+
9-byte header, is a multiple of 8. For each frame, however, there is a
1736+
maxmimum allowed number of padding bytes that is determined by current
1737+
flow control state and settings. If this maximum is less than the
1738+
calculated amount needed to ensure alignment, the maximum will be used
1739+
and the total frame length will *not* necessarily be aligned at 8 bytes.
17261740
* `peerMaxConcurrentStreams` {number} Sets the maximum number of concurrent
17271741
streams for the remote peer as if a SETTINGS frame had been received. Will
17281742
be overridden if the remote peer sets its own value for
@@ -1803,6 +1817,13 @@ changes:
18031817
* `http2.constants.PADDING_STRATEGY_CALLBACK` - Specifies that the user
18041818
provided `options.selectPadding` callback is to be used to determine the
18051819
amount of padding.
1820+
* `http2.constants.PADDING_STRATEGY_ALIGNED` - Will *attempt* to apply
1821+
enough padding to ensure that the total frame length, including the
1822+
9-byte header, is a multiple of 8. For each frame, however, there is a
1823+
maxmimum allowed number of padding bytes that is determined by current
1824+
flow control state and settings. If this maximum is less than the
1825+
calculated amount needed to ensure alignment, the maximum will be used
1826+
and the total frame length will *not* necessarily be aligned at 8 bytes.
18061827
* `peerMaxConcurrentStreams` {number} Sets the maximum number of concurrent
18071828
streams for the remote peer as if a SETTINGS frame had been received. Will
18081829
be overridden if the remote peer sets its own value for

src/node_http2.cc

+36-5
Original file line numberDiff line numberDiff line change
@@ -492,8 +492,7 @@ Http2Session::Http2Session(Environment* env,
492492
padding_strategy_ = opts.GetPaddingStrategy();
493493

494494
bool hasGetPaddingCallback =
495-
padding_strategy_ == PADDING_STRATEGY_MAX ||
496-
padding_strategy_ == PADDING_STRATEGY_CALLBACK;
495+
padding_strategy_ != PADDING_STRATEGY_NONE;
497496

498497
nghttp2_session_callbacks* callbacks
499498
= callback_struct_saved[hasGetPaddingCallback ? 1 : 0].callbacks;
@@ -681,6 +680,25 @@ inline void Http2Session::RemoveStream(int32_t id) {
681680
streams_.erase(id);
682681
}
683682

683+
// Used as one of the Padding Strategy functions. Will attempt to ensure
684+
// that the total frame size, including header bytes, are 8-byte aligned.
685+
// If maxPayloadLen is smaller than the number of bytes necessary to align,
686+
// will return maxPayloadLen instead.
687+
inline ssize_t Http2Session::OnDWordAlignedPadding(size_t frameLen,
688+
size_t maxPayloadLen) {
689+
size_t r = (frameLen + 9) % 8;
690+
if (r == 0) return frameLen; // If already a multiple of 8, return.
691+
692+
size_t pad = frameLen + (8 - r);
693+
694+
// If maxPayloadLen happens to be less than the calculated pad length,
695+
// use the max instead, even tho this means the frame will not be
696+
// aligned.
697+
pad = std::min(maxPayloadLen, pad);
698+
DEBUG_HTTP2SESSION2(this, "using frame size padding: %d", pad);
699+
return pad;
700+
}
701+
684702
// Used as one of the Padding Strategy functions. Uses the maximum amount
685703
// of padding allowed for the current frame.
686704
inline ssize_t Http2Session::OnMaxFrameSizePadding(size_t frameLen,
@@ -987,9 +1005,21 @@ inline ssize_t Http2Session::OnSelectPadding(nghttp2_session* handle,
9871005
Http2Session* session = static_cast<Http2Session*>(user_data);
9881006
ssize_t padding = frame->hd.length;
9891007

990-
return session->padding_strategy_ == PADDING_STRATEGY_MAX
991-
? session->OnMaxFrameSizePadding(padding, maxPayloadLen)
992-
: session->OnCallbackPadding(padding, maxPayloadLen);
1008+
switch (session->padding_strategy_) {
1009+
case PADDING_STRATEGY_NONE:
1010+
// Fall-through
1011+
break;
1012+
case PADDING_STRATEGY_MAX:
1013+
padding = session->OnMaxFrameSizePadding(padding, maxPayloadLen);
1014+
break;
1015+
case PADDING_STRATEGY_ALIGNED:
1016+
padding = session->OnDWordAlignedPadding(padding, maxPayloadLen);
1017+
break;
1018+
case PADDING_STRATEGY_CALLBACK:
1019+
padding = session->OnCallbackPadding(padding, maxPayloadLen);
1020+
break;
1021+
}
1022+
return padding;
9931023
}
9941024

9951025
#define BAD_PEER_MESSAGE "Remote peer returned unexpected data while we " \
@@ -2872,6 +2902,7 @@ void Initialize(Local<Object> target,
28722902
NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE);
28732903

28742904
NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_NONE);
2905+
NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_ALIGNED);
28752906
NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_MAX);
28762907
NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_CALLBACK);
28772908

src/node_http2.h

+4
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,8 @@ HTTP_STATUS_CODES(V)
359359
enum padding_strategy_type {
360360
// No padding strategy. This is the default.
361361
PADDING_STRATEGY_NONE,
362+
// Attempts to ensure that the frame is 8-byte aligned
363+
PADDING_STRATEGY_ALIGNED,
362364
// Padding will ensure all data frames are maxFrameSize
363365
PADDING_STRATEGY_MAX,
364366
// Padding will be determined via a JS callback. Note that this can be
@@ -917,6 +919,8 @@ class Http2Session : public AsyncWrap {
917919

918920
private:
919921
// Frame Padding Strategies
922+
inline ssize_t OnDWordAlignedPadding(size_t frameLength,
923+
size_t maxPayloadLen);
920924
inline ssize_t OnMaxFrameSizePadding(size_t frameLength,
921925
size_t maxPayloadLen);
922926
inline ssize_t OnCallbackPadding(size_t frame,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
const assert = require('assert');
7+
const http2 = require('http2');
8+
const { PADDING_STRATEGY_ALIGNED } = http2.constants;
9+
const makeDuplexPair = require('../common/duplexpair');
10+
11+
{
12+
const testData = '<h1>Hello World.</h1>';
13+
const server = http2.createServer({
14+
paddingStrategy: PADDING_STRATEGY_ALIGNED
15+
});
16+
server.on('stream', common.mustCall((stream, headers) => {
17+
stream.respond({
18+
'content-type': 'text/html',
19+
':status': 200
20+
});
21+
stream.end(testData);
22+
}));
23+
24+
const { clientSide, serverSide } = makeDuplexPair();
25+
26+
// The lengths of the expected writes... note that this is highly
27+
// sensitive to how the internals are implemented.
28+
const serverLengths = [24, 9, 9, 32];
29+
const clientLengths = [9, 9, 48, 9, 1, 21, 1, 16];
30+
31+
// Adjust for the 24-byte preamble and two 9-byte settings frames, and
32+
// the result must be equally divisible by 8
33+
assert.strictEqual(
34+
(serverLengths.reduce((i, n) => i + n) - 24 - 9 - 9) % 8, 0);
35+
36+
// Adjust for two 9-byte settings frames, and the result must be equally
37+
// divisible by 8
38+
assert.strictEqual(
39+
(clientLengths.reduce((i, n) => i + n) - 9 - 9) % 8, 0);
40+
41+
serverSide.on('data', common.mustCall((chunk) => {
42+
assert.strictEqual(chunk.length, serverLengths.shift());
43+
}, serverLengths.length));
44+
clientSide.on('data', common.mustCall((chunk) => {
45+
assert.strictEqual(chunk.length, clientLengths.shift());
46+
}, clientLengths.length));
47+
48+
server.emit('connection', serverSide);
49+
50+
const client = http2.connect('http://localhost:80', {
51+
paddingStrategy: PADDING_STRATEGY_ALIGNED,
52+
createConnection: common.mustCall(() => clientSide)
53+
});
54+
55+
const req = client.request({ ':path': '/a' });
56+
57+
req.on('response', common.mustCall());
58+
59+
req.setEncoding('utf8');
60+
req.on('data', common.mustCall((data) => {
61+
assert.strictEqual(data, testData);
62+
}));
63+
req.on('close', common.mustCall(() => {
64+
clientSide.destroy();
65+
clientSide.end();
66+
}));
67+
req.end();
68+
}

0 commit comments

Comments
 (0)