已开启
Fix CVE-2026-56846 #262
易大夫创建于 20 天前
Fix CVE-2026-56846 #262
已开启
共 2 个文件变更+382-0
| @@ -0,0 +1,191 @@ | |||
| 1 | +From f14d78b9e0201bfe0291f2b0dc4b5d88c70cc28e Mon Sep 17 00:00:00 2001 | ||
| 2 | +From: Matteo Collina <hello@matteocollina.com> | ||
| 3 | +Date: Mon, 8 Jun 2026 13:53:02 +0200 | ||
| 4 | +Subject: [PATCH] http2: retain header memory in session accounting | ||
| 5 | +MIME-Version: 1.0 | ||
| 6 | +Content-Type: text/plain; charset=UTF-8 | ||
| 7 | +Content-Transfer-Encoding: 8bit | ||
| 8 | + | ||
| 9 | +Signed-off-by: Matteo Collina <hello@matteocollina.com> | ||
| 10 | +PR-URL: https://github.com/nodejs/node/pull/63752 | ||
| 11 | +Reviewed-By: Tim Perry <pimterry@gmail.com> | ||
| 12 | +Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> | ||
| 13 | +Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day> | ||
| 14 | +CVE-ID: CVE-2026-56846 | ||
| 15 | +--- | ||
| 16 | + doc/api/http2.md | 21 +++--- | ||
| 17 | + src/node_http2.cc | 13 +++- | ||
| 18 | + src/node_http2.h | 4 +- | ||
| 19 | + ...ttp2-max-session-memory-stalled-headers.js | 69 +++++++++++++++++++ | ||
| 20 | + 4 files changed, 96 insertions(+), 11 deletions(-) | ||
| 21 | + create mode 100644 test/parallel/test-http2-max-session-memory-stalled-headers.js | ||
| 22 | + | ||
| 23 | +diff --git a/doc/api/http2.md b/doc/api/http2.md | ||
| 24 | +index 2180d0f70e6a..539979aa6d18 100644 | ||
| 25 | +--- a/doc/api/http2.md | ||
| 26 | ++++ b/doc/api/http2.md | ||
| 27 | +@@ -2845,9 +2845,10 @@ changes: | ||
| 28 | + This is a credit based limit, existing `Http2Stream`s may cause this | ||
| 29 | + limit to be exceeded, but new `Http2Stream` instances will be rejected | ||
| 30 | + while this limit is exceeded. The current number of `Http2Stream` sessions, | ||
| 31 | +- the current memory use of the header compression tables, current data | ||
| 32 | +- queued to be sent, and unacknowledged `PING` and `SETTINGS` frames are all | ||
| 33 | +- counted towards the current limit. **Default:** `10`. | ||
| 34 | ++ the current memory use of the header compression tables, header blocks | ||
| 35 | ++ retained by open streams, current data queued to be sent, and | ||
| 36 | ++ unacknowledged `PING` and `SETTINGS` frames are all counted towards the | ||
| 37 | ++ current limit. **Default:** `10`. | ||
| 38 | + * `maxHeaderListPairs` {number} Sets the maximum number of header entries. | ||
| 39 | + This is similar to [`server.maxHeadersCount`][] or | ||
| 40 | + [`request.maxHeadersCount`][] in the `node:http` module. The minimum value | ||
| 41 | +@@ -3034,9 +3035,10 @@ changes: | ||
| 42 | + credit based limit, existing `Http2Stream`s may cause this | ||
| 43 | + limit to be exceeded, but new `Http2Stream` instances will be rejected | ||
| 44 | + while this limit is exceeded. The current number of `Http2Stream` sessions, | ||
| 45 | +- the current memory use of the header compression tables, current data | ||
| 46 | +- queued to be sent, and unacknowledged `PING` and `SETTINGS` frames are all | ||
| 47 | +- counted towards the current limit. **Default:** `10`. | ||
| 48 | ++ the current memory use of the header compression tables, header blocks | ||
| 49 | ++ retained by open streams, current data queued to be sent, and | ||
| 50 | ++ unacknowledged `PING` and `SETTINGS` frames are all counted towards the | ||
| 51 | ++ current limit. **Default:** `10`. | ||
| 52 | + * `maxHeaderListPairs` {number} Sets the maximum number of header entries. | ||
| 53 | + This is similar to [`server.maxHeadersCount`][] or | ||
| 54 | + [`request.maxHeadersCount`][] in the `node:http` module. The minimum value | ||
| 55 | +@@ -3196,9 +3198,10 @@ changes: | ||
| 56 | + This is a credit based limit, existing `Http2Stream`s may cause this | ||
| 57 | + limit to be exceeded, but new `Http2Stream` instances will be rejected | ||
| 58 | + while this limit is exceeded. The current number of `Http2Stream` sessions, | ||
| 59 | +- the current memory use of the header compression tables, current data | ||
| 60 | +- queued to be sent, and unacknowledged `PING` and `SETTINGS` frames are all | ||
| 61 | +- counted towards the current limit. **Default:** `10`. | ||
| 62 | ++ the current memory use of the header compression tables, header blocks | ||
| 63 | ++ retained by open streams, current data queued to be sent, and | ||
| 64 | ++ unacknowledged `PING` and `SETTINGS` frames are all counted towards the | ||
| 65 | ++ current limit. **Default:** `10`. | ||
| 66 | + * `maxHeaderListPairs` {number} Sets the maximum number of header entries. | ||
| 67 | + This is similar to [`server.maxHeadersCount`][] or | ||
| 68 | + [`request.maxHeadersCount`][] in the `node:http` module. The minimum value | ||
| 69 | +diff --git a/src/node_http2.cc b/src/node_http2.cc | ||
| 70 | +index 403d9982ebde..a496e27289d7 100644 | ||
| 71 | +--- a/src/node_http2.cc | ||
| 72 | ++++ b/src/node_http2.cc | ||
| 73 | +@@ -907,6 +907,14 @@ BaseObjectPtr<Http2Stream> Http2Session::RemoveStream(int32_t id) { | ||
| 74 | + stream = FindStream(id); | ||
| 75 | + if (stream) { | ||
| 76 | + streams_.erase(id); | ||
| 77 | ++ if (stream->current_headers_length_ > 0) { | ||
| 78 | ++ DecrementCurrentSessionMemory(stream->current_headers_length_); | ||
| 79 | ++ stream->current_headers_length_ = 0; | ||
| 80 | ++ } | ||
| 81 | ++ if (stream->retained_headers_length_ > 0) { | ||
| 82 | ++ DecrementCurrentSessionMemory(stream->retained_headers_length_); | ||
| 83 | ++ stream->retained_headers_length_ = 0; | ||
| 84 | ++ } | ||
| 85 | + DecrementCurrentSessionMemory(sizeof(*stream)); | ||
| 86 | + } | ||
| 87 | + return stream; | ||
| 88 | +@@ -1553,7 +1561,10 @@ void Http2Session::HandleHeadersFrame(const nghttp2_frame* frame) { | ||
| 89 | + }); | ||
| 90 | + CHECK_EQ(stream->headers_count(), 0); | ||
| 91 | + | ||
| 92 | +- DecrementCurrentSessionMemory(stream->current_headers_length_); | ||
| 93 | ++ // Keep the header block charged against maxSessionMemory while the | ||
| 94 | ++ // corresponding JS objects can still keep it alive for the lifetime of | ||
| 95 | ++ // the stream. | ||
| 96 | ++ stream->retained_headers_length_ += stream->current_headers_length_; | ||
| 97 | + stream->current_headers_length_ = 0; | ||
| 98 | + | ||
| 99 | + Local<Value> args[] = { | ||
| 100 | +diff --git a/src/node_http2.h b/src/node_http2.h | ||
| 101 | +index 910460529275..cb0e067aeaa1 100644 | ||
| 102 | +--- a/src/node_http2.h | ||
| 103 | ++++ b/src/node_http2.h | ||
| 104 | +@@ -485,9 +485,11 @@ class Http2Stream : public AsyncWrap, | ||
| 105 | + | ||
| 106 | + // The Current Headers block... As headers are received for this stream, | ||
| 107 | + // they are temporarily stored here until the OnFrameReceived is called | ||
| 108 | +- // signalling the end of the HEADERS frame | ||
| 109 | ++ // signalling the end of the HEADERS frame. | ||
| 110 | + nghttp2_headers_category current_headers_category_ = NGHTTP2_HCAT_HEADERS; | ||
| 111 | + uint32_t current_headers_length_ = 0; // total number of octets | ||
| 112 | ++ // Charged against maxSessionMemory while headers stay alive in JS. | ||
| 113 | ++ uint64_t retained_headers_length_ = 0; | ||
| 114 | + std::vector<Http2Header> current_headers_; | ||
| 115 | + | ||
| 116 | + // This keeps track of the amount of data read from the socket while the | ||
| 117 | +diff --git a/test/parallel/test-http2-max-session-memory-stalled-headers.js b/test/parallel/test-http2-max-session-memory-stalled-headers.js | ||
| 118 | +new file mode 100644 | ||
| 119 | +index 000000000000..c04bfd09e627 | ||
| 120 | +--- /dev/null | ||
| 121 | ++++ b/test/parallel/test-http2-max-session-memory-stalled-headers.js | ||
| 122 | +@@ -0,0 +1,69 @@ | ||
| 123 | ++'use strict'; | ||
| 124 | ++ | ||
| 125 | ++const common = require('../common'); | ||
| 126 | ++if (!common.hasCrypto) | ||
| 127 | ++ common.skip('missing crypto'); | ||
| 128 | ++ | ||
| 129 | ++const Countdown = require('../common/countdown'); | ||
| 130 | ++const assert = require('assert'); | ||
| 131 | ++const http2 = require('http2'); | ||
| 132 | ++ | ||
| 133 | ++const { | ||
| 134 | ++ NGHTTP2_ENHANCE_YOUR_CALM, | ||
| 135 | ++} = http2.constants; | ||
| 136 | ++ | ||
| 137 | ++// Regression test: header blocks retained by stalled streams should continue | ||
| 138 | ++// to count against maxSessionMemory after they have been handed to JS. | ||
| 139 | ++const maxSessionMemory = 1; | ||
| 140 | ++const totalRequests = 400; | ||
| 141 | ++const cookieCrumbs = 120; | ||
| 142 | ++ | ||
| 143 | ++let accepted = 0; | ||
| 144 | ++let rejected = 0; | ||
| 145 | ++ | ||
| 146 | ++const server = http2.createServer({ maxSessionMemory }); | ||
| 147 | ++server.on('stream', (stream) => { | ||
| 148 | ++ accepted++; | ||
| 149 | ++ stream.on('error', () => {}); | ||
| 150 | ++ stream.respond(); | ||
| 151 | ++ stream.write('x'); | ||
| 152 | ++}); | ||
| 153 | ++ | ||
| 154 | ++server.listen(0, common.mustCall(() => { | ||
| 155 | ++ const client = http2.connect(`http://localhost:${server.address().port}`, { | ||
| 156 | ++ settings: { | ||
| 157 | ++ initialWindowSize: 0, | ||
| 158 | ++ }, | ||
| 159 | ++ }); | ||
| 160 | ++ client.on('error', () => {}); | ||
| 161 | ++ | ||
| 162 | ++ client.on('remoteSettings', common.mustCall(() => { | ||
| 163 | ++ let destroyed = false; | ||
| 164 | ++ const countdown = new Countdown(totalRequests, common.mustCall(() => { | ||
| 165 | ++ assert(rejected > 0); | ||
| 166 | ++ assert(accepted < totalRequests); | ||
| 167 | ++ server.close(); | ||
| 168 | ++ })); | ||
| 169 | ++ | ||
| 170 | ++ for (let i = 0; i < totalRequests; i++) { | ||
| 171 | ++ const headers = [':path', '/']; | ||
| 172 | ++ for (let j = 0; j < cookieCrumbs; j++) { | ||
| 173 | ++ headers.push('cookie', 'a=1'); | ||
| 174 | ++ } | ||
| 175 | ++ | ||
| 176 | ++ const req = client.request(headers); | ||
| 177 | ++ req.on('error', () => {}); | ||
| 178 | ++ req.on('close', () => { | ||
| 179 | ++ if (req.rstCode === NGHTTP2_ENHANCE_YOUR_CALM) { | ||
| 180 | ++ rejected++; | ||
| 181 | ++ if (!destroyed) { | ||
| 182 | ++ destroyed = true; | ||
| 183 | ++ client.destroy(); | ||
| 184 | ++ } | ||
| 185 | ++ } | ||
| 186 | ++ countdown.dec(); | ||
| 187 | ++ }); | ||
| 188 | ++ req.end(); | ||
| 189 | ++ } | ||
| 190 | ++ })); | ||
| 191 | ++})); | ||
| @@ -0,0 +1,191 @@ | |||
| 1 | +From 34ed88a069393a40fb97c7015e5539a0818ae756 Mon Sep 17 00:00:00 2001 | ||
| 2 | +From: Matteo Collina <hello@matteocollina.com> | ||
| 3 | +Date: Mon, 8 Jun 2026 13:53:02 +0200 | ||
| 4 | +Subject: [PATCH] http2: retain header memory in session accounting | ||
| 5 | +MIME-Version: 1.0 | ||
| 6 | +Content-Type: text/plain; charset=UTF-8 | ||
| 7 | +Content-Transfer-Encoding: 8bit | ||
| 8 | + | ||
| 9 | +Signed-off-by: Matteo Collina <hello@matteocollina.com> | ||
| 10 | +PR-URL: https://github.com/nodejs/node/pull/63752 | ||
| 11 | +Reviewed-By: Tim Perry <pimterry@gmail.com> | ||
| 12 | +Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> | ||
| 13 | +Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day> | ||
| 14 | +CVE-ID: CVE-2026-56846 | ||
| 15 | +--- | ||
| 16 | + doc/api/http2.md | 21 +++--- | ||
| 17 | + src/node_http2.cc | 13 +++- | ||
| 18 | + src/node_http2.h | 4 +- | ||
| 19 | + ...ttp2-max-session-memory-stalled-headers.js | 69 +++++++++++++++++++ | ||
| 20 | + 4 files changed, 96 insertions(+), 11 deletions(-) | ||
| 21 | + create mode 100644 test/parallel/test-http2-max-session-memory-stalled-headers.js | ||
| 22 | + | ||
| 23 | +diff --git a/doc/api/http2.md b/doc/api/http2.md | ||
| 24 | +index fc30d273967d..ca7c4a216404 100644 | ||
| 25 | +--- a/doc/api/http2.md | ||
| 26 | ++++ b/doc/api/http2.md | ||
| 27 | +@@ -2860,9 +2860,10 @@ changes: | ||
| 28 | + This is a credit based limit, existing `Http2Stream`s may cause this | ||
| 29 | + limit to be exceeded, but new `Http2Stream` instances will be rejected | ||
| 30 | + while this limit is exceeded. The current number of `Http2Stream` sessions, | ||
| 31 | +- the current memory use of the header compression tables, current data | ||
| 32 | +- queued to be sent, and unacknowledged `PING` and `SETTINGS` frames are all | ||
| 33 | +- counted towards the current limit. **Default:** `10`. | ||
| 34 | ++ the current memory use of the header compression tables, header blocks | ||
| 35 | ++ retained by open streams, current data queued to be sent, and | ||
| 36 | ++ unacknowledged `PING` and `SETTINGS` frames are all counted towards the | ||
| 37 | ++ current limit. **Default:** `10`. | ||
| 38 | + * `maxHeaderListPairs` {number} Sets the maximum number of header entries. | ||
| 39 | + This is similar to [`server.maxHeadersCount`][] or | ||
| 40 | + [`request.maxHeadersCount`][] in the `node:http` module. The minimum value | ||
| 41 | +@@ -3077,9 +3078,10 @@ changes: | ||
| 42 | + credit based limit, existing `Http2Stream`s may cause this | ||
| 43 | + limit to be exceeded, but new `Http2Stream` instances will be rejected | ||
| 44 | + while this limit is exceeded. The current number of `Http2Stream` sessions, | ||
| 45 | +- the current memory use of the header compression tables, current data | ||
| 46 | +- queued to be sent, and unacknowledged `PING` and `SETTINGS` frames are all | ||
| 47 | +- counted towards the current limit. **Default:** `10`. | ||
| 48 | ++ the current memory use of the header compression tables, header blocks | ||
| 49 | ++ retained by open streams, current data queued to be sent, and | ||
| 50 | ++ unacknowledged `PING` and `SETTINGS` frames are all counted towards the | ||
| 51 | ++ current limit. **Default:** `10`. | ||
| 52 | + * `maxHeaderListPairs` {number} Sets the maximum number of header entries. | ||
| 53 | + This is similar to [`server.maxHeadersCount`][] or | ||
| 54 | + [`request.maxHeadersCount`][] in the `node:http` module. The minimum value | ||
| 55 | +@@ -3257,9 +3259,10 @@ changes: | ||
| 56 | + This is a credit based limit, existing `Http2Stream`s may cause this | ||
| 57 | + limit to be exceeded, but new `Http2Stream` instances will be rejected | ||
| 58 | + while this limit is exceeded. The current number of `Http2Stream` sessions, | ||
| 59 | +- the current memory use of the header compression tables, current data | ||
| 60 | +- queued to be sent, and unacknowledged `PING` and `SETTINGS` frames are all | ||
| 61 | +- counted towards the current limit. **Default:** `10`. | ||
| 62 | ++ the current memory use of the header compression tables, header blocks | ||
| 63 | ++ retained by open streams, current data queued to be sent, and | ||
| 64 | ++ unacknowledged `PING` and `SETTINGS` frames are all counted towards the | ||
| 65 | ++ current limit. **Default:** `10`. | ||
| 66 | + * `maxHeaderListPairs` {number} Sets the maximum number of header entries. | ||
| 67 | + This is similar to [`server.maxHeadersCount`][] or | ||
| 68 | + [`request.maxHeadersCount`][] in the `node:http` module. The minimum value | ||
| 69 | +diff --git a/src/node_http2.cc b/src/node_http2.cc | ||
| 70 | +index 55bca557a81e..591798a63715 100644 | ||
| 71 | +--- a/src/node_http2.cc | ||
| 72 | ++++ b/src/node_http2.cc | ||
| 73 | +@@ -902,6 +902,14 @@ BaseObjectPtr<Http2Stream> Http2Session::RemoveStream(int32_t id) { | ||
| 74 | + stream = FindStream(id); | ||
| 75 | + if (stream) { | ||
| 76 | + streams_.erase(id); | ||
| 77 | ++ if (stream->current_headers_length_ > 0) { | ||
| 78 | ++ DecrementCurrentSessionMemory(stream->current_headers_length_); | ||
| 79 | ++ stream->current_headers_length_ = 0; | ||
| 80 | ++ } | ||
| 81 | ++ if (stream->retained_headers_length_ > 0) { | ||
| 82 | ++ DecrementCurrentSessionMemory(stream->retained_headers_length_); | ||
| 83 | ++ stream->retained_headers_length_ = 0; | ||
| 84 | ++ } | ||
| 85 | + DecrementCurrentSessionMemory(sizeof(*stream)); | ||
| 86 | + } | ||
| 87 | + return stream; | ||
| 88 | +@@ -1548,7 +1556,10 @@ void Http2Session::HandleHeadersFrame(const nghttp2_frame* frame) { | ||
| 89 | + }); | ||
| 90 | + CHECK_EQ(stream->headers_count(), 0); | ||
| 91 | + | ||
| 92 | +- DecrementCurrentSessionMemory(stream->current_headers_length_); | ||
| 93 | ++ // Keep the header block charged against maxSessionMemory while the | ||
| 94 | ++ // corresponding JS objects can still keep it alive for the lifetime of | ||
| 95 | ++ // the stream. | ||
| 96 | ++ stream->retained_headers_length_ += stream->current_headers_length_; | ||
| 97 | + stream->current_headers_length_ = 0; | ||
| 98 | + | ||
| 99 | + Local<Value> args[] = { | ||
| 100 | +diff --git a/src/node_http2.h b/src/node_http2.h | ||
| 101 | +index 28245d7b98e0..0bb2b0cb2891 100644 | ||
| 102 | +--- a/src/node_http2.h | ||
| 103 | ++++ b/src/node_http2.h | ||
| 104 | +@@ -490,9 +490,11 @@ class Http2Stream : public AsyncWrap, | ||
| 105 | + | ||
| 106 | + // The Current Headers block... As headers are received for this stream, | ||
| 107 | + // they are temporarily stored here until the OnFrameReceived is called | ||
| 108 | +- // signalling the end of the HEADERS frame | ||
| 109 | ++ // signalling the end of the HEADERS frame. | ||
| 110 | + nghttp2_headers_category current_headers_category_ = NGHTTP2_HCAT_HEADERS; | ||
| 111 | + uint32_t current_headers_length_ = 0; // total number of octets | ||
| 112 | ++ // Charged against maxSessionMemory while headers stay alive in JS. | ||
| 113 | ++ uint64_t retained_headers_length_ = 0; | ||
| 114 | + std::vector<Http2Header> current_headers_; | ||
| 115 | + | ||
| 116 | + // This keeps track of the amount of data read from the socket while the | ||
| 117 | +diff --git a/test/parallel/test-http2-max-session-memory-stalled-headers.js b/test/parallel/test-http2-max-session-memory-stalled-headers.js | ||
| 118 | +new file mode 100644 | ||
| 119 | +index 000000000000..c04bfd09e627 | ||
| 120 | +--- /dev/null | ||
| 121 | ++++ b/test/parallel/test-http2-max-session-memory-stalled-headers.js | ||
| 122 | +@@ -0,0 +1,69 @@ | ||
| 123 | ++'use strict'; | ||
| 124 | ++ | ||
| 125 | ++const common = require('../common'); | ||
| 126 | ++if (!common.hasCrypto) | ||
| 127 | ++ common.skip('missing crypto'); | ||
| 128 | ++ | ||
| 129 | ++const Countdown = require('../common/countdown'); | ||
| 130 | ++const assert = require('assert'); | ||
| 131 | ++const http2 = require('http2'); | ||
| 132 | ++ | ||
| 133 | ++const { | ||
| 134 | ++ NGHTTP2_ENHANCE_YOUR_CALM, | ||
| 135 | ++} = http2.constants; | ||
| 136 | ++ | ||
| 137 | ++// Regression test: header blocks retained by stalled streams should continue | ||
| 138 | ++// to count against maxSessionMemory after they have been handed to JS. | ||
| 139 | ++const maxSessionMemory = 1; | ||
| 140 | ++const totalRequests = 400; | ||
| 141 | ++const cookieCrumbs = 120; | ||
| 142 | ++ | ||
| 143 | ++let accepted = 0; | ||
| 144 | ++let rejected = 0; | ||
| 145 | ++ | ||
| 146 | ++const server = http2.createServer({ maxSessionMemory }); | ||
| 147 | ++server.on('stream', (stream) => { | ||
| 148 | ++ accepted++; | ||
| 149 | ++ stream.on('error', () => {}); | ||
| 150 | ++ stream.respond(); | ||
| 151 | ++ stream.write('x'); | ||
| 152 | ++}); | ||
| 153 | ++ | ||
| 154 | ++server.listen(0, common.mustCall(() => { | ||
| 155 | ++ const client = http2.connect(`http://localhost:${server.address().port}`, { | ||
| 156 | ++ settings: { | ||
| 157 | ++ initialWindowSize: 0, | ||
| 158 | ++ }, | ||
| 159 | ++ }); | ||
| 160 | ++ client.on('error', () => {}); | ||
| 161 | ++ | ||
| 162 | ++ client.on('remoteSettings', common.mustCall(() => { | ||
| 163 | ++ let destroyed = false; | ||
| 164 | ++ const countdown = new Countdown(totalRequests, common.mustCall(() => { | ||
| 165 | ++ assert(rejected > 0); | ||
| 166 | ++ assert(accepted < totalRequests); | ||
| 167 | ++ server.close(); | ||
| 168 | ++ })); | ||
| 169 | ++ | ||
| 170 | ++ for (let i = 0; i < totalRequests; i++) { | ||
| 171 | ++ const headers = [':path', '/']; | ||
| 172 | ++ for (let j = 0; j < cookieCrumbs; j++) { | ||
| 173 | ++ headers.push('cookie', 'a=1'); | ||
| 174 | ++ } | ||
| 175 | ++ | ||
| 176 | ++ const req = client.request(headers); | ||
| 177 | ++ req.on('error', () => {}); | ||
| 178 | ++ req.on('close', () => { | ||
| 179 | ++ if (req.rstCode === NGHTTP2_ENHANCE_YOUR_CALM) { | ||
| 180 | ++ rejected++; | ||
| 181 | ++ if (!destroyed) { | ||
| 182 | ++ destroyed = true; | ||
| 183 | ++ client.destroy(); | ||
| 184 | ++ } | ||
| 185 | ++ } | ||
| 186 | ++ countdown.dec(); | ||
| 187 | ++ }); | ||
| 188 | ++ req.end(); | ||
| 189 | ++ } | ||
| 190 | ++ })); | ||
| 191 | ++})); | ||