diff --git a/src/bun.js/api/bun/h2_frame_parser.zig b/src/bun.js/api/bun/h2_frame_parser.zig index 7a9f04786c..856d82b853 100644 --- a/src/bun.js/api/bun/h2_frame_parser.zig +++ b/src/bun.js/api/bun/h2_frame_parser.zig @@ -14,16 +14,19 @@ pub fn getHTTP2CommonString(globalObject: *jsc.JSGlobalObject, hpack_index: u32) if (value.isEmptyOrUndefinedOrNull()) return null; return value; } + const MAX_WINDOW_SIZE = std.math.maxInt(i32); const MAX_HEADER_TABLE_SIZE = std.math.maxInt(u32); const MAX_STREAM_ID = std.math.maxInt(i32); const MAX_FRAME_SIZE = std.math.maxInt(u24); const DEFAULT_WINDOW_SIZE = std.math.maxInt(u16); + const PaddingStrategy = enum { none, aligned, max, }; + const FrameType = enum(u8) { HTTP_FRAME_DATA = 0x00, HTTP_FRAME_HEADERS = 0x01, @@ -42,16 +45,19 @@ const FrameType = enum(u8) { const PingFrameFlags = enum(u8) { ACK = 0x1, }; + const DataFrameFlags = enum(u8) { END_STREAM = 0x1, PADDED = 0x8, }; + const HeadersFrameFlags = enum(u8) { END_STREAM = 0x1, END_HEADERS = 0x4, PADDED = 0x8, PRIORITY = 0x20, }; + const SettingsFlags = enum(u8) { ACK = 0x1, }; @@ -676,6 +682,7 @@ pub const H2FrameParser = struct { paddingStrategy: PaddingStrategy = .none, threadlocal var shared_request_buffer: [16384]u8 = undefined; + /// The streams hashmap may mutate when growing we use this when we need to make sure its safe to iterate over it pub const StreamResumableIterator = struct { parser: *H2FrameParser, @@ -696,11 +703,13 @@ pub const H2FrameParser = struct { return null; } }; + pub const FlushState = enum { no_action, flushed, backpressure, }; + const Stream = struct { id: u32 = 0, state: enum(u8) { @@ -1454,11 +1463,13 @@ pub const H2FrameParser = struct { value.ensureStillAlive(); return this.handlers.callEventHandlerWithResult(event, this_value, &[_]jsc.JSValue{ ctx_value, value }); } + pub fn dispatchWriteCallback(this: *H2FrameParser, callback: jsc.JSValue) void { jsc.markBinding(@src()); _ = this.handlers.callWriteCallback(callback, &[_]jsc.JSValue{}); } + pub fn dispatchWithExtra(this: *H2FrameParser, comptime event: js.gc, value: jsc.JSValue, extra: jsc.JSValue) void { jsc.markBinding(@src()); @@ -1479,6 +1490,7 @@ pub const H2FrameParser = struct { extra2.ensureStillAlive(); _ = this.handlers.callEventHandler(event, this_value, ctx_value, &[_]jsc.JSValue{ ctx_value, value, extra, extra2 }); } + pub fn dispatchWith3Extra(this: *H2FrameParser, comptime event: js.gc, value: jsc.JSValue, extra: jsc.JSValue, extra2: jsc.JSValue, extra3: jsc.JSValue) void { jsc.markBinding(@src()); @@ -1490,6 +1502,7 @@ pub const H2FrameParser = struct { extra3.ensureStillAlive(); _ = this.handlers.callEventHandler(event, this_value, ctx_value, &[_]jsc.JSValue{ ctx_value, value, extra, extra2, extra3 }); } + fn cork(this: *H2FrameParser) void { if (CORKED_H2) |corked| { if (@intFromPtr(corked) == @intFromPtr(this)) { @@ -1590,6 +1603,7 @@ pub const H2FrameParser = struct { } return true; } + /// be sure that we dont have any backpressure/data queued on writerBuffer before calling this fn flushStreamQueue(this: *H2FrameParser) usize { log("flushStreamQueue {}", .{this.outboundQueueSize}); @@ -1719,6 +1733,7 @@ pub const H2FrameParser = struct { this.ref(); AutoFlusher.registerDeferredMicrotaskWithTypeUnchecked(H2FrameParser, this, this.globalThis.bunVM()); } + fn unregisterAutoFlush(this: *H2FrameParser) void { if (!this.auto_flusher.registered) return; AutoFlusher.unregisterDeferredMicrotaskWithTypeUnchecked(H2FrameParser, this, this.globalThis.bunVM()); @@ -1996,6 +2011,7 @@ pub const H2FrameParser = struct { return end; } + pub fn handleGoAwayFrame(this: *H2FrameParser, frame: FrameHeader, data: []const u8, stream_: ?*Stream) usize { log("handleGoAwayFrame {} {s}", .{ frame.streamIdentifier, data }); if (stream_ != null) { @@ -2080,6 +2096,7 @@ pub const H2FrameParser = struct { } return data.len; } + pub fn handleAltsvcFrame(this: *H2FrameParser, frame: FrameHeader, data: []const u8, stream_: ?*Stream) bun.JSError!usize { log("handleAltsvcFrame {s}", .{data}); if (this.isServer) { @@ -2114,6 +2131,7 @@ pub const H2FrameParser = struct { } return data.len; } + pub fn handleRSTStreamFrame(this: *H2FrameParser, frame: FrameHeader, data: []const u8, stream_: ?*Stream) usize { log("handleRSTStreamFrame {s}", .{data}); var stream = stream_ orelse { @@ -2149,6 +2167,7 @@ pub const H2FrameParser = struct { } return data.len; } + pub fn handlePingFrame(this: *H2FrameParser, frame: FrameHeader, data: []const u8, stream_: ?*Stream) usize { if (stream_ != null) { this.sendGoAway(frame.streamIdentifier, ErrorCode.PROTOCOL_ERROR, "Ping frame on stream", this.lastStreamID, true); @@ -2177,6 +2196,7 @@ pub const H2FrameParser = struct { } return data.len; } + pub fn handlePriorityFrame(this: *H2FrameParser, frame: FrameHeader, data: []const u8, stream_: ?*Stream) usize { var stream = stream_ orelse { this.sendGoAway(frame.streamIdentifier, ErrorCode.PROTOCOL_ERROR, "Priority frame on connection stream", this.lastStreamID, true); @@ -2208,6 +2228,7 @@ pub const H2FrameParser = struct { } return data.len; } + pub fn handleContinuationFrame(this: *H2FrameParser, frame: FrameHeader, data: []const u8, stream_: ?*Stream) bun.JSError!usize { log("handleContinuationFrame", .{}); var stream = stream_ orelse { @@ -2315,6 +2336,7 @@ pub const H2FrameParser = struct { // needs more data return data.len; } + pub fn handleSettingsFrame(this: *H2FrameParser, frame: FrameHeader, data: []const u8) usize { const isACK = frame.flags & @intFromEnum(SettingsFlags.ACK) != 0; @@ -2727,6 +2749,7 @@ pub const H2FrameParser = struct { result.put(globalObject, jsc.ZigString.static("outboundQueueSize"), jsc.JSValue.jsNumber(this.outboundQueueSize)); return result; } + pub fn goaway(this: *H2FrameParser, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { jsc.markBinding(@src()); const args_list = callframe.arguments_old(3); @@ -2984,6 +3007,7 @@ pub const H2FrameParser = struct { // closed with cancel = aborted return jsc.JSValue.jsBoolean(stream.state == .CLOSED and stream.rstCode == @intFromEnum(ErrorCode.CANCEL)); } + pub fn getStreamState(this: *H2FrameParser, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { jsc.markBinding(@src()); const args_list = callframe.arguments_old(1); @@ -3113,6 +3137,7 @@ pub const H2FrameParser = struct { } return .true; } + pub fn rstStream(this: *H2FrameParser, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { log("rstStream", .{}); jsc.markBinding(@src()); @@ -3160,10 +3185,12 @@ pub const H2FrameParser = struct { return data.len; } }; + // get memory usage in MB fn getSessionMemoryUsage(this: *H2FrameParser) usize { return (this.writeBuffer.len + this.queuedDataSize) / 1024 / 1024; } + // get memory in bytes pub fn getBufferSize(this: *H2FrameParser, _: *jsc.JSGlobalObject, _: *jsc.CallFrame) bun.JSError!JSValue { jsc.markBinding(@src()); @@ -3266,6 +3293,7 @@ pub const H2FrameParser = struct { } } } + pub fn noTrailers(this: *H2FrameParser, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { jsc.markBinding(@src()); const args_list = callframe.arguments_old(1); @@ -3302,6 +3330,7 @@ pub const H2FrameParser = struct { this.dispatchWithExtra(.onStreamEnd, identifier, jsc.JSValue.jsNumber(@intFromEnum(stream.state))); return .js_undefined; } + /// validate header name and convert to lowecase if needed fn toValidHeaderName(in: []const u8, out: []u8) ![]const u8 { var in_slice = in; @@ -3522,6 +3551,7 @@ pub const H2FrameParser = struct { this.dispatchWithExtra(.onStreamEnd, identifier, jsc.JSValue.jsNumber(@intFromEnum(stream.state))); return .js_undefined; } + pub fn writeStream(this: *H2FrameParser, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { jsc.markBinding(@src()); const args = callframe.argumentsUndef(5); @@ -4412,6 +4442,7 @@ pub const H2FrameParser = struct { } return this; } + pub fn detachFromJS(this: *H2FrameParser, _: *jsc.JSGlobalObject, _: *jsc.CallFrame) bun.JSError!JSValue { jsc.markBinding(@src()); var it = this.streams.valueIterator(); @@ -4425,6 +4456,7 @@ pub const H2FrameParser = struct { } return .js_undefined; } + /// be careful when calling detach be sure that the socket is closed and the parser not accesible anymore /// this function can be called multiple times, it will erase stream info pub fn detach(this: *H2FrameParser) void { diff --git a/src/bun.js/bindings/ErrorCode.cpp b/src/bun.js/bindings/ErrorCode.cpp index 45072ae714..633f4e1143 100644 --- a/src/bun.js/bindings/ErrorCode.cpp +++ b/src/bun.js/bindings/ErrorCode.cpp @@ -2501,6 +2501,20 @@ JSC_DEFINE_HOST_FUNCTION(Bun::jsFunctionMakeErrorWithCode, (JSC::JSGlobalObject return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_VM_MODULE_DIFFERENT_CONTEXT, "Linked modules must use the same context"_s)); case ErrorCode::ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING: return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING, "A dynamic import callback was not specified."_s)); + case ErrorCode::ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS, "The ALPNCallback and ALPNProtocols TLS options are mutually exclusive"_s)); + case ErrorCode::ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS, "Number of custom settings exceeds MAX_ADDITIONAL_SETTINGS"_s)); + case ErrorCode::ERR_HTTP2_CONNECT_AUTHORITY: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_HTTP2_CONNECT_AUTHORITY, ":authority header is required for CONNECT requests"_s)); + case ErrorCode::ERR_HTTP2_CONNECT_SCHEME: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_HTTP2_CONNECT_SCHEME, "The :scheme header is forbidden for CONNECT requests"_s)); + case ErrorCode::ERR_HTTP2_CONNECT_PATH: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_HTTP2_CONNECT_PATH, "The :path header is forbidden for CONNECT requests"_s)); + case ErrorCode::ERR_HTTP2_TOO_MANY_INVALID_FRAMES: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_HTTP2_TOO_MANY_INVALID_FRAMES, "Too many invalid HTTP/2 frames"_s)); + case ErrorCode::ERR_HTTP2_PING_CANCEL: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_HTTP2_PING_CANCEL, "HTTP2 ping cancelled"_s)); default: { break; diff --git a/src/bun.js/bindings/ErrorCode.ts b/src/bun.js/bindings/ErrorCode.ts index da2b39521a..37a9ce660b 100644 --- a/src/bun.js/bindings/ErrorCode.ts +++ b/src/bun.js/bindings/ErrorCode.ts @@ -90,6 +90,9 @@ const errors: ErrorCodeMapping = [ ["ERR_HTTP_SOCKET_ASSIGNED", Error], ["ERR_HTTP2_ALTSVC_INVALID_ORIGIN", TypeError], ["ERR_HTTP2_ALTSVC_LENGTH", TypeError], + ["ERR_HTTP2_CONNECT_AUTHORITY", Error], + ["ERR_HTTP2_CONNECT_SCHEME", Error], + ["ERR_HTTP2_CONNECT_PATH", Error], ["ERR_HTTP2_ERROR", Error], ["ERR_HTTP2_HEADER_SINGLE_VALUE", TypeError], ["ERR_HTTP2_HEADERS_AFTER_RESPOND", Error], @@ -120,6 +123,7 @@ const errors: ErrorCodeMapping = [ ["ERR_HTTP2_TRAILERS_ALREADY_SENT", Error], ["ERR_HTTP2_TRAILERS_NOT_READY", Error], ["ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS", Error], + ["ERR_HTTP2_TOO_MANY_INVALID_FRAMES", Error], ["ERR_HTTP2_UNSUPPORTED_PROTOCOL", Error], ["ERR_HTTP2_INVALID_SETTING_VALUE", TypeError, "TypeError", RangeError], ["ERR_ILLEGAL_CONSTRUCTOR", TypeError], @@ -251,6 +255,7 @@ const errors: ErrorCodeMapping = [ ["ERR_TLS_PSK_SET_IDENTITY_HINT_FAILED", Error], ["ERR_TLS_RENEGOTIATION_DISABLED", Error], ["ERR_TLS_SNI_FROM_SERVER", Error], + ["ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS", TypeError], ["ERR_SSL_NO_CIPHER_MATCH", Error], ["ERR_UNAVAILABLE_DURING_EXIT", Error], ["ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET", Error], diff --git a/src/js/builtins.d.ts b/src/js/builtins.d.ts index 160ccdf6f8..570922df8c 100644 --- a/src/js/builtins.d.ts +++ b/src/js/builtins.d.ts @@ -830,6 +830,13 @@ declare function $ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA(): Error; declare function $ERR_VM_MODULE_NOT_MODULE(): Error; declare function $ERR_VM_MODULE_DIFFERENT_CONTEXT(): Error; declare function $ERR_VM_MODULE_LINK_FAILURE(message: string, cause: Error): Error; +declare function $ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS(): TypeError; +declare function $ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS(): Error; +declare function $ERR_HTTP2_CONNECT_AUTHORITY(): Error; +declare function $ERR_HTTP2_CONNECT_SCHEME(): Error; +declare function $ERR_HTTP2_CONNECT_PATH(): Error; +declare function $ERR_HTTP2_TOO_MANY_INVALID_FRAMES(): Error; +declare function $ERR_HTTP2_PING_CANCEL(): Error; /** * Convert a function to a class-like object. diff --git a/src/js/node/http2.ts b/src/js/node/http2.ts index b418db36fc..162e3095b1 100644 --- a/src/js/node/http2.ts +++ b/src/js/node/http2.ts @@ -52,7 +52,6 @@ const TLSSocket = tls.TLSSocket; const Socket = net.Socket; const EventEmitter = require("node:events"); const { Duplex } = Stream; - const { SafeArrayIterator, SafeSet } = require("internal/primordials"); const RegExpPrototypeExec = RegExp.prototype.exec; @@ -2725,11 +2724,9 @@ class ServerHttp2Session extends Http2Session { return -1; }, }; - #onRead(data: Buffer) { this.#parser?.read(data); } - #onClose() { const parser = this.#parser; if (parser) { @@ -2739,11 +2736,9 @@ class ServerHttp2Session extends Http2Session { } this.close(); } - #onError(error: Error) { this.destroy(error); } - #onTimeout() { const parser = this.#parser; if (parser) { @@ -2751,14 +2746,12 @@ class ServerHttp2Session extends Http2Session { } this.emit("timeout"); } - #onDrain() { const parser = this.#parser; if (parser) { parser.flush(); } } - altsvc(alt: string, originOrStream) { const MAX_LENGTH = 16382; const parser = this.#parser; diff --git a/test/js/node/test/common/index.js b/test/js/node/test/common/index.js index af723f1d50..80cc212fd2 100644 --- a/test/js/node/test/common/index.js +++ b/test/js/node/test/common/index.js @@ -574,8 +574,7 @@ function canCreateSymLink() { function getCallSite(top) { const originalStackFormatter = Error.prepareStackTrace; - Error.prepareStackTrace = (err, stack) => - `${stack[0].getFileName()}:${stack[0].getLineNumber()}`; + Error.prepareStackTrace = (err, stack) => `${stack[0].getFileName()}:${stack[0].getLineNumber()}:${stack[0].getColumnNumber()}`; const err = new Error(); Error.captureStackTrace(err, top); // With the V8 Error API, the stack is not formatted until it is accessed