Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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);
Comment thread
efekrskl marked this conversation as resolved.
Resolved
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