Skip to content
Merged
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
9 changes: 5 additions & 4 deletions doc/contributing/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,11 +272,12 @@ $ git reset --hard upstream/vN.x
The list of patches to include should be listed in the "Next Security Release"
issue in `nodejs-private`. Ask the security release steward if you're unsure.

The `git node land` tool does not work with the `nodejs-private`
organization. To land a PR in Node.js private, use `git cherry-pick` to apply
each commit from the PR. You will also need to manually apply the PR
metadata (`PR-URL`, `Reviewed-by`, etc.) by amending the commit messages. If
To use the `git node land` tool to land Pull Requests in the `nodejs-private`
organization, you need to specify the full URL to the Pull Request and make sure
you provide a GitHub token with read permission to the private repository. If
known, additionally include `CVE-ID: CVE-XXXX-XXXXX` in the commit metadata.
Make sure to sign and push to resulting commit to the private repository and not
the public one.

**Note**: Do not run CI on the PRs in `nodejs-private` until CI is locked down.
You can integrate the PRs into the proposal without running full CI.
Expand Down
2 changes: 1 addition & 1 deletion lib/eslint.config_partial.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const noRestrictedSyntax = [
message: "`btoa` supports only latin-1 charset, use Buffer.from(str).toString('base64') instead",
},
{
selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError|QuotaExceededError)$/])',
selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError|QuicError|QuotaExceededError)$/])',
message: "Use an error exported by 'internal/errors' instead.",
},
{
Expand Down
2 changes: 0 additions & 2 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1689,14 +1689,12 @@ E('ERR_PERFORMANCE_INVALID_TIMESTAMP',
E('ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS', '%s', TypeError);
E('ERR_PROXY_INVALID_CONFIG', '%s', Error);
E('ERR_PROXY_TUNNEL', '%s', Error);
E('ERR_QUIC_APPLICATION_ERROR', 'A QUIC application error occurred. %d [%s]', Error);
E('ERR_QUIC_CONNECTION_FAILED', 'QUIC connection failed', Error);
E('ERR_QUIC_ENDPOINT_CLOSED', 'QUIC endpoint closed: %s (%d)', Error);
E('ERR_QUIC_OPEN_STREAM_FAILED', 'Failed to open QUIC stream', Error);
E('ERR_QUIC_STREAM_ABORTED', '%s', Error);
E('ERR_QUIC_STREAM_RESET',
'The QUIC stream was reset by the peer with error code %d', Error);
E('ERR_QUIC_TRANSPORT_ERROR', 'A QUIC transport error occurred. %d [%s]', Error);
E('ERR_QUIC_VERSION_NEGOTIATION_ERROR', 'The QUIC session requires version negotiation', Error);
E('ERR_REQUIRE_ASYNC_MODULE', function(filename, parentFilename) {
let message = 'require() cannot be used on an ESM ' +
Expand Down
85 changes: 65 additions & 20 deletions lib/internal/quic/quic.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const {
ArrayPrototypePush,
BigInt,
DataViewPrototypeGetByteLength,
ErrorCaptureStackTrace,
FunctionPrototypeBind,
Number,
ObjectDefineProperties,
Expand Down Expand Up @@ -108,13 +109,11 @@ const {
ERR_INVALID_THIS,
ERR_MISSING_ARGS,
ERR_OUT_OF_RANGE,
ERR_QUIC_APPLICATION_ERROR,
ERR_QUIC_CONNECTION_FAILED,
ERR_QUIC_ENDPOINT_CLOSED,
ERR_QUIC_OPEN_STREAM_FAILED,
ERR_QUIC_STREAM_ABORTED,
ERR_QUIC_STREAM_RESET,
ERR_QUIC_TRANSPORT_ERROR,
ERR_QUIC_VERSION_NEGOTIATION_ERROR,
},
} = require('internal/errors');
Expand Down Expand Up @@ -738,10 +737,12 @@ setCallbacks({
* @param {number} errorType
* @param {number} code
* @param {string} [reason]
* @param {string} [errorName] Decoded TLS alert name when `code` is a
* CRYPTO_ERROR; otherwise undefined.
*/
onSessionClose(errorType, code, reason) {
debug('session close callback', errorType, code, reason);
this[kOwner][kFinishClose](errorType, code, reason);
onSessionClose(errorType, code, reason, errorName) {
debug('session close callback', errorType, code, reason, errorName);
this[kOwner][kFinishClose](errorType, code, reason, errorName);
},

/**
Expand Down Expand Up @@ -931,8 +932,12 @@ setCallbacks({
// was an abnormal termination even if the session closed cleanly.
const resetCode = getQuicStreamState(this[kOwner]).resetCode;
if (resetCode !== undefined && resetCode > 0n) {
error = new ERR_QUIC_APPLICATION_ERROR(
resetCode, `stream reset with code ${resetCode}`);
error = makeQuicError(
'ERR_QUIC_APPLICATION_ERROR',
'QUIC application error',
'application',
resetCode,
`stream reset with code ${resetCode}`);
}
}
debug(`stream ${this[kOwner].id} closed callback with error: ${error}`);
Expand Down Expand Up @@ -1054,21 +1059,50 @@ class QuicError extends Error {
}
}

// Converts a raw QuicError array [type, code, reason] from C++ into a
// proper Node.js Error object.
// Build the human-readable message for an ERR_QUIC_TRANSPORT_ERROR or
// ERR_QUIC_APPLICATION_ERROR. `errorName` is the symbolic name for
// the wire code when known: either the OpenSSL-decoded TLS alert
// (CRYPTO_ERROR; 0x100..0x1ff) or one of the named transport codes
// from RFC 9000 (e.g. PROTOCOL_VIOLATION). Otherwise undefined.
// `reason` is the peer-supplied UTF-8 reason string from the
// CONNECTION_CLOSE / RESET_STREAM frame, often empty.
function quicErrorMessage(prefix, errorCode, reason, errorName) {
let msg = `${prefix} `;
msg += errorName ? `${errorName} (${errorCode})` : `${errorCode}`;
if (reason) msg += `: ${reason}`;
return msg;
}

function makeQuicError(code, prefix, type, errorCode, reason, errorName) {
const err = new QuicError(
quicErrorMessage(prefix, errorCode, reason, errorName),
{ errorCode, code, type });
ErrorCaptureStackTrace(err, makeQuicError);
if (reason) err.reason = reason;
if (errorName) err.errorName = errorName;
return err;
}

function convertQuicError(error) {
const type = error[0];
const code = error[1];
const reason = error[2];
const errorName = error[3];
switch (type) {
case 'transport':
return new ERR_QUIC_TRANSPORT_ERROR(code, reason);
return makeQuicError('ERR_QUIC_TRANSPORT_ERROR',
'QUIC transport error',
'transport', code, reason, errorName);
case 'application':
return new ERR_QUIC_APPLICATION_ERROR(code, reason);
return makeQuicError('ERR_QUIC_APPLICATION_ERROR',
'QUIC application error',
'application', code, reason, errorName);
case 'version_negotiation':
return new ERR_QUIC_VERSION_NEGOTIATION_ERROR();
default:
return new ERR_QUIC_TRANSPORT_ERROR(code, reason);
return makeQuicError('ERR_QUIC_TRANSPORT_ERROR',
'QUIC transport error',
'transport', code, reason, errorName);
}
}

Expand Down Expand Up @@ -3575,7 +3609,7 @@ class QuicSession {
* @param {number} code
* @param {string} [reason]
*/
[kFinishClose](errorType, code, reason) {
[kFinishClose](errorType, code, reason, errorName) {
// If code is zero, then we closed without an error. Yay! We can destroy
// safely without specifying an error.
if (code === 0n) {
Expand All @@ -3584,7 +3618,8 @@ class QuicSession {
return;
}

debug('finishing closing the session with an error', errorType, code, reason);
debug('finishing closing the session with an error',
errorType, code, reason, errorName);

// If the local side initiated this close with an error code (via
// close({ code })), this is an intentional shutdown; not an error.
Expand All @@ -3611,10 +3646,14 @@ class QuicSession {
// session would leak with `closed` hanging forever.
switch (errorType) {
case 0: /* Transport Error */
this.destroy(new ERR_QUIC_TRANSPORT_ERROR(code, reason));
this.destroy(makeQuicError('ERR_QUIC_TRANSPORT_ERROR',
'QUIC transport error',
'transport', code, reason, errorName));
break;
case 1: /* Application Error */
this.destroy(new ERR_QUIC_APPLICATION_ERROR(code, reason));
this.destroy(makeQuicError('ERR_QUIC_APPLICATION_ERROR',
'QUIC application error',
'application', code, reason, errorName));
break;
case 2: /* Version Negotiation Error */
this.destroy(new ERR_QUIC_VERSION_NEGOTIATION_ERROR());
Expand All @@ -3623,7 +3662,9 @@ class QuicSession {
this.destroy();
break;
default:
this.destroy(new ERR_QUIC_TRANSPORT_ERROR(code, reason));
this.destroy(makeQuicError('ERR_QUIC_TRANSPORT_ERROR',
'QUIC transport error',
'transport', code, reason, errorName));
break;
}
}
Expand Down Expand Up @@ -3874,9 +3915,13 @@ class QuicSession {
// decide. In 'strict' mode, the handshake already failed at the C++
// level (SSL_VERIFY_PEER) so we won't reach here.
if (inner.verifyPeer === 'auto' && validationErrorReason !== undefined) {
const err = new ERR_QUIC_TRANSPORT_ERROR(
0, `Peer certificate validation failed: ${validationErrorReason}` +
` [${validationErrorCode}]`);
const err = makeQuicError(
'ERR_QUIC_TRANSPORT_ERROR',
'QUIC transport error',
'transport',
0n,
`Peer certificate validation failed: ${validationErrorReason}` +
` [${validationErrorCode}]`);
inner.pendingOpen.reject?.(err);
inner.pendingOpen.resolve = undefined;
inner.pendingOpen.reject = undefined;
Expand Down
107 changes: 59 additions & 48 deletions lib/internal/streams/iter/share.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ class ShareImpl {
resolve: null,
reject: null,
detached: false,
pendingNext: PromiseResolve(),
};

this.#consumers.add(state);
Expand All @@ -129,62 +130,72 @@ class ShareImpl {
return {
__proto__: null,
[SymbolAsyncIterator]() {
return {
__proto__: null,
async next() {
if (self.#sourceError) {
state.detached = true;
self.#consumers.delete(state);
throw self.#sourceError;
const getNext = async () => {
if (self.#sourceError) {
state.detached = true;
self.#consumers.delete(state);
throw self.#sourceError;
}

// Loop until we get data, source is exhausted, or
// consumer is detached. Multiple consumers may be woken
// after a single pull - those that find no data at their
// cursor must re-pull rather than terminating prematurely.
for (;;) {
if (state.detached) {
if (self.#sourceError) throw self.#sourceError;
return { __proto__: null, done: true, value: undefined };
}

// Loop until we get data, source is exhausted, or
// consumer is detached. Multiple consumers may be woken
// after a single pull - those that find no data at their
// cursor must re-pull rather than terminating prematurely.
for (;;) {
if (state.detached) {
if (self.#sourceError) throw self.#sourceError;
return { __proto__: null, done: true, value: undefined };
}
if (self.#cancelled) {
state.detached = true;
self.#deleteConsumer(state);
return { __proto__: null, done: true, value: undefined };
}

if (self.#cancelled) {
state.detached = true;
self.#deleteConsumer(state);
return { __proto__: null, done: true, value: undefined };
// Check if data is available in buffer
const bufferIndex = state.cursor - self.#bufferStart;
if (bufferIndex < self.#buffer.length) {
const chunk = self.#buffer.get(bufferIndex);
const cursor = state.cursor;
state.cursor++;
if (cursor === self.#cachedMinCursor &&
--self.#cachedMinCursorConsumers === 0) {
self.#tryTrimBuffer();
}
return { __proto__: null, done: false, value: chunk };
}

// Check if data is available in buffer
const bufferIndex = state.cursor - self.#bufferStart;
if (bufferIndex < self.#buffer.length) {
const chunk = self.#buffer.get(bufferIndex);
const cursor = state.cursor;
state.cursor++;
if (cursor === self.#cachedMinCursor &&
--self.#cachedMinCursorConsumers === 0) {
self.#tryTrimBuffer();
}
return { __proto__: null, done: false, value: chunk };
}
if (self.#sourceExhausted) {
state.detached = true;
self.#deleteConsumer(state);
if (self.#sourceError) throw self.#sourceError;
return { __proto__: null, done: true, value: undefined };
}

if (self.#sourceExhausted) {
state.detached = true;
self.#deleteConsumer(state);
if (self.#sourceError) throw self.#sourceError;
return { __proto__: null, done: true, value: undefined };
}
// Need to pull from source - check buffer limit
const canPull = await self.#waitForBufferSpace();
if (!canPull) {
state.detached = true;
self.#deleteConsumer(state);
if (self.#sourceError) throw self.#sourceError;
return { __proto__: null, done: true, value: undefined };
}

// Need to pull from source - check buffer limit
const canPull = await self.#waitForBufferSpace();
if (!canPull) {
state.detached = true;
self.#deleteConsumer(state);
if (self.#sourceError) throw self.#sourceError;
return { __proto__: null, done: true, value: undefined };
}
await self.#pullFromSource();
}
};

await self.#pullFromSource();
}
return {
__proto__: null,
next() {
const next = PromisePrototypeThen(
state.pendingNext,
getNext,
getNext);
state.pendingNext =
PromisePrototypeThen(next, undefined, () => {});
return next;
},

async return() {
Expand Down
5 changes: 0 additions & 5 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -1033,11 +1033,6 @@
'@rpath/lib<(node_core_target_name).<(shlib_suffix)'
},
}],
[ 'node_use_node_code_cache=="true"', {
'defines': [
'NODE_USE_NODE_CODE_CACHE=1',
],
}],
['node_shared=="true" and OS in "aix os400"', {
'product_name': 'node_base',
}],
Expand Down
8 changes: 8 additions & 0 deletions src/quic/bindingdata.cc
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,14 @@ QUIC_JS_CALLBACKS(V)

#undef V

Local<String> BindingData::error_name_string(const char* name) {
auto& slot = error_name_strings_[name];
if (slot.IsEmpty()) {
slot.Set(env()->isolate(), OneByteString(env()->isolate(), name));
}
return slot.Get(env()->isolate());
}

JS_METHOD_IMPL(BindingData::SetCallbacks) {
auto env = Environment::GetCurrent(args);
auto isolate = env->isolate();
Expand Down
5 changes: 5 additions & 0 deletions src/quic/bindingdata.h
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,8 @@ class BindingData final

std::unordered_map<Endpoint*, BaseObjectPtr<BaseObject>> listening_endpoints;

v8::Local<v8::String> error_name_string(const char* name);

size_t current_ngtcp2_memory_ = 0;

// The following set up various storage and accessors for common strings,
Expand Down Expand Up @@ -357,6 +359,9 @@ class BindingData final
QUIC_JS_CALLBACKS(V)
#undef V

// Lazy cache backing error_name_string()
std::unordered_map<const char*, v8::Eternal<v8::String>> error_name_strings_;

std::unique_ptr<SessionManager> session_manager_;

// Type-erased arena storage. The concrete AliasedStructArena<T> types
Expand Down
Loading
Loading