From 2dbd8aad7f22fe014173d63715a3a232fde6d32f Mon Sep 17 00:00:00 2001
From: zhangyongsheng <zhangyongsheng@youzan.com>
Date: Tue, 10 Nov 2020 22:22:35 +0800
Subject: [PATCH] http2: allow setting the local window size of a session

---
 doc/api/errors.md                             |   5 +
 doc/api/http2.md                              |  23 ++++
 lib/internal/errors.js                        |   1 +
 lib/internal/http2/core.js                    |  29 ++++-
 src/node_http2.cc                             |  21 +++
 src/node_http2.h                              |   3 +
 test/parallel/test-http2-client-destroy.js    |   2 +
 .../test-http2-client-setLocalWindowSize.js   | 121 ++++++++++++++++++
 .../test-http2-server-setLocalWindowSize.js   |  37 ++++++
 9 files changed, 237 insertions(+), 5 deletions(-)
 create mode 100644 test/parallel/test-http2-client-setLocalWindowSize.js
 create mode 100644 test/parallel/test-http2-server-setLocalWindowSize.js

diff --git a/doc/api/errors.md b/doc/api/errors.md
index 192c4ecf5772f2..9433e00897877b 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -1226,6 +1226,11 @@ reached.
 An attempt was made to initiate a new push stream from within a push stream.
 Nested push streams are not permitted.
 
+<a id="ERR_HTTP2_NO_MEM"></a>
+### `ERR_HTTP2_NO_MEM`
+
+Out of memory when using the `http2session.setLocalWindowSize(windowSize)` API.
+
 <a id="ERR_HTTP2_NO_SOCKET_MANIPULATION"></a>
 ### `ERR_HTTP2_NO_SOCKET_MANIPULATION`
 
diff --git a/doc/api/http2.md b/doc/api/http2.md
index b4977397f350c1..d09d093f3a71d6 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -519,6 +519,29 @@ added: v8.4.0
 A prototype-less object describing the current remote settings of this
 `Http2Session`. The remote settings are set by the *connected* HTTP/2 peer.
 
+#### `http2session.setLocalWindowSize(windowSize)`
+<!-- YAML
+added: REPLACEME
+-->
+
+* `windowSize` {number}
+
+Sets the local endpoint's window size.
+The `windowSize` is the total window size to set, not
+the delta.
+
+```js
+const http2 = require('http2');
+
+const server = http2.createServer();
+const expectedWindowSize = 2 ** 20;
+server.on('connect', (session) => {
+
+  // Set local window size to be 2 ** 20
+  session.setLocalWindowSize(expectedWindowSize);
+});
+```
+
 #### `http2session.setTimeout(msecs, callback)`
 <!-- YAML
 added: v8.4.0
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index 20a9e3e7df3e8a..6eee2d9ce5fe87 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -904,6 +904,7 @@ E('ERR_HTTP2_MAX_PENDING_SETTINGS_ACK',
   'Maximum number of pending settings acknowledgements', Error);
 E('ERR_HTTP2_NESTED_PUSH',
   'A push stream cannot initiate another push stream.', Error);
+E('ERR_HTTP2_NO_MEM', 'Out of memory', Error);
 E('ERR_HTTP2_NO_SOCKET_MANIPULATION',
   'HTTP/2 sockets should not be directly manipulated (e.g. read and written)',
   Error);
diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index b585d61cd1db92..a9c35d7cef1a6b 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -70,6 +70,7 @@ const {
     ERR_HTTP2_INVALID_STREAM,
     ERR_HTTP2_MAX_PENDING_SETTINGS_ACK,
     ERR_HTTP2_NESTED_PUSH,
+    ERR_HTTP2_NO_MEM,
     ERR_HTTP2_NO_SOCKET_MANIPULATION,
     ERR_HTTP2_ORIGIN_LENGTH,
     ERR_HTTP2_OUT_OF_STREAMS,
@@ -101,11 +102,13 @@ const {
   },
   hideStackFrames
 } = require('internal/errors');
