Skip to content
Open
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
7 changes: 5 additions & 2 deletions doc/api/quic.md
Original file line number Diff line number Diff line change
Expand Up @@ -987,7 +987,8 @@ added: v23.8.0
is application-specific. **Default:** `'transport'`.
* `reason` {string} An optional human-readable reason string included in
the `CONNECTION_CLOSE` frame. Per RFC 9000, this is for diagnostic purposes
only and should not be used for machine-readable error descriptions.
only and should not be used for machine-readable error descriptions. If its
UTF-8 encoding exceeds 256 bytes, it is truncated to at most 256 bytes.
* Returns: {Promise}

Initiate a graceful close of the session. Existing streams will be allowed
Expand Down Expand Up @@ -1066,7 +1067,9 @@ added: v23.8.0
* `type` {string} Either `'transport'` or `'application'`. **Default:**
`'transport'`.
* `reason` {string} An optional human-readable reason string included in
the `CONNECTION_CLOSE` frame.
the `CONNECTION_CLOSE` frame. Per RFC 9000, this is for diagnostic purposes
only and should not be used for machine-readable error descriptions. If its
UTF-8 encoding exceeds 256 bytes, it is truncated to at most 256 bytes.

Immediately destroy the session. All streams will be destroyed and the
session will be closed. If `error` is provided and [`session.onerror`][] is
Expand Down
17 changes: 17 additions & 0 deletions src/quic/data.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
#include <openssl/ssl.h>
#include <string_bytes.h>
#include <v8.h>
#include <algorithm>
#include "bindingdata.h"
#include "data.h"
#include "defs.h"
#include "simdutf.h"
#include "util.h"