-const { validateInteger,
-        validateNumber,
-        validateString,
-        validateUint32,
-        isUint32,
+const {
+  isUint32,
+  validateInt32,
+  validateInteger,
+  validateNumber,
+  validateString,
+  validateUint32,
 } = require('internal/validators');
 const fsPromisesInternal = require('internal/fs/promises');
 const { utcDate } = require('internal/http');
@@ -252,6 +255,7 @@ const {
   NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE,
   NGHTTP2_ERR_INVALID_ARGUMENT,
   NGHTTP2_ERR_STREAM_CLOSED,
+  NGHTTP2_ERR_NOMEM,
 
   HTTP2_HEADER_AUTHORITY,
   HTTP2_HEADER_DATE,
@@ -1254,6 +1258,21 @@ class Http2Session extends EventEmitter {
     this[kHandle].setNextStreamID(id);
   }
 
+  // Sets the local window size (local endpoints's window size)
+  // Returns 0 if sucess or throw an exception if NGHTTP2_ERR_NOMEM
+  // if the window allocation fails
+  setLocalWindowSize(windowSize) {
+    if (this.destroyed)
+      throw new ERR_HTTP2_INVALID_SESSION();
+
+    validateInt32(windowSize, 'windowSize', 0);
+    const ret = this[kHandle].setLocalWindowSize(windowSize);
+
+    if (ret === NGHTTP2_ERR_NOMEM) {
+      this.destroy(new ERR_HTTP2_NO_MEM());
+    }
+  }
+
   // If ping is called while we are still connecting, or after close() has
   // been called, the ping callback will be invoked immediately will a ping
   // cancelled error and a duration of 0.0.
diff --git a/src/node_http2.cc b/src/node_http2.cc
index b8e462419e5feb..776393de8f800a 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -2416,6 +2416,25 @@ void Http2Session::SetNextStreamID(const FunctionCallbackInfo<Value>& args) {
   Debug(session, "set next stream id to %d", id);
 }
 
+// Set local window size (local endpoints's window size) to the given
+// window_size for the stream denoted by 0.
+// This function returns 0 if it succeeds, or one of a negative codes
+void Http2Session::SetLocalWindowSize(
+    const FunctionCallbackInfo<Value>& args) {
+  Environment* env = Environment::GetCurrent(args);
+  Http2Session* session;
+  ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
+
+  int32_t window_size = args[0]->Int32Value(env->context()).ToChecked();
+
+  int result = nghttp2_session_set_local_window_size(
+      session->session(), NGHTTP2_FLAG_NONE, 0, window_size);
+
+  args.GetReturnValue().Set(result);
+
+  Debug(session, "set local window size to %d", window_size);
+}
+
 // A TypedArray instance is shared between C++ and JS land to contain the
 // SETTINGS (either remote or local). RefreshSettings updates the current
 // values established for each of the settings so those can be read in JS land.
@@ -3088,6 +3107,8 @@ void Initialize(Local<Object> target,
   env->SetProtoMethod(session, "request", Http2Session::Request);
   env->SetProtoMethod(session, "setNextStreamID",
                       Http2Session::SetNextStreamID);
+  env->SetProtoMethod(session, "setLocalWindowSize",
+                      Http2Session::SetLocalWindowSize);
   env->SetProtoMethod(session, "updateChunksSent",
                       Http2Session::UpdateChunksSent);
   env->SetProtoMethod(session, "refreshState", Http2Session::RefreshState);
diff --git a/src/node_http2.h b/src/node_http2.h
index 306f5460691e06..9c7ffce2c41955 100644
--- a/src/node_http2.h
+++ b/src/node_http2.h
@@ -699,6 +699,8 @@ class Http2Session : public AsyncWrap,
   static void Settings(const v8::FunctionCallbackInfo<v8::Value>& args);
   static void Request(const v8::FunctionCallbackInfo<v8::Value>& args);
   static void SetNextStreamID(const v8::FunctionCallbackInfo<v8::Value>& args);
+  static void SetLocalWindowSize(
+      const v8::FunctionCallbackInfo<v8::Value>& args);
   static void Goaway(const v8::FunctionCallbackInfo<v8::Value>& args);
   static void UpdateChunksSent(const v8::FunctionCallbackInfo<v8::Value>& args);
   static void RefreshState(const v8::FunctionCallbackInfo<v8::Value>& args);
@@ -1115,6 +1117,7 @@ class Origins {
   V(NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE)                                       \
   V(NGHTTP2_ERR_INVALID_ARGUMENT)                                              \
   V(NGHTTP2_ERR_STREAM_CLOSED)                                                 \
+  V(NGHTTP2_ERR_NOMEM)                                                         \
   V(STREAM_OPTION_EMPTY_PAYLOAD)                                               \
   V(STREAM_OPTION_GET_TRAILERS)
 
diff --git a/test/parallel/test-http2-client-destroy.js b/test/parallel/test-http2-client-destroy.js
index 12da903c6535a0..c91e88079db6bb 100644
--- a/test/parallel/test-http2-client-destroy.js
+++ b/test/parallel/test-http2-client-destroy.js
@@ -77,6 +77,7 @@ const Countdown = require('../common/countdown');
     };
 
     assert.throws(() => client.setNextStreamID(), sessionError);
+    assert.throws(() => client.setLocalWindowSize(), sessionError);
     assert.throws(() => client.ping(), sessionError);
     assert.throws(() => client.settings({}), sessionError);
     assert.throws(() => client.goaway(), sessionError);
@@ -87,6 +88,7 @@ const Countdown = require('../common/countdown');
     // so that state.destroyed is set to true
     setImmediate(() => {
       assert.throws(() => client.setNextStreamID(), sessionError);
+      assert.throws(() => client.setLocalWindowSize(), sessionError);
       assert.throws(() => client.ping(), sessionError);
       assert.throws(() => client.settings({}), sessionError);
       assert.throws(() => client.goaway(), sessionError);
diff --git a/test/parallel/test-http2-client-setLocalWindowSize.js b/test/parallel/test-http2-client-setLocalWindowSize.js
new file mode 100644
index 00000000000000..8e3b57ed0c1a6b
--- /dev/null
+++ b/test/parallel/test-http2-client-setLocalWindowSize.js
@@ -0,0 +1,121 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+const assert = require('assert');
+const http2 = require('http2');
+
+{
+  const server = http2.createServer();
+  server.on('stream', common.mustNotCall((stream) => {
+    stream.respond();
+    stream.end('ok');
+  }));
+
+  const types = {
+    boolean: true,
+    function: () => {},
+    number: 1,
+    object: {},
+    array: [],
+    null: null,
+  };
+
+  server.listen(0, common.mustCall(() => {
+    const client = http2.connect(`http://localhost:${server.address().port}`);
+
+    client.on('connect', common.mustCall(() => {
+      const outOfRangeNum = 2 ** 32;
+      assert.throws(
+        () => client.setLocalWindowSize(outOfRangeNum),
+        {
+          name: 'RangeError',
+          code: 'ERR_OUT_OF_RANGE',
+          message: 'The value of "windowSize" is out of range.' +
+            ' It must be >= 0 && <= 2147483647. Received ' + outOfRangeNum
+        }
+      );
+
+      // Throw if something other than number is passed to setLocalWindowSize
+      Object.entries(types).forEach(([type, value]) => {
+        if (type === 'number') {
+          return;
+        }
+
+        assert.throws(
+          () => client.setLocalWindowSize(value),
+          {
+            name: 'TypeError',
+            code: 'ERR_INVALID_ARG_TYPE',
+            message: 'The "windowSize" argument must be of type number.' +
+                    common.invalidArgTypeHelper(value)
+          }
+        );
+      });
+
+      server.close();
+      client.close();
+    }));
+  }));
+}
+
+{
+  const server = http2.createServer();
+  server.on('stream', common.mustNotCall((stream) => {
+    stream.respond();
+    stream.end('ok');
+  }));
+
+  server.listen(0, common.mustCall(() => {
+    const client = http2.connect(`http://localhost:${server.address().port}`);
+
+    client.on('connect', common.mustCall(() => {
+      const windowSize = 2 ** 20;
+      const defaultSetting = http2.getDefaultSettings();
+      client.setLocalWindowSize(windowSize);
+
+      assert.strictEqual(client.state.effectiveLocalWindowSize, windowSize);
+      assert.strictEqual(client.state.localWindowSize, windowSize);
+      assert.strictEqual(
+        client.state.remoteWindowSize,
+        defaultSetting.initialWindowSize
+      );
+
+      server.close();
+      client.close();
+    }));
+  }));
+}
+
+{
+  const server = http2.createServer();
+  server.on('stream', common.mustNotCall((stream) => {
+    stream.respond();
+    stream.end('ok');
+  }));
+
+  server.listen(0, common.mustCall(() => {
+    const client = http2.connect(`http://localhost:${server.address().port}`);
+
+    client.on('connect', common.mustCall(() => {
+      const windowSize = 20;
+      const defaultSetting = http2.getDefaultSettings();
+      client.setLocalWindowSize(windowSize);
+
+      assert.strictEqual(client.state.effectiveLocalWindowSize, windowSize);
+      assert.strictEqual(
+        client.state.localWindowSize,
+        defaultSetting.initialWindowSize
+      );
+      assert.strictEqual(
+        client.state.remoteWindowSize,
+        defaultSetting.initialWindowSize
+      );
+
+      server.close();
+      client.close();
+    }));
+  }));
+}
diff --git a/test/parallel/test-http2-server-setLocalWindowSize.js b/test/parallel/test-http2-server-setLocalWindowSize.js
new file mode 100644
index 00000000000000..8fcb9b9d0d81b0
--- /dev/null
+++ b/test/parallel/test-http2-server-setLocalWindowSize.js
@@ -0,0 +1,37 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+const assert = require('assert');
+const http2 = require('http2');
+
+const server = http2.createServer();
+server.on('stream', common.mustCall((stream) => {
+  stream.respond();
+  stream.end('ok');
+}));
+server.on('session', common.mustCall((session) => {
+  const windowSize = 2 ** 20;
+  const defaultSetting = http2.getDefaultSettings();
+  session.setLocalWindowSize(windowSize);
+
+  assert.strictEqual(session.state.effectiveLocalWindowSize, windowSize);
+  assert.strictEqual(session.state.localWindowSize, windowSize);
+  assert.strictEqual(
+    session.state.remoteWindowSize,
+    defaultSetting.initialWindowSize
+  );
+}));
+
+server.listen(0, common.mustCall(() => {
+  const client = http2.connect(`http://localhost:${server.address().port}`);
+
+  const req = client.request();
+  req.resume();
+  req.on('close', common.mustCall(() => {
+    client.close();
+    server.close();
+  }));
+}));