namespace node {
Expand Down Expand Up @@ -218,6 +220,9 @@ void Store::MemoryInfo(MemoryTracker* tracker) const {
// ============================================================================

namespace {

constexpr size_t kMaxConnectionCloseReasonLength = 256;

constexpr std::string_view TypeName(QuicError::Type type) {
switch (type) {
case QuicError::Type::APPLICATION:
Expand Down Expand Up @@ -332,6 +337,18 @@ QuicError::operator const ngtcp2_ccerr*() const {
return ptr_;
}

ngtcp2_ccerr QuicError::ToNgtcp2ConnectionCloseError() const {
ngtcp2_ccerr error = *ptr_;
error.reason = reason_c_str();

const size_t reasonlen =
std::min(reason_.size(), kMaxConnectionCloseReasonLength);

error.reasonlen = simdutf::trim_partial_utf8(reason_.data(), reasonlen);

return error;
}

std::string QuicError::reason_for_liberr(int liberr) {
return ngtcp2_strerror(liberr);
}
Expand Down
2 changes: 2 additions & 0 deletions src/quic/data.h
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,8 @@ class QuicError final : public MemoryRetainer {
operator const ngtcp2_ccerr&() const;
operator const ngtcp2_ccerr*() const;

ngtcp2_ccerr ToNgtcp2ConnectionCloseError() const;

// Crypto errors are a subset of transport errors. The error code includes
// the TLS alert code embedded within it.
bool is_crypto_error() const;
Expand Down
3 changes: 2 additions & 1 deletion src/quic/packet.cc
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,9 @@ Packet::Ptr Packet::CreateConnectionClosePacket(
if (!packet) return packet;
ngtcp2_vec vec = *packet;

ngtcp2_ccerr wire_error = error.ToNgtcp2ConnectionCloseError();
ssize_t nwrite = ngtcp2_conn_write_connection_close(
conn, nullptr, nullptr, vec.base, vec.len, error, uv_hrtime());
conn, nullptr, nullptr, vec.base, vec.len, &wire_error, uv_hrtime());
if (nwrite < 0) return Ptr();
packet->Truncate(static_cast<size_t>(nwrite));
return packet;
Expand Down
10 changes: 3 additions & 7 deletions src/quic/session.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3680,13 +3680,9 @@ void Session::SendConnectionClose() {

ngtcp2_vec vec = *packet;
Path path(impl_->local_address_, impl_->remote_address_);
ssize_t nwrite = ngtcp2_conn_write_connection_close(*this,
&path,
nullptr,
vec.base,
vec.len,
impl_->last_error_,
uv_hrtime());
ngtcp2_ccerr wire_error = impl_->last_error_.ToNgtcp2ConnectionCloseError();
ssize_t nwrite = ngtcp2_conn_write_connection_close(
*this, &path, nullptr, vec.base, vec.len, &wire_error, uv_hrtime());

if (nwrite < 0) [[unlikely]] {
return ErrorAndSilentClose();
Expand Down
112 changes: 112 additions & 0 deletions test/parallel/test-quic-session-close-reason-truncation.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Flags: --experimental-quic --no-warnings

// CONNECTION_CLOSE reason strings are truncated to 256 UTF-8 bytes.

import { hasQuic, skip, mustCall } from '../common/index.mjs';
import assert from 'node:assert';
import { setTimeout } from 'node:timers/promises';

if (!hasQuic) {
skip('QUIC is not enabled');
}

const { listen, connect } = await import('../common/quic.mjs');

const kMaxReasonLength = 256;

// Client close with an ASCII reason longer than the wire limit.
{
const reason = 'a'.repeat(kMaxReasonLength + 1);
const expectedReason = reason.slice(0, kMaxReasonLength);
const serverDone = Promise.withResolvers();

const serverEndpoint = await listen(mustCall(async (serverSession) => {
serverSession.onerror = mustCall((error) => {
assert.strictEqual(error.reason, expectedReason);
});
await assert.rejects(serverSession.closed, {
code: 'ERR_QUIC_APPLICATION_ERROR',
reason: expectedReason,
});
serverDone.resolve();
}));

const clientSession = await connect(serverEndpoint.address, {
reuseEndpoint: false,
});
await clientSession.opened;
await setTimeout(100);
await clientSession.close({
code: 1n,
type: 'application',
reason,
});

await serverDone.promise;
await serverEndpoint.close();
}

// Client close where truncating at the byte limit would split a UTF-8 sequence.
{
const prefix = 'a'.repeat(kMaxReasonLength - 1);
const reason = `${prefix}€`;
const serverDone = Promise.withResolvers();

const serverEndpoint = await listen(mustCall(async (serverSession) => {
serverSession.onerror = mustCall((error) => {
assert.strictEqual(error.reason, prefix);
});
await assert.rejects(serverSession.closed, {
code: 'ERR_QUIC_APPLICATION_ERROR',
reason: prefix,
});
serverDone.resolve();
}));

const clientSession = await connect(serverEndpoint.address, {
reuseEndpoint: false,
});
await clientSession.opened;
await setTimeout(100);
await clientSession.close({
code: 2n,
type: 'application',
reason,
});

await serverDone.promise;
await serverEndpoint.close();
}

// Server close exercises the server-specific CONNECTION_CLOSE packet path.
{
const reason = 's'.repeat(kMaxReasonLength + 1);
const expectedReason = reason.slice(0, kMaxReasonLength);
const serverDone = Promise.withResolvers();

const serverEndpoint = await listen(mustCall(async (serverSession) => {
await serverSession.opened;
await setTimeout(100);
await serverSession.close({
code: 3n,
type: 'application',
reason,
});
serverDone.resolve();
}));

const clientSession = await connect(serverEndpoint.address, {
reuseEndpoint: false,
onerror: mustCall((error) => {
assert.strictEqual(error.reason, expectedReason);
}),
});
await clientSession.opened;
await assert.rejects(clientSession.closed, {
code: 'ERR_QUIC_APPLICATION_ERROR',
reason: expectedReason,
});

await serverDone.promise;
await serverEndpoint.close();
}
Loading