feat(node:http2) Implement HTTP2 server support (#14286)

Co-authored-by: cirospaciari <cirospaciari@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
This commit is contained in:
Ciro Spaciari
2024-10-15 16:28:21 -07:00
committed by GitHub
parent d15eadaa2c
commit 409e674526
128 changed files with 18727 additions and 2652 deletions

View File

@@ -52,6 +52,12 @@ pub fn BabyList(comptime Type: type) type {
this.* = .{};
}
pub fn shrinkAndFree(this: *@This(), allocator: std.mem.Allocator, size: usize) void {
var list_ = this.listManaged(allocator);
list_.shrinkAndFree(size);
this.update(list_);
}
pub fn orderedRemove(this: *@This(), index: usize) Type {
var l = this.list();
defer this.update(l);

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,8 @@ const lshpack_header = extern struct {
name_len: usize = 0,
value: [*]const u8 = undefined,
value_len: usize = 0,
never_index: bool = false,
hpack_index: u16 = 255,
};
/// wrapper implemented at src/bun.js/bindings/c-bindings.cpp
@@ -16,6 +18,8 @@ pub const HPACK = extern struct {
pub const DecodeResult = struct {
name: []const u8,
value: []const u8,
never_index: bool,
well_know: u16,
// offset of the next header position in src
next: usize,
};
@@ -37,6 +41,8 @@ pub const HPACK = extern struct {
.name = header.name[0..header.name_len],
.value = header.value[0..header.value_len],
.next = offset,
.never_index = header.never_index,
.well_know = header.hpack_index,
};
}

View File

@@ -19,6 +19,7 @@ const BoringSSL = bun.BoringSSL;
const X509 = @import("./x509.zig");
const Async = bun.Async;
const uv = bun.windows.libuv;
const H2FrameParser = @import("./h2_frame_parser.zig").H2FrameParser;
noinline fn getSSLException(globalThis: *JSC.JSGlobalObject, defaultMessage: []const u8) JSValue {
var zig_str: ZigString = ZigString.init("");
var output_buf: [4096]u8 = undefined;
@@ -1309,7 +1310,6 @@ fn selectALPNCallback(
return BoringSSL.SSL_TLSEXT_ERR_NOACK;
}
}
fn NewSocket(comptime ssl: bool) type {
return struct {
pub const Socket = uws.NewSocketHandler(ssl);
@@ -1328,13 +1328,42 @@ fn NewSocket(comptime ssl: bool) type {
connection: ?Listener.UnixOrHost = null,
protos: ?[]const u8,
server_name: ?[]const u8 = null,
bytesWritten: u64 = 0,
// TODO: switch to something that uses `visitAggregate` and have the
// `Listener` keep a list of all the sockets JSValue in there
// This is wasteful because it means we are keeping a JSC::Weak for every single open socket
has_pending_activity: std.atomic.Value(bool) = std.atomic.Value(bool).init(true),
native_callback: NativeCallbacks = .none,
pub usingnamespace bun.NewRefCounted(@This(), @This().deinit);
pub const DEBUG_REFCOUNT_NAME = "Socket";
// We use this direct callbacks on HTTP2 when available
pub const NativeCallbacks = union(enum) {
h2: *H2FrameParser,
none,
pub fn onData(this: NativeCallbacks, data: []const u8) bool {
switch (this) {
.h2 => |h2| {
h2.onNativeRead(data);
return true;
},
.none => return false,
}
}
pub fn onWritable(this: NativeCallbacks) bool {
switch (this) {
.h2 => |h2| {
h2.onNativeWritable();
return true;
},
.none => return false,
}
}
};
const This = @This();
const log = Output.scoped(.Socket, false);
const WriteResult = union(enum) {
@@ -1362,6 +1391,29 @@ fn NewSocket(comptime ssl: bool) type {
return this.has_pending_activity.load(.acquire);
}
pub fn attachNativeCallback(this: *This, callback: NativeCallbacks) bool {
if (this.native_callback != .none) return false;
this.native_callback = callback;
switch (callback) {
.h2 => |h2| h2.ref(),
.none => {},
}
return true;
}
pub fn detachNativeCallback(this: *This) void {
const native_callback = this.native_callback;
this.native_callback = .none;
switch (native_callback) {
.h2 => |h2| {
h2.onNativeClose();
h2.deref();
},
.none => {},
}
}
pub fn doConnect(this: *This, connection: Listener.UnixOrHost) !void {
bun.assert(this.socket_context != null);
this.ref();
@@ -1418,6 +1470,7 @@ fn NewSocket(comptime ssl: bool) type {
JSC.markBinding(@src());
log("onWritable", .{});
if (this.socket.isDetached()) return;
if (this.native_callback.onWritable()) return;
const handlers = this.handlers;
const callback = handlers.onWritable;
if (callback == .zero) return;
@@ -1549,6 +1602,8 @@ fn NewSocket(comptime ssl: bool) type {
pub fn closeAndDetach(this: *This, code: uws.CloseCode) void {
const socket = this.socket;
this.socket.detach();
this.detachNativeCallback();
socket.close(code);
}
@@ -1780,6 +1835,7 @@ fn NewSocket(comptime ssl: bool) type {
pub fn onClose(this: *This, _: Socket, err: c_int, _: ?*anyopaque) void {
JSC.markBinding(@src());
log("onClose", .{});
this.detachNativeCallback();
this.socket.detach();
defer this.deref();
defer this.markInactive();
@@ -1821,6 +1877,8 @@ fn NewSocket(comptime ssl: bool) type {
log("onData({d})", .{data.len});
if (this.socket.isDetached()) return;
if (this.native_callback.onData(data)) return;
const handlers = this.handlers;
const callback = handlers.onData;
if (callback == .zero or this.flags.finalizing) return;
@@ -2015,7 +2073,7 @@ fn NewSocket(comptime ssl: bool) type {
return ZigString.init(text).toJS(globalThis);
}
fn writeMaybeCorked(this: *This, buffer: []const u8, is_end: bool) i32 {
pub fn writeMaybeCorked(this: *This, buffer: []const u8, is_end: bool) i32 {
if (this.socket.isShutdown() or this.socket.isClosed()) {
return -1;
}
@@ -2025,12 +2083,18 @@ fn NewSocket(comptime ssl: bool) type {
// TLS wrapped but in TCP mode
if (this.wrapped == .tcp) {
const res = this.socket.rawWrite(buffer, is_end);
if (res > 0) {
this.bytesWritten += @intCast(res);
}
log("write({d}, {any}) = {d}", .{ buffer.len, is_end, res });
return res;
}
}
const res = this.socket.write(buffer, is_end);
if (res > 0) {
this.bytesWritten += @intCast(res);
}
log("write({d}, {any}) = {d}", .{ buffer.len, is_end, res });
return res;
}
@@ -2261,6 +2325,7 @@ fn NewSocket(comptime ssl: bool) type {
pub fn deinit(this: *This) void {
this.markInactive();
this.detachNativeCallback();
this.poll_ref.unref(JSC.VirtualMachine.get());
// need to deinit event without being attached
@@ -2499,7 +2564,12 @@ fn NewSocket(comptime ssl: bool) type {
bun.assert(result_size == size);
return buffer;
}
pub fn getBytesWritten(
this: *This,
_: *JSC.JSGlobalObject,
) JSValue {
return JSC.JSValue.jsNumber(this.bytesWritten);
}
pub fn getALPNProtocol(
this: *This,
globalObject: *JSC.JSGlobalObject,
@@ -3322,6 +3392,7 @@ fn NewSocket(comptime ssl: bool) type {
defer this.deref();
// detach and invalidate the old instance
this.detachNativeCallback();
this.socket.detach();
// start TLS handshake after we set extension on the socket

View File

@@ -9,6 +9,10 @@ export default [
fn: "request",
length: 2,
},
setNativeSocket: {
fn: "setNativeSocketFromJS",
length: 1,
},
ping: {
fn: "ping",
length: 0,
@@ -29,6 +33,10 @@ export default [
fn: "read",
length: 1,
},
flush: {
fn: "flushFromJS",
length: 0,
},
rstStream: {
fn: "rstStream",
length: 1,
@@ -41,12 +49,20 @@ export default [
fn: "sendTrailers",
length: 2,
},
noTrailers: {
fn: "noTrailers",
length: 1,
},
setStreamPriority: {
fn: "setStreamPriority",
length: 2,
},
setEndAfterHeaders: {
fn: "setEndAfterHeaders",
getStreamContext: {
fn: "getStreamContext",
length: 1,
},
setStreamContext: {
fn: "setStreamContext",
length: 2,
},
getEndAfterHeaders: {
@@ -61,6 +77,26 @@ export default [
fn: "getStreamState",
length: 1,
},
bufferSize: {
fn: "getBufferSize",
length: 0,
},
hasNativeRead: {
fn: "hasNativeRead",
length: 1,
},
getAllStreams: {
fn: "getAllStreams",
length: 0,
},
emitErrorToAllStreams: {
fn: "emitErrorToAllStreams",
length: 1,
},
getNextStream: {
fn: "getNextStream",
length: 0,
},
},
finalize: true,
construct: true,

View File

@@ -83,6 +83,9 @@ function generate(ssl) {
alpnProtocol: {
getter: "getALPNProtocol",
},
bytesWritten: {
getter: "getBytesWritten",
},
write: {
fn: "write",
length: 3,

View File

@@ -0,0 +1,37 @@
#include "root.h"
#include "BunHttp2CommonStrings.h"
#include <JavaScriptCore/JSString.h>
#include <JavaScriptCore/JSGlobalObject.h>
#include <JavaScriptCore/LazyProperty.h>
#include <JavaScriptCore/LazyPropertyInlines.h>
#include "ZigGlobalObject.h"
#include <JavaScriptCore/SlotVisitorInlines.h>
#include <JavaScriptCore/VMTrapsInlines.h>
namespace Bun {
using namespace JSC;
#define HTTP2_COMMON_STRINGS_LAZY_PROPERTY_DEFINITION(jsName, key, value, idx) \
this->m_names[idx].initLater( \
[](const JSC::LazyProperty<JSGlobalObject, JSString>::Initializer& init) { \
init.set(jsOwnedString(init.vm, key)); \
});
#define HTTP2_COMMON_STRINGS_LAZY_PROPERTY_VISITOR(name, key, value, idx) \
this->m_names[idx].visit(visitor);
void Http2CommonStrings::initialize()
{
HTTP2_COMMON_STRINGS_EACH_NAME(HTTP2_COMMON_STRINGS_LAZY_PROPERTY_DEFINITION)
}
template<typename Visitor>
void Http2CommonStrings::visit(Visitor& visitor)
{
HTTP2_COMMON_STRINGS_EACH_NAME(HTTP2_COMMON_STRINGS_LAZY_PROPERTY_VISITOR)
}
template void Http2CommonStrings::visit(JSC::AbstractSlotVisitor&);
template void Http2CommonStrings::visit(JSC::SlotVisitor&);
} // namespace Bun

View File

@@ -0,0 +1,107 @@
#pragma once
// clang-format off
#define HTTP2_COMMON_STRINGS_EACH_NAME(macro) \
macro(authority, ":authority"_s, ""_s, 0) \
macro(methodGet, ":method"_s, "GET"_s, 1) \
macro(methodPost, ":method"_s, "POST"_s, 2) \
macro(pathRoot, ":path"_s, "/"_s, 3) \
macro(pathIndex, ":path"_s, "/index.html"_s, 4) \
macro(schemeHttp, ":scheme"_s, "http"_s, 5) \
macro(schemeHttps, ":scheme"_s, "https"_s, 6) \
macro(status200, ":status"_s, "200"_s, 7) \
macro(status204, ":status"_s, "204"_s, 8) \
macro(status206, ":status"_s, "206"_s, 9) \
macro(status304, ":status"_s, "304"_s, 10) \
macro(status400, ":status"_s, "400"_s, 11) \
macro(status404, ":status"_s, "404"_s, 12) \
macro(status500, ":status"_s, "500"_s, 13) \
macro(acceptCharset, "accept-charset"_s, ""_s, 14) \
macro(acceptEncoding, "accept-encoding"_s, "gzip, deflate"_s, 15) \
macro(acceptLanguage, "accept-language"_s, ""_s, 16) \
macro(acceptRanges, "accept-ranges"_s, ""_s, 17) \
macro(accept, "accept"_s, ""_s, 18) \
macro(accessControlAllowOrigin, "access-control-allow-origin"_s, ""_s, 19) \
macro(age, "age"_s, ""_s, 20) \
macro(allow, "allow"_s, ""_s, 21) \
macro(authorization, "authorization"_s, ""_s, 22) \
macro(cacheControl, "cache-control"_s, ""_s, 23) \
macro(contentDisposition, "content-disposition"_s, ""_s, 24) \
macro(contentEncoding, "content-encoding"_s, ""_s, 25) \
macro(contentLanguage, "content-language"_s, ""_s, 26) \
macro(contentLength, "content-length"_s, ""_s, 27) \
macro(contentLocation, "content-location"_s, ""_s, 28) \
macro(contentRange, "content-range"_s, ""_s, 29) \
macro(contentType, "content-type"_s, ""_s, 30) \
macro(cookie, "cookie"_s, ""_s, 31) \
macro(date, "date"_s, ""_s, 32) \
macro(etag, "etag"_s, ""_s, 33) \
macro(expect, "expect"_s, ""_s, 34) \
macro(expires, "expires"_s, ""_s, 35) \
macro(from, "from"_s, ""_s, 36) \
macro(host, "host"_s, ""_s, 37) \
macro(ifMatch, "if-match"_s, ""_s, 38) \
macro(ifModifiedSince, "if-modified-since"_s, ""_s, 39) \
macro(ifNoneMatch, "if-none-match"_s, ""_s, 40) \
macro(ifRange, "if-range"_s, ""_s, 41) \
macro(ifUnmodifiedSince, "if-unmodified-since"_s, ""_s, 42) \
macro(lastModified, "last-modified"_s, ""_s, 43) \
macro(link, "link"_s, ""_s, 44) \
macro(location, "location"_s, ""_s, 45) \
macro(maxForwards, "max-forwards"_s, ""_s, 46) \
macro(proxyAuthenticate, "proxy-authenticate"_s, ""_s, 47) \
macro(proxyAuthorization, "proxy-authorization"_s, ""_s, 48) \
macro(range, "range"_s, ""_s, 49) \
macro(referer, "referer"_s, ""_s, 50) \
macro(refresh, "refresh"_s, ""_s, 51) \
macro(retryAfter, "retry-after"_s, ""_s, 52) \
macro(server, "server"_s, ""_s, 53) \
macro(setCookie, "set-cookie"_s, ""_s, 54) \
macro(strictTransportSecurity, "strict-transport-security"_s, ""_s, 55) \
macro(transferEncoding, "transfer-encoding"_s, ""_s, 56) \
macro(userAgent, "user-agent"_s, ""_s, 57) \
macro(vary, "vary"_s, ""_s, 58) \
macro(via, "via"_s, ""_s, 59) \
macro(wwwAuthenticate, "www-authenticate"_s, ""_s, 60)
// clang-format on
#define HTTP2_COMMON_STRINGS_ACCESSOR_DEFINITION(name, key, value, idx) \
JSC::JSString* name##String(JSC::JSGlobalObject* globalObject) \
{ \
return m_names[idx].getInitializedOnMainThread(globalObject); \
}
namespace Bun {
using namespace JSC;
class Http2CommonStrings {
public:
typedef JSC::JSString* (*commonStringInitializer)(Http2CommonStrings*, JSC::JSGlobalObject* globalObject);
HTTP2_COMMON_STRINGS_EACH_NAME(HTTP2_COMMON_STRINGS_ACCESSOR_DEFINITION)
void initialize();
template<typename Visitor>
void visit(Visitor& visitor);
JSC::JSString* getStringFromHPackIndex(uint16_t index, JSC::JSGlobalObject* globalObject)
{
if (index > 60) {
return nullptr;
}
return m_names[index].getInitializedOnMainThread(globalObject);
}
private:
JSC::LazyProperty<JSC::JSGlobalObject, JSC::JSString> m_names[61];
};
} // namespace Bun
#undef BUN_COMMON_STRINGS_ACCESSOR_DEFINITION
#undef BUN_COMMON_STRINGS_LAZY_PROPERTY_DECLARATION

View File

@@ -13,9 +13,6 @@ export default [
["ABORT_ERR", Error, "AbortError"],
["ERR_CRYPTO_INVALID_DIGEST", TypeError, "TypeError"],
["ERR_ENCODING_INVALID_ENCODED_DATA", TypeError, "TypeError"],
["ERR_HTTP2_INVALID_HEADER_VALUE", TypeError, "TypeError"],
["ERR_HTTP2_INVALID_PSEUDOHEADER", TypeError, "TypeError"],
["ERR_HTTP2_INVALID_SINGLE_VALUE_HEADER", TypeError, "TypeError"],
["ERR_INVALID_ARG_TYPE", TypeError, "TypeError"],
["ERR_INVALID_ARG_VALUE", TypeError, "TypeError"],
["ERR_INVALID_PROTOCOL", TypeError, "TypeError"],
@@ -54,4 +51,30 @@ export default [
["ERR_BODY_ALREADY_USED", Error, "Error"],
["ERR_STREAM_WRAP", Error, "Error"],
["ERR_BORINGSSL", Error, "Error"],
//HTTP2
["ERR_INVALID_HTTP_TOKEN", TypeError, "TypeError"],
["ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED", TypeError, "TypeError"],
["ERR_HTTP2_SEND_FILE", Error, "Error"],
["ERR_HTTP2_SEND_FILE_NOSEEK", Error, "Error"],
["ERR_HTTP2_HEADERS_SENT", Error, "ERR_HTTP2_HEADERS_SENT"],
["ERR_HTTP2_INFO_STATUS_NOT_ALLOWED", RangeError, "RangeError"],
["ERR_HTTP2_STATUS_INVALID", RangeError, "RangeError"],
["ERR_HTTP2_INVALID_PSEUDOHEADER", TypeError, "TypeError"],
["ERR_HTTP2_INVALID_HEADER_VALUE", TypeError, "TypeError"],
["ERR_HTTP2_PING_CANCEL", Error, "Error"],
["ERR_HTTP2_STREAM_ERROR", Error, "Error"],
["ERR_HTTP2_INVALID_SINGLE_VALUE_HEADER", TypeError, "TypeError"],
["ERR_HTTP2_SESSION_ERROR", Error, "Error"],
["ERR_HTTP2_INVALID_SESSION", Error, "Error"],
["ERR_HTTP2_INVALID_HEADERS", Error, "Error"],
["ERR_HTTP2_PING_LENGTH", RangeError, "RangeError"],
["ERR_HTTP2_INVALID_STREAM", Error, "Error"],
["ERR_HTTP2_TRAILERS_ALREADY_SENT", Error, "Error"],
["ERR_HTTP2_TRAILERS_NOT_READY", Error, "Error"],
["ERR_HTTP2_PAYLOAD_FORBIDDEN", Error, "Error"],
["ERR_HTTP2_NO_SOCKET_MANIPULATION", Error, "Error"],
["ERR_HTTP2_SOCKET_UNBOUND", Error, "Error"],
["ERR_HTTP2_ERROR", Error, "Error"],
["ERR_HTTP2_OUT_OF_STREAMS", Error, "Error"],
] as ErrorCodeMapping;

View File

@@ -172,6 +172,7 @@ using namespace Bun;
BUN_DECLARE_HOST_FUNCTION(Bun__NodeUtil__jsParseArgs);
BUN_DECLARE_HOST_FUNCTION(BUN__HTTP2__getUnpackedSettings);
BUN_DECLARE_HOST_FUNCTION(BUN__HTTP2_getPackedSettings);
BUN_DECLARE_HOST_FUNCTION(BUN__HTTP2_assertSettings);
using JSGlobalObject = JSC::JSGlobalObject;
using Exception = JSC::Exception;
@@ -2737,6 +2738,7 @@ void GlobalObject::finishCreation(VM& vm)
ASSERT(inherits(info()));
m_commonStrings.initialize();
m_http2_commongStrings.initialize();
Bun::addNodeModuleConstructorProperties(vm, this);
@@ -3607,6 +3609,15 @@ extern "C" void JSC__JSGlobalObject__drainMicrotasks(Zig::GlobalObject* globalOb
globalObject->drainMicrotasks();
}
extern "C" EncodedJSValue JSC__JSGlobalObject__getHTTP2CommonString(Zig::GlobalObject* globalObject, uint32_t hpack_index)
{
auto value = globalObject->http2CommonStrings().getStringFromHPackIndex(hpack_index, globalObject);
if (value != nullptr) {
return JSValue::encode(value);
}
return JSValue::encode(JSValue::JSUndefined);
}
template<typename Visitor>
void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor)
{
@@ -3630,6 +3641,7 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor)
thisObject->m_builtinInternalFunctions.visit(visitor);
thisObject->m_commonStrings.visit<Visitor>(visitor);
thisObject->m_http2_commongStrings.visit<Visitor>(visitor);
visitor.append(thisObject->m_assignToStream);
visitor.append(thisObject->m_readableStreamToArrayBuffer);
visitor.append(thisObject->m_readableStreamToArrayBufferResolve);

View File

@@ -51,6 +51,7 @@ class GlobalInternals;
#include "WebCoreJSBuiltins.h"
#include "headers-handwritten.h"
#include "BunCommonStrings.h"
#include "BunHttp2CommonStrings.h"
#include "BunGlobalScope.h"
namespace WebCore {
@@ -484,7 +485,7 @@ public:
JSObject* cryptoObject() const { return m_cryptoObject.getInitializedOnMainThread(this); }
JSObject* JSDOMFileConstructor() const { return m_JSDOMFileConstructor.getInitializedOnMainThread(this); }
Bun::CommonStrings& commonStrings() { return m_commonStrings; }
Bun::Http2CommonStrings& http2CommonStrings() { return m_http2_commongStrings; }
#include "ZigGeneratedClasses+lazyStructureHeader.h"
void finishCreation(JSC::VM&);
@@ -500,6 +501,7 @@ private:
Lock m_gcLock;
Ref<WebCore::DOMWrapperWorld> m_world;
Bun::CommonStrings m_commonStrings;
Bun::Http2CommonStrings m_http2_commongStrings;
RefPtr<WebCore::Performance> m_performance { nullptr };
// JSC's hashtable code-generator tries to access these properties, so we make them public.

View File

@@ -252,6 +252,8 @@ typedef struct {
size_t name_len;
const char* value;
size_t value_len;
bool never_index;
uint16_t hpack_index;
} lshpack_header;
lshpack_wrapper* lshpack_wrapper_init(lshpack_wrapper_alloc alloc, lshpack_wrapper_free free, unsigned max_capacity)
@@ -310,6 +312,12 @@ size_t lshpack_wrapper_decode(lshpack_wrapper* self,
output->name_len = hdr.name_len;
output->value = lsxpack_header_get_value(&hdr);
output->value_len = hdr.val_len;
output->never_index = (hdr.flags & LSXPACK_NEVER_INDEX) != 0;
if (hdr.hpack_index != LSHPACK_HDR_UNKNOWN && hdr.hpack_index <= LSHPACK_HDR_WWW_AUTHENTICATE) {
output->hpack_index = hdr.hpack_index - 1;
} else {
output->hpack_index = 255;
}
return s - src;
}

View File

@@ -878,6 +878,17 @@ pub const EventLoop = struct {
globalObject.reportActiveExceptionAsUnhandled(err);
}
pub fn runCallbackWithResult(this: *EventLoop, callback: JSC.JSValue, globalObject: *JSC.JSGlobalObject, thisValue: JSC.JSValue, arguments: []const JSC.JSValue) JSC.JSValue {
this.enter();
defer this.exit();
const result = callback.call(globalObject, thisValue, arguments) catch |err| {
globalObject.reportActiveExceptionAsUnhandled(err);
return .zero;
};
return result;
}
fn tickQueueWithCount(this: *EventLoop, virtual_machine: *VirtualMachine, comptime queue_name: []const u8) u32 {
var global = this.global;
const global_vm = global.vm();

View File

@@ -83,11 +83,14 @@ function ErrorCaptureStackTrace(targetObject) {
}
const arrayProtoPush = Array.prototype.push;
const ArrayPrototypeSymbolIterator = uncurryThis(Array.prototype[Symbol.iterator]);
const ArrayIteratorPrototypeNext = uncurryThis(ArrayPrototypeSymbolIterator.next);
export default {
makeSafe, // exported for testing
Array,
ArrayFrom: Array.from,
ArrayIsArray: Array.isArray,
SafeArrayIterator: createSafeIterator(ArrayPrototypeSymbolIterator, ArrayIteratorPrototypeNext),
ArrayPrototypeFlat: uncurryThis(Array.prototype.flat),
ArrayPrototypeFilter: uncurryThis(Array.prototype.filter),
ArrayPrototypeForEach,
@@ -169,6 +172,8 @@ export default {
}
},
),
DatePrototypeGetMilliseconds: uncurryThis(Date.prototype.getMilliseconds),
DatePrototypeToUTCString: uncurryThis(Date.prototype.toUTCString),
SetPrototypeGetSize: getGetter(Set, "size"),
SetPrototypeEntries: uncurryThis(Set.prototype.entries),
SetPrototypeValues: uncurryThis(Set.prototype.values),

View File

@@ -1,4 +1,67 @@
const { hideFromStack } = require("internal/shared");
const RegExpPrototypeExec = RegExp.prototype.exec;
const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/;
/**
* Verifies that the given val is a valid HTTP token
* per the rules defined in RFC 7230
* See https://tools.ietf.org/html/rfc7230#section-3.2.6
*/
function checkIsHttpToken(val) {
return RegExpPrototypeExec.$call(tokenRegExp, val) !== null;
}
/*
The rules for the Link header field are described here:
https://www.rfc-editor.org/rfc/rfc8288.html#section-3
This regex validates any string surrounded by angle brackets
(not necessarily a valid URI reference) followed by zero or more
link-params separated by semicolons.
*/
const linkValueRegExp = /^(?:<[^>]*>)(?:\s*;\s*[^;"\s]+(?:=(")?[^;"\s]*\1)?)*$/;
function validateLinkHeaderFormat(value, name) {
if (typeof value === "undefined" || !RegExpPrototypeExec.$call(linkValueRegExp, value)) {
throw $ERR_INVALID_ARG_VALUE(
`The arguments ${name} is invalid must be an array or string of format "</styles.css>; rel=preload; as=style"`,
);
}
}
function validateLinkHeaderValue(hints) {
if (typeof hints === "string") {
validateLinkHeaderFormat(hints, "hints");
return hints;
} else if (ArrayIsArray(hints)) {
const hintsLength = hints.length;
let result = "";
if (hintsLength === 0) {
return result;
}
for (let i = 0; i < hintsLength; i++) {
const link = hints[i];
validateLinkHeaderFormat(link, "hints");
result += link;
if (i !== hintsLength - 1) {
result += ", ";
}
}
return result;
}
throw $ERR_INVALID_ARG_VALUE(
`The arguments hints is invalid must be an array or string of format "</styles.css>; rel=preload; as=style"`,
);
}
hideFromStack(validateLinkHeaderValue);
export default {
validateLinkHeaderValue: validateLinkHeaderValue,
checkIsHttpToken: checkIsHttpToken,
/** `(value, name, min = NumberMIN_SAFE_INTEGER, max = NumberMAX_SAFE_INTEGER)` */
validateInteger: $newCppFunction("NodeValidator.cpp", "jsFunction_validateInteger", 0),
/** `(value, name, min = undefined, max)` */

View File

@@ -6,7 +6,7 @@ const { ERR_INVALID_ARG_TYPE, ERR_INVALID_PROTOCOL } = require("internal/errors"
const { isPrimary } = require("internal/cluster/isPrimary");
const { kAutoDestroyed } = require("internal/shared");
const { urlToHttpOptions } = require("internal/url");
const { validateFunction } = require("internal/validators");
const { validateFunction, checkIsHttpToken } = require("internal/validators");
const {
getHeader,
@@ -59,8 +59,7 @@ function checkInvalidHeaderChar(val: string) {
const validateHeaderName = (name, label) => {
if (typeof name !== "string" || !name || !checkIsHttpToken(name)) {
// throw new ERR_INVALID_HTTP_TOKEN(label || "Header name", name);
throw new Error("ERR_INVALID_HTTP_TOKEN");
throw $ERR_INVALID_HTTP_TOKEN(`The arguments Header name is invalid. Received ${name}`);
}
};
@@ -1767,8 +1766,7 @@ class ClientRequest extends OutgoingMessage {
if (methodIsString && method) {
if (!checkIsHttpToken(method)) {
// throw new ERR_INVALID_HTTP_TOKEN("Method", method);
throw new Error("ERR_INVALID_HTTP_TOKEN: Method");
throw $ERR_INVALID_HTTP_TOKEN("Method");
}
method = this.#method = StringPrototypeToUpperCase.$call(method);
} else {
@@ -2008,16 +2006,6 @@ function validateHost(host, name) {
return host;
}
const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/;
/**
* Verifies that the given val is a valid HTTP token
* per the rules defined in RFC 7230
* See https://tools.ietf.org/html/rfc7230#section-3.2.6
*/
function checkIsHttpToken(val) {
return RegExpPrototypeExec.$call(tokenRegExp, val) !== null;
}
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a

File diff suppressed because it is too large Load Diff

View File

@@ -175,7 +175,6 @@ const Socket = (function (InternalSocket) {
self.authorized = false;
self.authorizationError = verifyError.code || verifyError.message;
if (self._rejectUnauthorized) {
self.emit("error", verifyError);
self.destroy(verifyError);
return;
}
@@ -237,7 +236,6 @@ const Socket = (function (InternalSocket) {
const chunk = self.#writeChunk;
const written = socket.write(chunk);
self.bytesWritten += written;
if (written < chunk.length) {
self.#writeChunk = chunk.slice(written);
} else {
@@ -295,9 +293,9 @@ const Socket = (function (InternalSocket) {
this.pauseOnConnect = pauseOnConnect;
if (isTLS) {
// add secureConnection event handler
self.once("secureConnection", () => connectionListener(_socket));
self.once("secureConnection", () => connectionListener.$call(self, _socket));
} else {
connectionListener(_socket);
connectionListener.$call(self, _socket);
}
}
self.emit("connection", _socket);
@@ -351,7 +349,6 @@ const Socket = (function (InternalSocket) {
};
bytesRead = 0;
bytesWritten = 0;
#closed = false;
#ended = false;
#final_callback = null;
@@ -420,6 +417,9 @@ const Socket = (function (InternalSocket) {
this.once("connect", () => this.emit("ready"));
}
get bytesWritten() {
return this[bunSocketInternal]?.bytesWritten || 0;
}
address() {
return {
address: this.localAddress,
@@ -805,6 +805,7 @@ const Socket = (function (InternalSocket) {
_write(chunk, encoding, callback) {
if (typeof chunk == "string" && encoding !== "ascii") chunk = Buffer.from(chunk, encoding);
var written = this[bunSocketInternal]?.write(chunk);
if (written == chunk.length) {
callback();
} else if (this.#writeCallback) {
@@ -879,7 +880,7 @@ class Server extends EventEmitter {
if (typeof callback === "function") {
if (!this[bunSocketInternal]) {
this.once("close", function close() {
callback(new ERR_SERVER_NOT_RUNNING());
callback(ERR_SERVER_NOT_RUNNING());
});
} else {
this.once("close", callback);

View File

@@ -21,6 +21,7 @@ const ENABLE_LOGGING = false;
import { describe, test } from "bun:test";
import { isWindows } from "harness";
import { EventEmitter } from "events";
const Promise = globalThis.Promise;
globalThis.Promise = function (...args) {
@@ -219,6 +220,9 @@ function callAllMethods(object) {
for (const methodName of allThePropertyNames(object, callBanned)) {
try {
try {
if (object instanceof EventEmitter) {
object?.on?.("error", () => {});
}
const returnValue = wrap(Reflect.apply(object?.[methodName], object, []));
Bun.inspect?.(returnValue), queue.push(returnValue);
} catch (e) {
@@ -245,6 +249,9 @@ function callAllMethods(object) {
continue;
}
seen.add(method);
if (value instanceof EventEmitter) {
value?.on?.("error", () => {});
}
const returnValue = wrap(Reflect?.apply?.(method, value, []));
if (returnValue?.then) {
continue;

View File

@@ -1,3 +1,5 @@
import { heapStats } from "bun:jsc";
// This file is meant to be able to run in node and bun
const http2 = require("http2");
const { TLS_OPTIONS, nodeEchoServer } = require("./http2-helpers.cjs");
@@ -20,7 +22,8 @@ const sleep = dur => new Promise(resolve => setTimeout(resolve, dur));
// X iterations should be enough to detect a leak
const ITERATIONS = 20;
// lets send a bigish payload
const PAYLOAD = Buffer.from("BUN".repeat((1024 * 128) / 3));
// const PAYLOAD = Buffer.from("BUN".repeat((1024 * 128) / 3));
const PAYLOAD = Buffer.alloc(1024 * 128, "b");
const MULTIPLEX = 50;
async function main() {
@@ -84,19 +87,19 @@ async function main() {
try {
const startStats = getHeapStats();
// warm up
await runRequests(ITERATIONS);
await sleep(10);
gc(true);
// take a baseline
const baseline = process.memoryUsage.rss();
console.error("Initial memory usage", (baseline / 1024 / 1024) | 0, "MB");
// run requests
await runRequests(ITERATIONS);
await sleep(10);
gc(true);
await sleep(10);
// take an end snapshot
const end = process.memoryUsage.rss();
@@ -106,7 +109,7 @@ async function main() {
// we executed 100 requests per iteration, memory usage should not go up by 10 MB
if (deltaMegaBytes > 20) {
console.log("Too many bodies leaked", deltaMegaBytes);
console.error("Too many bodies leaked", deltaMegaBytes);
process.exit(1);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,58 @@
//#FILE: test-http2-client-priority-before-connect.js
//#SHA1: bc94924856dc82c18ccf699d467d63c28fed0d13
//-----------------
'use strict';
const h2 = require('http2');
let server;
let port;
beforeAll(async () => {
// Check if crypto is available
try {
require('crypto');
} catch (err) {
return test.skip('missing crypto');
}
});
afterAll(() => {
if (server) {
server.close();
}
});
test('HTTP2 client priority before connect', (done) => {
server = h2.createServer();
// We use the lower-level API here
server.on('stream', (stream) => {
stream.respond();
stream.end('ok');
});
server.listen(0, () => {
port = server.address().port;
const client = h2.connect(`http://localhost:${port}`);
const req = client.request();
req.priority({});
req.on('response', () => {
// Response received
});
req.resume();
req.on('end', () => {
// Request ended
});
req.on('close', () => {
client.close();
done();
});
});
});
//<#END_FILE: test-http2-client-priority-before-connect.js

View File

@@ -0,0 +1,70 @@
//#FILE: test-http2-client-request-listeners-warning.js
//#SHA1: cb4f9a71d1f670a78f989caed948e88fa5dbd681
//-----------------
"use strict";
const http2 = require("http2");
const EventEmitter = require("events");
// Skip the test if crypto is not available
let hasCrypto;
try {
require("crypto");
hasCrypto = true;
} catch (err) {
hasCrypto = false;
}
(hasCrypto ? describe : describe.skip)("HTTP2 client request listeners warning", () => {
let server;
let port;
beforeAll(done => {
server = http2.createServer();
server.on("stream", stream => {
stream.respond();
stream.end();
});
server.listen(0, () => {
port = server.address().port;
done();
});
});
afterAll(() => {
server.close();
});
test("should not emit MaxListenersExceededWarning", done => {
const warningListener = jest.fn();
process.on("warning", warningListener);
const client = http2.connect(`http://localhost:${port}`);
function request() {
return new Promise((resolve, reject) => {
const stream = client.request();
stream.on("error", reject);
stream.on("response", resolve);
stream.end();
});
}
const requests = [];
for (let i = 0; i < EventEmitter.defaultMaxListeners + 1; i++) {
requests.push(request());
}
Promise.all(requests)
.then(() => {
expect(warningListener).not.toHaveBeenCalled();
})
.finally(() => {
process.removeListener("warning", warningListener);
client.close();
done();
});
});
});
//<#END_FILE: test-http2-client-request-listeners-warning.js

View File

@@ -0,0 +1,40 @@
//#FILE: test-http2-client-shutdown-before-connect.js
//#SHA1: 75a343e9d8b577911242f867708310346fe9ddce
//-----------------
'use strict';
const h2 = require('http2');
// Skip test if crypto is not available
const hasCrypto = (() => {
try {
require('crypto');
return true;
} catch (err) {
return false;
}
})();
if (!hasCrypto) {
test.skip('missing crypto', () => {});
} else {
test('HTTP/2 client shutdown before connect', (done) => {
const server = h2.createServer();
// We use the lower-level API here
server.on('stream', () => {
throw new Error('Stream should not be created');
});
server.listen(0, () => {
const client = h2.connect(`http://localhost:${server.address().port}`);
client.close(() => {
server.close(() => {
done();
});
});
});
});
}
//<#END_FILE: test-http2-client-shutdown-before-connect.js

View File

@@ -0,0 +1,58 @@
//#FILE: test-http2-client-write-before-connect.js
//#SHA1: f38213aa6b5fb615d5b80f0213022ea06e2705cc
//-----------------
'use strict';
const h2 = require('http2');
let server;
let client;
beforeAll(() => {
if (!process.versions.openssl) {
test.skip('missing crypto');
return;
}
});
afterEach(() => {
if (client) {
client.close();
}
if (server) {
server.close();
}
});
test('HTTP/2 client write before connect', (done) => {
server = h2.createServer();
server.on('stream', (stream, headers, flags) => {
let data = '';
stream.setEncoding('utf8');
stream.on('data', (chunk) => data += chunk);
stream.on('end', () => {
expect(data).toBe('some data more data');
});
stream.respond();
stream.end('ok');
});
server.listen(0, () => {
const port = server.address().port;
client = h2.connect(`http://localhost:${port}`);
const req = client.request({ ':method': 'POST' });
req.write('some data ');
req.end('more data');
req.on('response', () => {});
req.resume();
req.on('end', () => {});
req.on('close', () => {
done();
});
});
});
//<#END_FILE: test-http2-client-write-before-connect.js

View File

@@ -0,0 +1,74 @@
//#FILE: test-http2-client-write-empty-string.js
//#SHA1: d4371ceba660942fe3c398bbb3144ce691054cec
//-----------------
'use strict';
const http2 = require('http2');
const runTest = async (chunkSequence) => {
return new Promise((resolve, reject) => {
const server = http2.createServer();
server.on('stream', (stream, headers, flags) => {
stream.respond({ 'content-type': 'text/html' });
let data = '';
stream.on('data', (chunk) => {
data += chunk.toString();
});
stream.on('end', () => {
stream.end(`"${data}"`);
});
});
server.listen(0, async () => {
const port = server.address().port;
const client = http2.connect(`http://localhost:${port}`);
const req = client.request({
':method': 'POST',
':path': '/'
});
req.on('response', (headers) => {
expect(headers[':status']).toBe(200);
expect(headers['content-type']).toBe('text/html');
});
let data = '';
req.setEncoding('utf8');
req.on('data', (d) => data += d);
req.on('end', () => {
expect(data).toBe('""');
server.close();
client.close();
resolve();
});
for (const chunk of chunkSequence) {
req.write(chunk);
}
req.end();
});
});
};
const testCases = [
[''],
['', '']
];
describe('http2 client write empty string', () => {
beforeAll(() => {
if (typeof http2 === 'undefined') {
return test.skip('http2 module not available');
}
});
testCases.forEach((chunkSequence, index) => {
it(`should handle chunk sequence ${index + 1}`, async () => {
await runTest(chunkSequence);
});
});
});
//<#END_FILE: test-http2-client-write-empty-string.js

View File

@@ -0,0 +1,55 @@
//#FILE: test-http2-compat-aborted.js
//#SHA1: 2aaf11840d98c2b8f4387473180ec86626ac48d1
//-----------------
"use strict";
const h2 = require("http2");
let server;
let port;
beforeAll(done => {
if (!process.versions.openssl) {
return test.skip("missing crypto");
}
server = h2.createServer((req, res) => {
req.on("aborted", () => {
expect(req.aborted).toBe(true);
expect(req.complete).toBe(true);
});
expect(req.aborted).toBe(false);
expect(req.complete).toBe(false);
res.write("hello");
server.close();
});
server.listen(0, () => {
port = server.address().port;
done();
});
});
afterAll(() => {
if (server) {
server.close();
}
});
test("HTTP/2 compat aborted", done => {
const url = `http://localhost:${port}`;
const client = h2.connect(url, () => {
const request = client.request();
request.on("data", chunk => {
client.destroy();
});
request.on("end", () => {
done();
});
});
client.on("error", err => {
// Ignore client errors as we're forcibly destroying the connection
});
});
//<#END_FILE: test-http2-compat-aborted.js

View File

@@ -0,0 +1,62 @@
//#FILE: test-http2-compat-client-upload-reject.js
//#SHA1: 4dff98612ac613af951070f79f07f5c1750045da
//-----------------
'use strict';
const http2 = require('http2');
const fs = require('fs');
const path = require('path');
const fixturesPath = path.resolve(__dirname, '..', 'fixtures');
const loc = path.join(fixturesPath, 'person-large.jpg');
let server;
let client;
beforeAll(() => {
if (!process.versions.openssl) {
return test.skip('missing crypto');
}
});
afterEach(() => {
if (server) server.close();
if (client) client.close();
});
test('HTTP/2 client upload reject', (done) => {
expect(fs.existsSync(loc)).toBe(true);
fs.readFile(loc, (err, data) => {
expect(err).toBeNull();
server = http2.createServer((req, res) => {
setImmediate(() => {
res.writeHead(400);
res.end();
});
});
server.listen(0, () => {
const port = server.address().port;
client = http2.connect(`http://localhost:${port}`);
const req = client.request({ ':method': 'POST' });
req.on('response', (headers) => {
expect(headers[':status']).toBe(400);
});
req.resume();
req.on('end', () => {
server.close();
client.close();
done();
});
const str = fs.createReadStream(loc);
str.pipe(req);
});
});
});
//<#END_FILE: test-http2-compat-client-upload-reject.js

View File

@@ -0,0 +1,67 @@
//#FILE: test-http2-compat-errors.js
//#SHA1: 3a958d2216c02d05272fbc89bd09a532419876a4
//-----------------
'use strict';
const h2 = require('http2');
// Simulate crypto check
const hasCrypto = true;
if (!hasCrypto) {
test.skip('missing crypto', () => {});
} else {
let expected = null;
describe('http2 compat errors', () => {
let server;
let url;
beforeAll((done) => {
server = h2.createServer((req, res) => {
const resStreamErrorHandler = jest.fn();
const reqErrorHandler = jest.fn();
const resErrorHandler = jest.fn();
const reqAbortedHandler = jest.fn();
const resAbortedHandler = jest.fn();
res.stream.on('error', resStreamErrorHandler);
req.on('error', reqErrorHandler);
res.on('error', resErrorHandler);
req.on('aborted', reqAbortedHandler);
res.on('aborted', resAbortedHandler);
res.write('hello');
expected = new Error('kaboom');
res.stream.destroy(expected);
// Use setImmediate to allow event handlers to be called
setImmediate(() => {
expect(resStreamErrorHandler).toHaveBeenCalled();
expect(reqErrorHandler).not.toHaveBeenCalled();
expect(resErrorHandler).not.toHaveBeenCalled();
expect(reqAbortedHandler).toHaveBeenCalled();
expect(resAbortedHandler).not.toHaveBeenCalled();
server.close(done);
});
});
server.listen(0, () => {
url = `http://localhost:${server.address().port}`;
done();
});
});
test('should handle errors correctly', (done) => {
const client = h2.connect(url, () => {
const request = client.request();
request.on('data', (chunk) => {
client.destroy();
done();
});
});
});
});
}
//<#END_FILE: test-http2-compat-errors.js

View File

@@ -0,0 +1,77 @@
//#FILE: test-http2-compat-expect-continue-check.js
//#SHA1: cfaba2929ccb61aa085572010d7730ceef07859e
//-----------------
'use strict';
const http2 = require('http2');
const testResBody = 'other stuff!\n';
describe('HTTP/2 100-continue flow', () => {
let server;
beforeAll(() => {
if (!process.versions.openssl) {
return test.skip('missing crypto');
}
});
afterEach(() => {
if (server) {
server.close();
}
});
test('Full 100-continue flow', (done) => {
server = http2.createServer();
const fullRequestHandler = jest.fn();
server.on('request', fullRequestHandler);
server.on('checkContinue', (req, res) => {
res.writeContinue();
res.writeHead(200, {});
res.end(testResBody);
expect(res.writeContinue()).toBe(false);
res.on('finish', () => {
process.nextTick(() => {
expect(res.writeContinue()).toBe(false);
});
});
});
server.listen(0, () => {
let body = '';
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request({
':method': 'POST',
'expect': '100-continue'
});
let gotContinue = false;
req.on('continue', () => {
gotContinue = true;
});
req.on('response', (headers) => {
expect(gotContinue).toBe(true);
expect(headers[':status']).toBe(200);
req.end();
});
req.setEncoding('utf-8');
req.on('data', (chunk) => { body += chunk; });
req.on('end', () => {
expect(body).toBe(testResBody);
expect(fullRequestHandler).not.toHaveBeenCalled();
client.close();
done();
});
});
});
});
//<#END_FILE: test-http2-compat-expect-continue-check.js

View File

@@ -0,0 +1,98 @@
//#FILE: test-http2-compat-expect-continue.js
//#SHA1: 3c95de1bb9a0bf620945ec5fc39ba3a515dfe5fd
//-----------------
'use strict';
const http2 = require('http2');
// Skip the test if crypto is not available
const hasCrypto = (() => {
try {
require('crypto');
return true;
} catch (err) {
return false;
}
})();
if (!hasCrypto) {
test.skip('missing crypto', () => {});
} else {
describe('HTTP/2 100-continue flow', () => {
test('full 100-continue flow with response', (done) => {
const testResBody = 'other stuff!\n';
const server = http2.createServer();
let sentResponse = false;
server.on('request', (req, res) => {
res.end(testResBody);
sentResponse = true;
});
server.listen(0, () => {
let body = '';
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request({
':method': 'POST',
'expect': '100-continue'
});
let gotContinue = false;
req.on('continue', () => {
gotContinue = true;
});
req.on('response', (headers) => {
expect(gotContinue).toBe(true);
expect(sentResponse).toBe(true);
expect(headers[':status']).toBe(200);
req.end();
});
req.setEncoding('utf8');
req.on('data', (chunk) => { body += chunk; });
req.on('end', () => {
expect(body).toBe(testResBody);
client.close();
server.close(done);
});
});
});
test('100-continue flow with immediate response', (done) => {
const server = http2.createServer();
server.on('request', (req, res) => {
res.end();
});
server.listen(0, () => {
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request({
':path': '/',
'expect': '100-continue'
});
let gotContinue = false;
req.on('continue', () => {
gotContinue = true;
});
let gotResponse = false;
req.on('response', () => {
gotResponse = true;
});
req.setEncoding('utf8');
req.on('end', () => {
expect(gotContinue).toBe(true);
expect(gotResponse).toBe(true);
client.close();
server.close(done);
});
});
});
});
}
//<#END_FILE: test-http2-compat-expect-continue.js

View File

@@ -0,0 +1,96 @@
//#FILE: test-http2-compat-expect-handling.js
//#SHA1: 015a7b40547c969f4d631e7e743f5293d9e8f843
//-----------------
"use strict";
const http2 = require("http2");
const hasCrypto = (() => {
try {
require("crypto");
return true;
} catch (err) {
return false;
}
})();
const expectValue = "meoww";
describe("HTTP/2 Expect Header Handling", () => {
let server;
let port;
beforeAll(done => {
server = http2.createServer();
server.listen(0, () => {
port = server.address().port;
done();
});
});
afterAll(() => {
server.close();
});
test("server should not call request handler", () => {
const requestHandler = jest.fn();
server.on("request", requestHandler);
return new Promise(resolve => {
server.once("checkExpectation", (req, res) => {
expect(req.headers.expect).toBe(expectValue);
res.statusCode = 417;
res.end();
expect(requestHandler).not.toHaveBeenCalled();
resolve();
});
const client = http2.connect(`http://localhost:${port}`);
const req = client.request({
":path": "/",
":method": "GET",
":scheme": "http",
":authority": `localhost:${port}`,
"expect": expectValue,
});
req.on("response", headers => {
expect(headers[":status"]).toBe(417);
req.resume();
});
req.on("end", () => {
client.close();
});
});
});
test("client should receive 417 status", () => {
return new Promise(resolve => {
const client = http2.connect(`http://localhost:${port}`);
const req = client.request({
":path": "/",
":method": "GET",
":scheme": "http",
":authority": `localhost:${port}`,
"expect": expectValue,
});
req.on("response", headers => {
expect(headers[":status"]).toBe(417);
req.resume();
});
req.on("end", () => {
client.close();
resolve();
});
});
});
});
if (!hasCrypto) {
test.skip("skipping HTTP/2 tests due to missing crypto support", () => {});
}
//<#END_FILE: test-http2-compat-expect-handling.js

View File

@@ -0,0 +1,75 @@
//#FILE: test-http2-compat-serverrequest-pause.js
//#SHA1: 3f3eff95f840e6321b0d25211ef5116304049dc7
//-----------------
'use strict';
const h2 = require('http2');
const hasCrypto = (() => {
try {
require('crypto');
return true;
} catch (err) {
return false;
}
})();
if (!hasCrypto) {
test.skip('missing crypto', () => {});
} else {
const testStr = 'Request Body from Client';
let server;
let client;
beforeAll(() => {
server = h2.createServer();
});
afterAll(() => {
if (client) client.close();
if (server) server.close();
});
test('pause & resume work as expected with Http2ServerRequest', (done) => {
const requestHandler = jest.fn((req, res) => {
let data = '';
req.pause();
req.setEncoding('utf8');
req.on('data', jest.fn((chunk) => (data += chunk)));
setTimeout(() => {
expect(data).toBe('');
req.resume();
}, 100);
req.on('end', () => {
expect(data).toBe(testStr);
res.end();
});
res.on('finish', () => process.nextTick(() => {
req.pause();
req.resume();
}));
});
server.on('request', requestHandler);
server.listen(0, () => {
const port = server.address().port;
client = h2.connect(`http://localhost:${port}`);
const request = client.request({
':path': '/foobar',
':method': 'POST',
':scheme': 'http',
':authority': `localhost:${port}`
});
request.resume();
request.end(testStr);
request.on('end', () => {
expect(requestHandler).toHaveBeenCalled();
done();
});
});
});
}
//<#END_FILE: test-http2-compat-serverrequest-pause.js

View File

@@ -0,0 +1,69 @@
//#FILE: test-http2-compat-serverrequest-pipe.js
//#SHA1: c4254ac88df3334dccc8adb4b60856193a6e644e
//-----------------
"use strict";
const http2 = require("http2");
const fs = require("fs");
const path = require("path");
const os = require("os");
const { isWindows } = require("harness");
const fixtures = path.join(__dirname, "..", "fixtures");
const tmpdir = os.tmpdir();
let server;
let client;
let port;
beforeAll(async () => {
if (!process.versions.openssl) {
return test.skip("missing crypto");
}
await fs.promises.mkdir(tmpdir, { recursive: true });
});
afterAll(async () => {
if (server) server.close();
if (client) client.close();
});
test.todoIf(isWindows)("HTTP/2 server request pipe", done => {
const loc = path.join(fixtures, "person-large.jpg");
const fn = path.join(tmpdir, "http2-url-tests.js");
server = http2.createServer();
server.on("request", (req, res) => {
const dest = req.pipe(fs.createWriteStream(fn));
dest.on("finish", () => {
expect(req.complete).toBe(true);
expect(fs.readFileSync(loc).length).toBe(fs.readFileSync(fn).length);
fs.unlinkSync(fn);
res.end();
});
});
server.listen(0, () => {
port = server.address().port;
client = http2.connect(`http://localhost:${port}`);
let remaining = 2;
function maybeClose() {
if (--remaining === 0) {
done();
}
}
const req = client.request({ ":method": "POST" });
req.on("response", () => {});
req.resume();
req.on("end", maybeClose);
const str = fs.createReadStream(loc);
str.on("end", maybeClose);
str.pipe(req);
});
});
//<#END_FILE: test-http2-compat-serverrequest-pipe.js

View File

@@ -0,0 +1,69 @@
//#FILE: test-http2-compat-serverrequest.js
//#SHA1: f661c6c9249c0cdc770439f7498943fc5edbf86b
//-----------------
"use strict";
const h2 = require("http2");
const net = require("net");
let server;
let port;
beforeAll(done => {
server = h2.createServer();
server.listen(0, () => {
port = server.address().port;
done();
});
});
afterAll(done => {
server.close(done);
});
// today we deatch the socket earlier
test.todo("Http2ServerRequest should expose convenience properties", done => {
expect.assertions(7);
server.once("request", (request, response) => {
const expected = {
version: "2.0",
httpVersionMajor: 2,
httpVersionMinor: 0,
};
expect(request.httpVersion).toBe(expected.version);
expect(request.httpVersionMajor).toBe(expected.httpVersionMajor);
expect(request.httpVersionMinor).toBe(expected.httpVersionMinor);
expect(request.socket).toBeInstanceOf(net.Socket);
expect(request.connection).toBeInstanceOf(net.Socket);
expect(request.socket).toBe(request.connection);
response.on("finish", () => {
process.nextTick(() => {
expect(request.socket).toBeTruthy();
done();
});
});
response.end();
});
const url = `http://localhost:${port}`;
const client = h2.connect(url, () => {
const headers = {
":path": "/foobar",
":method": "GET",
":scheme": "http",
":authority": `localhost:${port}`,
};
const request = client.request(headers);
request.on("end", () => {
client.close();
});
request.end();
request.resume();
});
});
//<#END_FILE: test-http2-compat-serverrequest.js

View File

@@ -0,0 +1,64 @@
//#FILE: test-http2-compat-serverresponse-close.js
//#SHA1: 6b61a9cea948447ae33843472678ffbed0b47c9a
//-----------------
"use strict";
const h2 = require("http2");
// Skip the test if crypto is not available
let hasCrypto;
try {
require("crypto");
hasCrypto = true;
} catch (err) {
hasCrypto = false;
}
(hasCrypto ? describe : describe.skip)("HTTP/2 server response close", () => {
let server;
let url;
beforeAll(done => {
server = h2.createServer((req, res) => {
res.writeHead(200);
res.write("a");
const reqCloseMock = jest.fn();
const resCloseMock = jest.fn();
const reqErrorMock = jest.fn();
req.on("close", reqCloseMock);
res.on("close", resCloseMock);
req.on("error", reqErrorMock);
// Use Jest's fake timers to ensure the test doesn't hang
setTimeout(() => {
expect(reqCloseMock).toHaveBeenCalled();
expect(resCloseMock).toHaveBeenCalled();
expect(reqErrorMock).not.toHaveBeenCalled();
done();
}, 1000);
});
server.listen(0, () => {
url = `http://localhost:${server.address().port}`;
done();
});
});
afterAll(() => {
server.close();
});
test("Server request and response should receive close event if connection terminated before response.end", done => {
const client = h2.connect(url, () => {
const request = client.request();
request.on("data", chunk => {
client.destroy();
done();
});
});
});
});
//<#END_FILE: test-http2-compat-serverresponse-close.js

View File

@@ -0,0 +1,61 @@
//#FILE: test-http2-compat-serverresponse-drain.js
//#SHA1: 4ec55745f622a31b4729fcb9daf9bfd707a3bdb3
//-----------------
'use strict';
const h2 = require('http2');
const hasCrypto = (() => {
try {
require('crypto');
return true;
} catch (err) {
return false;
}
})();
const testString = 'tests';
test('HTTP/2 server response drain event', async () => {
if (!hasCrypto) {
test.skip('missing crypto');
return;
}
const server = h2.createServer();
const requestHandler = jest.fn((req, res) => {
res.stream._writableState.highWaterMark = testString.length;
expect(res.write(testString)).toBe(false);
res.on('drain', jest.fn(() => res.end(testString)));
});
server.on('request', requestHandler);
await new Promise(resolve => server.listen(0, resolve));
const port = server.address().port;
const client = h2.connect(`http://localhost:${port}`);
const request = client.request({
':path': '/foobar',
':method': 'POST',
':scheme': 'http',
':authority': `localhost:${port}`
});
request.resume();
request.end();
let data = '';
request.setEncoding('utf8');
request.on('data', (chunk) => (data += chunk));
await new Promise(resolve => request.on('end', resolve));
expect(data).toBe(testString.repeat(2));
expect(requestHandler).toHaveBeenCalled();
client.close();
await new Promise(resolve => server.close(resolve));
});
//<#END_FILE: test-http2-compat-serverresponse-drain.js

View File

@@ -0,0 +1,51 @@
//#FILE: test-http2-compat-serverresponse-end-after-statuses-without-body.js
//#SHA1: c4a4b76e1b04b7e6779f80f7077758dfab0e8b80
//-----------------
"use strict";
const h2 = require("http2");
const { HTTP_STATUS_NO_CONTENT, HTTP_STATUS_RESET_CONTENT, HTTP_STATUS_NOT_MODIFIED } = h2.constants;
const statusWithoutBody = [HTTP_STATUS_NO_CONTENT, HTTP_STATUS_RESET_CONTENT, HTTP_STATUS_NOT_MODIFIED];
const STATUS_CODES_COUNT = statusWithoutBody.length;
describe("HTTP/2 server response end after statuses without body", () => {
let server;
let url;
beforeAll(done => {
server = h2.createServer((req, res) => {
res.writeHead(statusWithoutBody.pop());
res.end();
});
server.listen(0, () => {
url = `http://localhost:${server.address().port}`;
done();
});
});
afterAll(() => {
server.close();
});
it("should handle end() after sending statuses without body", done => {
const client = h2.connect(url, () => {
let responseCount = 0;
const closeAfterResponse = () => {
if (STATUS_CODES_COUNT === ++responseCount) {
client.destroy();
done();
}
};
for (let i = 0; i < STATUS_CODES_COUNT; i++) {
const request = client.request();
request.on("response", closeAfterResponse);
}
});
});
});
//<#END_FILE: test-http2-compat-serverresponse-end-after-statuses-without-body.js

View File

@@ -0,0 +1,80 @@
//#FILE: test-http2-compat-serverresponse-end.js
//#SHA1: 672da69abcb0b86d5234556e692949ac36ef6395
//-----------------
'use strict';
const http2 = require('http2');
const { promisify } = require('util');
// Mock the common module functions
const mustCall = (fn) => jest.fn(fn);
const mustNotCall = () => jest.fn().mockImplementation(() => {
throw new Error('This function should not have been called');
});
const {
HTTP2_HEADER_STATUS,
HTTP_STATUS_OK
} = http2.constants;
// Helper function to create a server and get its port
const createServerAndGetPort = async (requestListener) => {
const server = http2.createServer(requestListener);
await promisify(server.listen.bind(server))(0);
const { port } = server.address();
return { server, port };
};
// Helper function to create a client
const createClient = (port) => {
const url = `http://localhost:${port}`;
return http2.connect(url);
};
describe('Http2ServerResponse.end', () => {
test('accepts chunk, encoding, cb as args and can be called multiple times', async () => {
const { server, port } = await createServerAndGetPort((request, response) => {
const endCallback = jest.fn(() => {
response.end(jest.fn());
process.nextTick(() => {
response.end(jest.fn());
server.close();
});
});
response.end('end', 'utf8', endCallback);
response.on('finish', () => {
response.end(jest.fn());
});
response.end(jest.fn());
});
const client = createClient(port);
const headers = {
':path': '/',
':method': 'GET',
':scheme': 'http',
':authority': `localhost:${port}`
};
let data = '';
const request = client.request(headers);
request.setEncoding('utf8');
request.on('data', (chunk) => (data += chunk));
await new Promise(resolve => {
request.on('end', () => {
expect(data).toBe('end');
client.close();
resolve();
});
request.end();
request.resume();
});
});
// Add more tests here...
});
// More test blocks for other scenarios...
//<#END_FILE: test-http2-compat-serverresponse-end.test.js

View File

@@ -0,0 +1,68 @@
//#FILE: test-http2-compat-serverresponse-finished.js
//#SHA1: 6ef7a05f30923975d7a267cee54aafae1bfdbc7d
//-----------------
'use strict';
const h2 = require('http2');
const net = require('net');
let server;
beforeAll(() => {
// Skip the test if crypto is not available
if (!process.versions.openssl) {
return test.skip('missing crypto');
}
});
afterEach(() => {
if (server) {
server.close();
}
});
test('Http2ServerResponse.finished', (done) => {
server = h2.createServer();
server.listen(0, () => {
const port = server.address().port;
server.once('request', (request, response) => {
expect(response.socket).toBeInstanceOf(net.Socket);
expect(response.connection).toBeInstanceOf(net.Socket);
expect(response.socket).toBe(response.connection);
response.on('finish', () => {
expect(response.socket).toBeUndefined();
expect(response.connection).toBeUndefined();
process.nextTick(() => {
expect(response.stream).toBeDefined();
done();
});
});
expect(response.finished).toBe(false);
expect(response.writableEnded).toBe(false);
response.end();
expect(response.finished).toBe(true);
expect(response.writableEnded).toBe(true);
});
const url = `http://localhost:${port}`;
const client = h2.connect(url, () => {
const headers = {
':path': '/',
':method': 'GET',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.on('end', () => {
client.close();
});
request.end();
request.resume();
});
});
});
//<#END_FILE: test-http2-compat-serverresponse-finished.js

View File

@@ -0,0 +1,71 @@
//#FILE: test-http2-compat-serverresponse-flushheaders.js
//#SHA1: ea772e05a29f43bd7b61e4d70f24b94c1e1e201c
//-----------------
"use strict";
const h2 = require("http2");
let server;
let serverResponse;
beforeAll(done => {
server = h2.createServer();
server.listen(0, () => {
done();
});
});
afterAll(() => {
server.close();
});
test("Http2ServerResponse.flushHeaders", done => {
const port = server.address().port;
server.once("request", (request, response) => {
expect(response.headersSent).toBe(false);
expect(response._header).toBe(false); // Alias for headersSent
response.flushHeaders();
expect(response.headersSent).toBe(true);
expect(response._header).toBe(true);
response.flushHeaders(); // Idempotent
expect(() => {
response.writeHead(400, { "foo-bar": "abc123" });
}).toThrow(
expect.objectContaining({
code: "ERR_HTTP2_HEADERS_SENT",
}),
);
response.on("finish", () => {
process.nextTick(() => {
response.flushHeaders(); // Idempotent
done();
});
});
serverResponse = response;
});
const url = `http://localhost:${port}`;
const client = h2.connect(url, () => {
const headers = {
":path": "/",
":method": "GET",
":scheme": "http",
":authority": `localhost:${port}`,
};
const request = client.request(headers);
request.on("response", (headers, flags) => {
expect(headers["foo-bar"]).toBeUndefined();
expect(headers[":status"]).toBe(200);
serverResponse.end();
});
request.on("end", () => {
client.close();
});
request.end();
request.resume();
});
});
//<#END_FILE: test-http2-compat-serverresponse-flushheaders.js

View File

@@ -0,0 +1,48 @@
//#FILE: test-http2-compat-serverresponse-headers-send-date.js
//#SHA1: 1ed6319986a3bb9bf58709d9577d03407fdde3f2
//-----------------
"use strict";
const http2 = require("http2");
let server;
let port;
beforeAll(done => {
if (!process.versions.openssl) {
return test.skip("missing crypto");
}
server = http2.createServer((request, response) => {
response.sendDate = false;
response.writeHead(200);
response.end();
});
server.listen(0, () => {
port = server.address().port;
done();
});
});
afterAll(() => {
server.close();
});
test("HTTP/2 server response should not send Date header when sendDate is false", done => {
const session = http2.connect(`http://localhost:${port}`);
const req = session.request();
req.on("response", (headers, flags) => {
expect(headers).not.toHaveProperty("Date");
expect(headers).not.toHaveProperty("date");
});
req.on("end", () => {
session.close();
done();
});
req.end();
});
//<#END_FILE: test-http2-compat-serverresponse-headers-send-date.js

View File

@@ -0,0 +1,78 @@
//#FILE: test-http2-compat-serverresponse-settimeout.js
//#SHA1: fe2e0371e885463968a268362464724494b758a6
//-----------------
"use strict";
const http2 = require("http2");
const msecs = 1000; // Assuming a reasonable timeout for all platforms
let server;
let client;
beforeAll(done => {
if (!process.versions.openssl) {
return test.skip("missing crypto");
}
server = http2.createServer();
server.listen(0, () => {
done();
});
});
afterAll(() => {
if (client) {
client.close();
}
if (server) {
server.close();
}
});
test("HTTP2 ServerResponse setTimeout", done => {
const timeoutCallback = jest.fn();
const onTimeout = jest.fn();
const onFinish = jest.fn();
server.on("request", (req, res) => {
res.setTimeout(msecs, timeoutCallback);
res.on("timeout", onTimeout);
res.on("finish", () => {
onFinish();
res.setTimeout(msecs, jest.fn());
process.nextTick(() => {
res.setTimeout(msecs, jest.fn());
});
});
// Explicitly end the response after a short delay
setTimeout(() => {
res.end();
}, 100);
});
const port = server.address().port;
client = http2.connect(`http://localhost:${port}`);
const req = client.request({
":path": "/",
":method": "GET",
":scheme": "http",
":authority": `localhost:${port}`,
});
req.on("end", () => {
client.close();
// Move assertions here to ensure they run after the response has finished
expect(timeoutCallback).not.toHaveBeenCalled();
expect(onTimeout).not.toHaveBeenCalled();
expect(onFinish).toHaveBeenCalledTimes(1);
done();
});
req.resume();
req.end();
}, 10000); // Increase the timeout to 10 seconds
//<#END_FILE: test-http2-compat-serverresponse-settimeout.js

View File

@@ -0,0 +1,95 @@
//#FILE: test-http2-compat-serverresponse-statuscode.js
//#SHA1: 10cb487c1fd9e256f807319b84c426b356be443f
//-----------------
"use strict";
const h2 = require("http2");
let server;
let port;
beforeAll(async () => {
server = h2.createServer();
await new Promise(resolve => server.listen(0, resolve));
port = server.address().port;
});
afterAll(async () => {
server.close();
});
test("Http2ServerResponse should have a statusCode property", async () => {
const responsePromise = new Promise(resolve => {
server.once("request", (request, response) => {
const expectedDefaultStatusCode = 200;
const realStatusCodes = {
continue: 100,
ok: 200,
multipleChoices: 300,
badRequest: 400,
internalServerError: 500,
};
const fakeStatusCodes = {
tooLow: 99,
tooHigh: 600,
};
expect(response.statusCode).toBe(expectedDefaultStatusCode);
// Setting the response.statusCode should not throw.
response.statusCode = realStatusCodes.ok;
response.statusCode = realStatusCodes.multipleChoices;
response.statusCode = realStatusCodes.badRequest;
response.statusCode = realStatusCodes.internalServerError;
expect(() => {
response.statusCode = realStatusCodes.continue;
}).toThrow(
expect.objectContaining({
code: "ERR_HTTP2_INFO_STATUS_NOT_ALLOWED",
name: "RangeError",
}),
);
expect(() => {
response.statusCode = fakeStatusCodes.tooLow;
}).toThrow(
expect.objectContaining({
code: "ERR_HTTP2_STATUS_INVALID",
name: "RangeError",
}),
);
expect(() => {
response.statusCode = fakeStatusCodes.tooHigh;
}).toThrow(
expect.objectContaining({
code: "ERR_HTTP2_STATUS_INVALID",
name: "RangeError",
}),
);
response.on("finish", resolve);
response.end();
});
});
const url = `http://localhost:${port}`;
const client = h2.connect(url);
const headers = {
":path": "/",
":method": "GET",
":scheme": "http",
":authority": `localhost:${port}`,
};
const request = client.request(headers);
request.end();
await new Promise(resolve => request.resume().on("end", resolve));
await responsePromise;
client.close();
});
//<#END_FILE: test-http2-compat-serverresponse-statuscode.js

View File

@@ -0,0 +1,114 @@
//#FILE: test-http2-compat-serverresponse-writehead-array.js
//#SHA1: e43a5a9f99ddad68b313e15fbb69839cca6d0775
//-----------------
'use strict';
const http2 = require('http2');
// Skip the test if crypto is not available
const hasCrypto = (() => {
try {
require('crypto');
return true;
} catch (err) {
return false;
}
})();
if (!hasCrypto) {
test.skip('missing crypto', () => {});
} else {
describe('Http2ServerResponse.writeHead with arrays', () => {
test('should support nested arrays', (done) => {
const server = http2.createServer();
server.listen(0, () => {
const port = server.address().port;
server.once('request', (request, response) => {
const returnVal = response.writeHead(200, [
['foo', 'bar'],
['foo', 'baz'],
['ABC', 123],
]);
expect(returnVal).toBe(response);
response.end(() => { server.close(); });
});
const client = http2.connect(`http://localhost:${port}`, () => {
const request = client.request();
request.on('response', (headers) => {
expect(headers.foo).toBe('bar, baz');
expect(headers.abc).toBe('123');
expect(headers[':status']).toBe(200);
});
request.on('end', () => {
client.close();
done();
});
request.end();
request.resume();
});
});
});
test('should support flat arrays', (done) => {
const server = http2.createServer();
server.listen(0, () => {
const port = server.address().port;
server.once('request', (request, response) => {
const returnVal = response.writeHead(200, ['foo', 'bar', 'foo', 'baz', 'ABC', 123]);
expect(returnVal).toBe(response);
response.end(() => { server.close(); });
});
const client = http2.connect(`http://localhost:${port}`, () => {
const request = client.request();
request.on('response', (headers) => {
expect(headers.foo).toBe('bar, baz');
expect(headers.abc).toBe('123');
expect(headers[':status']).toBe(200);
});
request.on('end', () => {
client.close();
done();
});
request.end();
request.resume();
});
});
});
test('should throw ERR_INVALID_ARG_VALUE for invalid array', (done) => {
const server = http2.createServer();
server.listen(0, () => {
const port = server.address().port;
server.once('request', (request, response) => {
expect(() => {
response.writeHead(200, ['foo', 'bar', 'ABC', 123, 'extra']);
}).toThrow(expect.objectContaining({
code: 'ERR_INVALID_ARG_VALUE'
}));
response.end(() => { server.close(); });
});
const client = http2.connect(`http://localhost:${port}`, () => {
const request = client.request();
request.on('end', () => {
client.close();
done();
});
request.end();
request.resume();
});
});
});
});
}
//<#END_FILE: test-http2-compat-serverresponse-writehead-array.js

View File

@@ -0,0 +1,65 @@
//#FILE: test-http2-compat-serverresponse-writehead.js
//#SHA1: fa267d5108f95ba69583bc709a82185ee9d18e76
//-----------------
'use strict';
const h2 = require('http2');
// Http2ServerResponse.writeHead should override previous headers
test('Http2ServerResponse.writeHead overrides previous headers', (done) => {
const server = h2.createServer();
server.listen(0, () => {
const port = server.address().port;
server.once('request', (request, response) => {
response.setHeader('foo-bar', 'def456');
// Override
const returnVal = response.writeHead(418, { 'foo-bar': 'abc123' });
expect(returnVal).toBe(response);
expect(() => { response.writeHead(300); }).toThrow(expect.objectContaining({
code: 'ERR_HTTP2_HEADERS_SENT'
}));
response.on('finish', () => {
server.close();
process.nextTick(() => {
// The stream is invalid at this point,
// and this line verifies this does not throw.
response.writeHead(300);
done();
});
});
response.end();
});
const url = `http://localhost:${port}`;
const client = h2.connect(url, () => {
const headers = {
':path': '/',
':method': 'GET',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.on('response', (headers) => {
expect(headers['foo-bar']).toBe('abc123');
expect(headers[':status']).toBe(418);
});
request.on('end', () => {
client.close();
});
request.end();
request.resume();
});
});
});
// Skip the test if crypto is not available
if (!process.versions.openssl) {
test.skip('missing crypto', () => {});
}
//<#END_FILE: test-http2-compat-serverresponse-writehead.js

View File

@@ -0,0 +1,47 @@
//#FILE: test-http2-compat-socket-destroy-delayed.js
//#SHA1: c7b5b8b5de4667a89e0e261e36098f617d411ed2
//-----------------
"use strict";
const http2 = require("http2");
const { HTTP2_HEADER_PATH, HTTP2_HEADER_METHOD } = http2.constants;
// Skip the test if crypto is not available
if (!process.versions.openssl) {
test.skip("missing crypto", () => {});
} else {
test("HTTP/2 socket destroy delayed", done => {
const app = http2.createServer((req, res) => {
res.end("hello");
setImmediate(() => req.socket?.destroy());
});
app.listen(0, () => {
const session = http2.connect(`http://localhost:${app.address().port}`);
const request = session.request({
[HTTP2_HEADER_PATH]: "/",
[HTTP2_HEADER_METHOD]: "get",
});
request.once("response", (headers, flags) => {
let data = "";
request.on("data", chunk => {
data += chunk;
});
request.on("end", () => {
expect(data).toBe("hello");
session.close();
app.close();
done();
});
});
request.end();
});
});
}
// This tests verifies that calling `req.socket.destroy()` via
// setImmediate does not crash.
// Fixes https://github.com/nodejs/node/issues/22855.
//<#END_FILE: test-http2-compat-socket-destroy-delayed.js

View File

@@ -0,0 +1,72 @@
//#FILE: test-http2-compat-write-early-hints-invalid-argument-type.js
//#SHA1: 8ae2eba59668a38b039a100d3ad26f88e54be806
//-----------------
"use strict";
const http2 = require("node:http2");
const util = require("node:util");
const debug = util.debuglog("test");
const testResBody = "response content";
// Check if crypto is available
let hasCrypto = false;
try {
require("crypto");
hasCrypto = true;
} catch (err) {
// crypto not available
}
(hasCrypto ? describe : describe.skip)("HTTP2 compat writeEarlyHints invalid argument type", () => {
let server;
let client;
beforeAll(done => {
server = http2.createServer();
server.listen(0, () => {
done();
});
});
afterAll(() => {
if (client) {
client.close();
}
server.close();
});
test("should throw ERR_INVALID_ARG_TYPE for invalid object value", done => {
server.on("request", (req, res) => {
debug("Server sending early hints...");
expect(() => {
res.writeEarlyHints("this should not be here");
}).toThrow(
expect.objectContaining({
code: "ERR_INVALID_ARG_TYPE",
name: "TypeError",
}),
);
debug("Server sending full response...");
res.end(testResBody);
});
client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request();
debug("Client sending request...");
req.on("headers", () => {
done(new Error("Should not receive headers"));
});
req.on("response", () => {
done();
});
req.end();
});
});
//<#END_FILE: test-http2-compat-write-early-hints-invalid-argument-type.js

View File

@@ -0,0 +1,146 @@
//#FILE: test-http2-compat-write-early-hints.js
//#SHA1: 0ed18263958421cde07c37b8ec353005b7477499
//-----------------
'use strict';
const http2 = require('node:http2');
const util = require('node:util');
const debug = util.debuglog('test');
const testResBody = 'response content';
describe('HTTP/2 Early Hints', () => {
test('Happy flow - string argument', async () => {
const server = http2.createServer();
server.on('request', (req, res) => {
debug('Server sending early hints...');
res.writeEarlyHints({
link: '</styles.css>; rel=preload; as=style'
});
debug('Server sending full response...');
res.end(testResBody);
});
await new Promise(resolve => server.listen(0, resolve));
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request();
debug('Client sending request...');
await new Promise(resolve => {
req.on('headers', (headers) => {
expect(headers).toBeDefined();
expect(headers[':status']).toBe(103);
expect(headers.link).toBe('</styles.css>; rel=preload; as=style');
});
req.on('response', (headers) => {
expect(headers[':status']).toBe(200);
});
let data = '';
req.on('data', (d) => data += d);
req.on('end', () => {
debug('Got full response.');
expect(data).toBe(testResBody);
client.close();
server.close(resolve);
});
});
});
test('Happy flow - array argument', async () => {
const server = http2.createServer();
server.on('request', (req, res) => {
debug('Server sending early hints...');
res.writeEarlyHints({
link: [
'</styles.css>; rel=preload; as=style',
'</scripts.js>; rel=preload; as=script',
]
});
debug('Server sending full response...');
res.end(testResBody);
});
await new Promise(resolve => server.listen(0, resolve));
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request();
debug('Client sending request...');
await new Promise(resolve => {
req.on('headers', (headers) => {
expect(headers).toBeDefined();
expect(headers[':status']).toBe(103);
expect(headers.link).toBe(
'</styles.css>; rel=preload; as=style, </scripts.js>; rel=preload; as=script'
);
});
req.on('response', (headers) => {
expect(headers[':status']).toBe(200);
});
let data = '';
req.on('data', (d) => data += d);
req.on('end', () => {
debug('Got full response.');
expect(data).toBe(testResBody);
client.close();
server.close(resolve);
});
});
});
test('Happy flow - empty array', async () => {
const server = http2.createServer();
server.on('request', (req, res) => {
debug('Server sending early hints...');
res.writeEarlyHints({
link: []
});
debug('Server sending full response...');
res.end(testResBody);
});
await new Promise(resolve => server.listen(0, resolve));
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request();
debug('Client sending request...');
await new Promise(resolve => {
const headersListener = jest.fn();
req.on('headers', headersListener);
req.on('response', (headers) => {
expect(headers[':status']).toBe(200);
expect(headersListener).not.toHaveBeenCalled();
});
let data = '';
req.on('data', (d) => data += d);
req.on('end', () => {
debug('Got full response.');
expect(data).toBe(testResBody);
client.close();
server.close(resolve);
});
});
});
});
//<#END_FILE: test-http2-compat-write-early-hints.js

View File

@@ -0,0 +1,59 @@
//#FILE: test-http2-compat-write-head-destroyed.js
//#SHA1: 29f693f49912d4621c1a19ab7412b1b318d55d8e
//-----------------
"use strict";
const http2 = require("http2");
let server;
let port;
beforeAll(done => {
if (!process.versions.openssl) {
done();
return;
}
server = http2.createServer((req, res) => {
// Destroy the stream first
req.stream.destroy();
res.writeHead(200);
res.write("hello ");
res.end("world");
});
server.listen(0, () => {
port = server.address().port;
done();
});
});
afterAll(() => {
if (server) {
server.close();
}
});
test("writeHead, write and end do not crash in compatibility mode", done => {
if (!process.versions.openssl) {
return test.skip("missing crypto");
}
const client = http2.connect(`http://localhost:${port}`);
const req = client.request();
req.on("response", () => {
done.fail("Should not receive response");
});
req.on("close", () => {
client.close();
done();
});
req.resume();
});
//<#END_FILE: test-http2-compat-write-head-destroyed.js

View File

@@ -0,0 +1,62 @@
//#FILE: test-http2-connect-tls-with-delay.js
//#SHA1: 8c5489e025ec14c2cc53788b27fde11a11990e42
//-----------------
'use strict';
const http2 = require('http2');
const tls = require('tls');
const fs = require('fs');
const path = require('path');
const serverOptions = {
key: fs.readFileSync(path.join(__dirname, '..', 'fixtures', 'keys', 'agent1-key.pem')),
cert: fs.readFileSync(path.join(__dirname, '..', 'fixtures', 'keys', 'agent1-cert.pem'))
};
let server;
beforeAll((done) => {
server = http2.createSecureServer(serverOptions, (req, res) => {
res.end();
});
server.listen(0, '127.0.0.1', done);
});
afterAll((done) => {
server.close(done);
});
test('HTTP/2 connect with TLS and delay', (done) => {
const options = {
ALPNProtocols: ['h2'],
host: '127.0.0.1',
servername: 'localhost',
port: server.address().port,
rejectUnauthorized: false
};
const socket = tls.connect(options, async () => {
socket.once('readable', () => {
const client = http2.connect(
'https://localhost:' + server.address().port,
{ ...options, createConnection: () => socket }
);
client.once('remoteSettings', () => {
const req = client.request({
':path': '/'
});
req.on('data', () => req.resume());
req.on('end', () => {
client.close();
req.close();
done();
});
req.end();
});
});
});
});
//<#END_FILE: test-http2-connect-tls-with-delay.js

View File

@@ -0,0 +1,71 @@
//#FILE: test-http2-cookies.js
//#SHA1: 91bdbacba9eb8ebd9dddd43327aa2271dc00c271
//-----------------
'use strict';
const h2 = require('http2');
const hasCrypto = (() => {
try {
require('crypto');
return true;
} catch (err) {
return false;
}
})();
if (!hasCrypto) {
test.skip('missing crypto', () => {});
} else {
test('HTTP/2 cookies', async () => {
const server = h2.createServer();
const setCookie = [
'a=b',
'c=d; Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly',
'e=f',
];
server.on('stream', (stream, headers) => {
expect(typeof headers.abc).toBe('string');
expect(headers.abc).toBe('1, 2, 3');
expect(typeof headers.cookie).toBe('string');
expect(headers.cookie).toBe('a=b; c=d; e=f');
stream.respond({
'content-type': 'text/html',
':status': 200,
'set-cookie': setCookie
});
stream.end('hello world');
});
await new Promise(resolve => server.listen(0, resolve));
const client = h2.connect(`http://localhost:${server.address().port}`);
const req = client.request({
':path': '/',
'abc': [1, 2, 3],
'cookie': ['a=b', 'c=d', 'e=f'],
});
await new Promise((resolve, reject) => {
req.on('response', (headers) => {
expect(Array.isArray(headers['set-cookie'])).toBe(true);
expect(headers['set-cookie']).toEqual(setCookie);
});
req.on('end', resolve);
req.on('error', reject);
req.end();
req.resume();
});
server.close();
client.close();
});
}
//<#END_FILE: test-http2-cookies.js

View File

@@ -0,0 +1,88 @@
//#FILE: test-http2-createwritereq.js
//#SHA1: 8b0d2399fb8a26ce6cc76b9f338be37a7ff08ca5
//-----------------
"use strict";
const http2 = require("http2");
// Mock the gc function
global.gc = jest.fn();
const testString = "a\u00A1\u0100\uD83D\uDE00";
const encodings = {
// "buffer": "utf8",
"ascii": "ascii",
// "latin1": "latin1",
// "binary": "latin1",
// "utf8": "utf8",
// "utf-8": "utf8",
// "ucs2": "ucs2",
// "ucs-2": "ucs2",
// "utf16le": "ucs2",
// "utf-16le": "ucs2",
// "UTF8": "utf8",
};
describe("http2 createWriteReq", () => {
let server;
let serverAddress;
beforeAll(done => {
server = http2.createServer((req, res) => {
const testEncoding = encodings[req.url.slice(1)];
req.on("data", chunk => {
// console.error(testEncoding, chunk, Buffer.from(testString, testEncoding));
expect(Buffer.from(testString, testEncoding).equals(chunk)).toBe(true);
});
req.on("end", () => res.end());
});
server.listen(0, () => {
serverAddress = `http://localhost:${server.address().port}`;
done();
});
});
afterAll(() => {
server.close();
});
Object.keys(encodings).forEach(writeEncoding => {
test(`should handle ${writeEncoding} encoding`, done => {
const client = http2.connect(serverAddress);
const req = client.request({
":path": `/${writeEncoding}`,
":method": "POST",
});
expect(req._writableState.decodeStrings).toBe(false);
req.write(
writeEncoding !== "buffer" ? testString : Buffer.from(testString),
writeEncoding !== "buffer" ? writeEncoding : undefined,
);
req.resume();
req.on("end", () => {
client.close();
done();
});
// Ref: https://github.com/nodejs/node/issues/17840
const origDestroy = req.destroy;
req.destroy = function (...args) {
// Schedule a garbage collection event at the end of the current
// MakeCallback() run.
process.nextTick(global.gc);
return origDestroy.call(this, ...args);
};
req.end();
});
});
});
//<#END_FILE: test-http2-createwritereq.test.js

View File

@@ -0,0 +1,54 @@
//#FILE: test-http2-destroy-after-write.js
//#SHA1: 193688397df0b891b9286ff825ca873935d30e04
//-----------------
"use strict";
const http2 = require("http2");
let server;
let port;
beforeAll(done => {
server = http2.createServer();
server.on("session", session => {
session.on("stream", stream => {
stream.on("end", function () {
this.respond({
":status": 200,
});
this.write("foo");
this.destroy();
});
stream.resume();
});
});
server.listen(0, () => {
port = server.address().port;
done();
});
});
afterAll(() => {
server.close();
});
test("http2 destroy after write", done => {
const client = http2.connect(`http://localhost:${port}`);
const stream = client.request({ ":method": "POST" });
stream.on("response", headers => {
expect(headers[":status"]).toBe(200);
});
stream.on("close", () => {
client.close();
done();
});
stream.resume();
stream.end();
});
//<#END_FILE: test-http2-destroy-after-write.js

View File

@@ -0,0 +1,58 @@
//#FILE: test-http2-dont-override.js
//#SHA1: d295b8c4823cc34c03773eb08bf0393fca541694
//-----------------
'use strict';
const http2 = require('http2');
// Skip test if crypto is not available
if (!process.versions.openssl) {
test.skip('missing crypto', () => {});
} else {
test('http2 should not override options', (done) => {
const options = {};
const server = http2.createServer(options);
// Options are defaulted but the options are not modified
expect(Object.keys(options)).toEqual([]);
server.on('stream', (stream) => {
const headers = {};
const options = {};
stream.respond(headers, options);
// The headers are defaulted but the original object is not modified
expect(Object.keys(headers)).toEqual([]);
// Options are defaulted but the original object is not modified
expect(Object.keys(options)).toEqual([]);
stream.end();
});
server.listen(0, () => {
const client = http2.connect(`http://localhost:${server.address().port}`);
const headers = {};
const options = {};
const req = client.request(headers, options);
// The headers are defaulted but the original object is not modified
expect(Object.keys(headers)).toEqual([]);
// Options are defaulted but the original object is not modified
expect(Object.keys(options)).toEqual([]);
req.resume();
req.on('end', () => {
server.close();
client.close();
done();
});
});
});
}
//<#END_FILE: test-http2-dont-override.js

View File

@@ -0,0 +1,85 @@
//#FILE: test-http2-forget-closed-streams.js
//#SHA1: 2f917924c763cc220e68ce2b829c63dc03a836ab
//-----------------
"use strict";
const http2 = require("http2");
// Skip test if crypto is not available
const hasCrypto = (() => {
try {
require("crypto");
return true;
} catch (err) {
return false;
}
})();
(hasCrypto ? describe : describe.skip)("http2 forget closed streams", () => {
let server;
beforeAll(() => {
server = http2.createServer({ maxSessionMemory: 1 });
server.on("session", session => {
session.on("stream", stream => {
stream.on("end", () => {
stream.respond(
{
":status": 200,
},
{
endStream: true,
},
);
});
stream.resume();
});
});
});
afterAll(() => {
server.close();
});
test("should handle 10000 requests without memory issues", done => {
const listenPromise = new Promise(resolve => {
server.listen(0, () => {
resolve(server.address().port);
});
});
listenPromise.then(port => {
const client = http2.connect(`http://localhost:${port}`);
function makeRequest(i) {
return new Promise(resolve => {
const stream = client.request({ ":method": "POST" });
stream.on("response", headers => {
expect(headers[":status"]).toBe(200);
stream.on("close", resolve);
});
stream.end();
});
}
async function runRequests() {
for (let i = 0; i < 10000; i++) {
await makeRequest(i);
}
client.close();
}
runRequests()
.then(() => {
// If we've reached here without errors, the test has passed
expect(true).toBe(true);
done();
})
.catch(err => {
done(err);
});
});
}, 30000); // Increase timeout to 30 seconds
});
//<#END_FILE: test-http2-forget-closed-streams.js

View File

@@ -0,0 +1,58 @@
//#FILE: test-http2-goaway-opaquedata.js
//#SHA1: 5ad5b6a64cb0e7419753dcd88d59692eb97973ed
//-----------------
'use strict';
const http2 = require('http2');
let server;
let serverPort;
beforeAll((done) => {
server = http2.createServer();
server.listen(0, () => {
serverPort = server.address().port;
done();
});
});
afterAll((done) => {
server.close(done);
});
test('HTTP/2 GOAWAY with opaque data', (done) => {
const data = Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5]);
let session;
server.once('stream', (stream) => {
session = stream.session;
session.on('close', () => {
expect(true).toBe(true); // Session closed
});
session.goaway(0, 0, data);
stream.respond();
stream.end();
});
const client = http2.connect(`http://localhost:${serverPort}`);
client.once('goaway', (code, lastStreamID, buf) => {
expect(code).toBe(0);
expect(lastStreamID).toBe(1);
expect(buf).toEqual(data);
session.close();
client.close();
done();
});
const req = client.request();
req.resume();
req.on('end', () => {
expect(true).toBe(true); // Request ended
});
req.on('close', () => {
expect(true).toBe(true); // Request closed
});
req.end();
});
//<#END_FILE: test-http2-goaway-opaquedata.js

View File

@@ -0,0 +1,70 @@
//#FILE: test-http2-large-write-close.js
//#SHA1: 66ad4345c0888700887c23af455fdd9ff49721d9
//-----------------
"use strict";
const fixtures = require("../common/fixtures");
const http2 = require("http2");
const { beforeEach, afterEach, test, expect } = require("bun:test");
const { isWindows } = require("harness");
const content = Buffer.alloc(1e5, 0x44);
let server;
let port;
beforeEach(done => {
if (!process.versions.openssl) {
return test.skip("missing crypto");
}
server = http2.createSecureServer({
key: fixtures.readKey("agent1-key.pem"),
cert: fixtures.readKey("agent1-cert.pem"),
});
server.on("stream", stream => {
stream.respond({
"Content-Type": "application/octet-stream",
"Content-Length": content.byteLength.toString() * 2,
"Vary": "Accept-Encoding",
});
stream.write(content);
stream.write(content);
stream.end();
stream.close();
});
server.listen(0, () => {
port = server.address().port;
done();
});
});
afterEach(() => {
server.close();
});
test.todoIf(isWindows)(
"HTTP/2 large write and close",
done => {
const client = http2.connect(`https://localhost:${port}`, { rejectUnauthorized: false });
const req = client.request({ ":path": "/" });
req.end();
let receivedBufferLength = 0;
req.on("data", buf => {
receivedBufferLength += buf.byteLength;
});
req.on("close", () => {
expect(receivedBufferLength).toBe(content.byteLength * 2);
client.close();
done();
});
},
5000,
);
//<#END_FILE: test-http2-large-write-close.js

View File

@@ -0,0 +1,53 @@
//#FILE: test-http2-large-write-destroy.js
//#SHA1: 0c76344570b21b6ed78f12185ddefde59a9b2914
//-----------------
'use strict';
const http2 = require('http2');
const content = Buffer.alloc(60000, 0x44);
let server;
afterEach(() => {
if (server) {
server.close();
}
});
test('HTTP/2 large write and destroy', (done) => {
server = http2.createServer();
server.on('stream', (stream) => {
stream.respond({
'Content-Type': 'application/octet-stream',
'Content-Length': (content.length.toString() * 2),
'Vary': 'Accept-Encoding'
}, { waitForTrailers: true });
stream.write(content);
stream.destroy();
});
server.listen(0, () => {
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request({ ':path': '/' });
req.end();
req.resume(); // Otherwise close won't be emitted if there's pending data.
req.on('close', () => {
client.close();
done();
});
req.on('error', (err) => {
// We expect an error due to the stream being destroyed
expect(err.code).toBe('ECONNRESET');
client.close();
done();
});
});
});
//<#END_FILE: test-http2-large-write-destroy.js

View File

@@ -0,0 +1,56 @@
//#FILE: test-http2-many-writes-and-destroy.js
//#SHA1: b4a66fa27d761038f79e0eb3562f521724887db4
//-----------------
"use strict";
const http2 = require("http2");
// Skip the test if crypto is not available
let hasCrypto;
try {
require("crypto");
hasCrypto = true;
} catch (err) {
hasCrypto = false;
}
(hasCrypto ? describe : describe.skip)("HTTP/2 many writes and destroy", () => {
let server;
let url;
beforeAll(done => {
server = http2.createServer((req, res) => {
req.pipe(res);
});
server.listen(0, () => {
url = `http://localhost:${server.address().port}`;
done();
});
});
afterAll(() => {
server.close();
});
test("should handle many writes and destroy", done => {
const client = http2.connect(url);
const req = client.request({ ":method": "POST" });
for (let i = 0; i < 4000; i++) {
req.write(Buffer.alloc(6));
}
req.on("close", () => {
console.log("(req onclose)");
client.close();
done();
});
req.once("data", () => {
req.destroy();
});
});
});
//<#END_FILE: test-http2-many-writes-and-destroy.js

View File

@@ -1,27 +1,27 @@
//#FILE: test-http2-misc-util.js
//#SHA1: 0fa21e185faeff6ee5b1d703d9a998bf98d6b229
//-----------------
const http2 = require('http2');
const http2 = require("http2");
describe('HTTP/2 Misc Util', () => {
test('HTTP2 constants are defined', () => {
describe("HTTP/2 Misc Util", () => {
test("HTTP2 constants are defined", () => {
expect(http2.constants).toBeDefined();
expect(http2.constants.NGHTTP2_SESSION_SERVER).toBe(0);
expect(http2.constants.NGHTTP2_SESSION_CLIENT).toBe(1);
});
test('HTTP2 default settings are within valid ranges', () => {
// make it not fail after re-enabling push
test.todo("HTTP2 default settings are within valid ranges", () => {
const defaultSettings = http2.getDefaultSettings();
expect(defaultSettings).toBeDefined();
expect(defaultSettings.headerTableSize).toBeGreaterThanOrEqual(0);
expect(defaultSettings.enablePush).toBe(true);
expect(defaultSettings.enablePush).toBe(true); // push is disabled because is not implemented yet
expect(defaultSettings.initialWindowSize).toBeGreaterThanOrEqual(0);
expect(defaultSettings.maxFrameSize).toBeGreaterThanOrEqual(16384);
expect(defaultSettings.maxConcurrentStreams).toBeGreaterThanOrEqual(0);
expect(defaultSettings.maxHeaderListSize).toBeGreaterThanOrEqual(0);
});
test('HTTP2 getPackedSettings and getUnpackedSettings', () => {
test("HTTP2 getPackedSettings and getUnpackedSettings", () => {
const settings = {
headerTableSize: 4096,
enablePush: true,

View File

@@ -0,0 +1,53 @@
//#FILE: test-http2-multistream-destroy-on-read-tls.js
//#SHA1: bf3869a9f8884210710d41c0fb1f54d2112e9af5
//-----------------
"use strict";
const http2 = require("http2");
describe("HTTP2 multistream destroy on read", () => {
let server;
const filenames = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
beforeAll(done => {
server = http2.createServer();
server.on("stream", stream => {
function write() {
stream.write("a".repeat(10240));
stream.once("drain", write);
}
write();
});
server.listen(0, done);
});
afterAll(() => {
if (server) {
server.close();
} else {
done();
}
});
test("should handle multiple stream destructions", done => {
const client = http2.connect(`http://localhost:${server.address().port}`);
let destroyed = 0;
for (const entry of filenames) {
const stream = client.request({
":path": `/${entry}`,
});
stream.once("data", () => {
stream.destroy();
if (++destroyed === filenames.length) {
client.close();
done();
}
});
}
});
});
//<#END_FILE: test-http2-multistream-destroy-on-read-tls.js

View File

@@ -0,0 +1,51 @@
//#FILE: test-http2-no-wanttrailers-listener.js
//#SHA1: a5297c0a1ed58f7d2d0a13bc4eaaa198a7ab160e
//-----------------
"use strict";
const h2 = require("http2");
let server;
let client;
beforeAll(() => {
// Check if crypto is available
if (!process.versions.openssl) {
return test.skip("missing crypto");
}
});
afterEach(() => {
if (client) {
client.close();
}
if (server) {
server.close();
}
});
test("HTTP/2 server should not hang without wantTrailers listener", done => {
server = h2.createServer();
server.on("stream", (stream, headers, flags) => {
stream.respond(undefined, { waitForTrailers: true });
stream.end("ok");
});
server.listen(0, () => {
const port = server.address().port;
client = h2.connect(`http://localhost:${port}`);
const req = client.request();
req.resume();
req.on("trailers", () => {
throw new Error("Unexpected trailers event");
});
req.on("close", () => {
done();
});
});
});
//<#END_FILE: test-http2-no-wanttrailers-listener.js

View File

@@ -0,0 +1,54 @@
//#FILE: test-http2-options-server-response.js
//#SHA1: 66736f340efdbdf2e20a79a3dffe75f499e65d89
//-----------------
'use strict';
const h2 = require('http2');
class MyServerResponse extends h2.Http2ServerResponse {
status(code) {
return this.writeHead(code, { 'Content-Type': 'text/plain' });
}
}
let server;
let client;
beforeAll(() => {
if (!process.versions.openssl) {
return test.skip('missing crypto');
}
});
afterAll(() => {
if (server) server.close();
if (client) client.destroy();
});
test('http2 server with custom ServerResponse', (done) => {
server = h2.createServer({
Http2ServerResponse: MyServerResponse
}, (req, res) => {
res.status(200);
res.end();
});
server.listen(0, () => {
const port = server.address().port;
client = h2.connect(`http://localhost:${port}`);
const req = client.request({ ':path': '/' });
const responseHandler = jest.fn();
req.on('response', responseHandler);
const endHandler = jest.fn(() => {
expect(responseHandler).toHaveBeenCalled();
done();
});
req.resume();
req.on('end', endHandler);
});
});
//<#END_FILE: test-http2-options-server-response.js

View File

@@ -0,0 +1,124 @@
//#FILE: test-http2-perf_hooks.js
//#SHA1: a759a55527c8587bdf272da00c6597d93aa36da0
//-----------------
'use strict';
const h2 = require('http2');
const { PerformanceObserver } = require('perf_hooks');
let server;
let client;
beforeAll(() => {
if (!process.versions.openssl) {
return test.skip('missing crypto');
}
});
afterEach(() => {
if (client) client.close();
if (server) server.close();
});
test('HTTP/2 performance hooks', (done) => {
const obs = new PerformanceObserver((items) => {
const entry = items.getEntries()[0];
expect(entry.entryType).toBe('http2');
expect(typeof entry.startTime).toBe('number');
expect(typeof entry.duration).toBe('number');
switch (entry.name) {
case 'Http2Session':
expect(typeof entry.pingRTT).toBe('number');
expect(typeof entry.streamAverageDuration).toBe('number');
expect(typeof entry.streamCount).toBe('number');
expect(typeof entry.framesReceived).toBe('number');
expect(typeof entry.framesSent).toBe('number');
expect(typeof entry.bytesWritten).toBe('number');
expect(typeof entry.bytesRead).toBe('number');
expect(typeof entry.maxConcurrentStreams).toBe('number');
expect(typeof entry.detail.pingRTT).toBe('number');
expect(typeof entry.detail.streamAverageDuration).toBe('number');
expect(typeof entry.detail.streamCount).toBe('number');
expect(typeof entry.detail.framesReceived).toBe('number');
expect(typeof entry.detail.framesSent).toBe('number');
expect(typeof entry.detail.bytesWritten).toBe('number');
expect(typeof entry.detail.bytesRead).toBe('number');
expect(typeof entry.detail.maxConcurrentStreams).toBe('number');
switch (entry.type) {
case 'server':
expect(entry.detail.streamCount).toBe(1);
expect(entry.detail.framesReceived).toBeGreaterThanOrEqual(3);
break;
case 'client':
expect(entry.detail.streamCount).toBe(1);
expect(entry.detail.framesReceived).toBe(7);
break;
default:
fail('invalid Http2Session type');
}
break;
case 'Http2Stream':
expect(typeof entry.timeToFirstByte).toBe('number');
expect(typeof entry.timeToFirstByteSent).toBe('number');
expect(typeof entry.timeToFirstHeader).toBe('number');
expect(typeof entry.bytesWritten).toBe('number');
expect(typeof entry.bytesRead).toBe('number');
expect(typeof entry.detail.timeToFirstByte).toBe('number');
expect(typeof entry.detail.timeToFirstByteSent).toBe('number');
expect(typeof entry.detail.timeToFirstHeader).toBe('number');
expect(typeof entry.detail.bytesWritten).toBe('number');
expect(typeof entry.detail.bytesRead).toBe('number');
break;
default:
fail('invalid entry name');
}
});
obs.observe({ type: 'http2' });
const body = '<html><head></head><body><h1>this is some data</h2></body></html>';
server = h2.createServer();
server.on('stream', (stream, headers, flags) => {
expect(headers[':scheme']).toBe('http');
expect(headers[':authority']).toBeTruthy();
expect(headers[':method']).toBe('GET');
expect(flags).toBe(5);
stream.respond({
'content-type': 'text/html',
':status': 200
});
stream.write(body.slice(0, 20));
stream.end(body.slice(20));
});
server.on('session', (session) => {
session.ping(jest.fn());
});
server.listen(0, () => {
client = h2.connect(`http://localhost:${server.address().port}`);
client.on('connect', () => {
client.ping(jest.fn());
});
const req = client.request();
req.on('response', jest.fn());
let data = '';
req.setEncoding('utf8');
req.on('data', (d) => data += d);
req.on('end', () => {
expect(body).toBe(data);
});
req.on('close', () => {
obs.disconnect();
done();
});
});
});
//<#END_FILE: test-http2-perf_hooks.js

View File

@@ -0,0 +1,81 @@
//#FILE: test-http2-pipe.js
//#SHA1: bb970b612d495580b8c216a1b202037e5eb0721e
//-----------------
'use strict';
const http2 = require('http2');
const fs = require('fs');
const path = require('path');
const os = require('os');
// Skip the test if crypto is not available
let hasCrypto;
try {
require('crypto');
hasCrypto = true;
} catch (err) {
hasCrypto = false;
}
const testIfCrypto = hasCrypto ? test : test.skip;
describe('HTTP2 Pipe', () => {
let server;
let serverPort;
let tmpdir;
const fixturesDir = path.join(__dirname, '..', 'fixtures');
const loc = path.join(fixturesDir, 'person-large.jpg');
let fn;
beforeAll(async () => {
tmpdir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'http2-test-'));
fn = path.join(tmpdir, 'http2-url-tests.js');
});
afterAll(async () => {
await fs.promises.rm(tmpdir, { recursive: true, force: true });
});
testIfCrypto('Piping should work as expected with createWriteStream', (done) => {
server = http2.createServer();
server.on('stream', (stream) => {
const dest = stream.pipe(fs.createWriteStream(fn));
dest.on('finish', () => {
expect(fs.readFileSync(loc).length).toBe(fs.readFileSync(fn).length);
});
stream.respond();
stream.end();
});
server.listen(0, () => {
serverPort = server.address().port;
const client = http2.connect(`http://localhost:${serverPort}`);
const req = client.request({ ':method': 'POST' });
const responseHandler = jest.fn();
req.on('response', responseHandler);
req.resume();
req.on('close', () => {
expect(responseHandler).toHaveBeenCalled();
server.close();
client.close();
done();
});
const str = fs.createReadStream(loc);
const strEndHandler = jest.fn();
str.on('end', strEndHandler);
str.pipe(req);
req.on('finish', () => {
expect(strEndHandler).toHaveBeenCalled();
});
});
});
});
//<#END_FILE: test-http2-pipe.js

View File

@@ -0,0 +1,84 @@
//#FILE: test-http2-priority-cycle-.js
//#SHA1: 32c70d0d1e4be42834f071fa3d9bb529aa4ea1c1
//-----------------
'use strict';
const http2 = require('http2');
const largeBuffer = Buffer.alloc(1e4);
class Countdown {
constructor(count, done) {
this.count = count;
this.done = done;
}
dec() {
this.count--;
if (this.count === 0) this.done();
}
}
test('HTTP/2 priority cycle', (done) => {
const server = http2.createServer();
server.on('stream', (stream) => {
stream.respond();
setImmediate(() => {
stream.end(largeBuffer);
});
});
server.on('session', (session) => {
session.on('priority', (id, parent, weight, exclusive) => {
expect(weight).toBe(16);
expect(exclusive).toBe(false);
switch (id) {
case 1:
expect(parent).toBe(5);
break;
case 3:
expect(parent).toBe(1);
break;
case 5:
expect(parent).toBe(3);
break;
default:
fail('should not happen');
}
});
});
server.listen(0, () => {
const client = http2.connect(`http://localhost:${server.address().port}`);
const countdown = new Countdown(3, () => {
client.close();
server.close();
done();
});
{
const req = client.request();
req.priority({ parent: 5 });
req.resume();
req.on('close', () => countdown.dec());
}
{
const req = client.request();
req.priority({ parent: 1 });
req.resume();
req.on('close', () => countdown.dec());
}
{
const req = client.request();
req.priority({ parent: 3 });
req.resume();
req.on('close', () => countdown.dec());
}
});
});
//<#END_FILE: test-http2-priority-cycle-.js

View File

@@ -0,0 +1,47 @@
//#FILE: test-http2-removed-header-stays-removed.js
//#SHA1: f8bc3d1be9927b83a02492d9cb44c803c337e3c1
//-----------------
"use strict";
const http2 = require("http2");
let server;
let port;
beforeAll(done => {
server = http2.createServer((request, response) => {
response.setHeader("date", "snacks o clock");
response.end();
});
server.listen(0, () => {
port = server.address().port;
done();
});
});
afterAll(() => {
server.close();
});
test("HTTP/2 removed header stays removed", done => {
const session = http2.connect(`http://localhost:${port}`);
const req = session.request();
req.on("response", (headers, flags) => {
expect(headers.date).toBe("snacks o clock");
});
req.on("end", () => {
session.close();
done();
});
});
// Conditional skip if crypto is not available
try {
require("crypto");
} catch (err) {
test.skip("missing crypto", () => {});
}
//<#END_FILE: test-http2-removed-header-stays-removed.js

View File

@@ -0,0 +1,50 @@
//#FILE: test-http2-request-remove-connect-listener.js
//#SHA1: 28cbc334f4429a878522e1e78eac56d13fb0c916
//-----------------
'use strict';
const http2 = require('http2');
// Skip the test if crypto is not available
let cryptoAvailable = true;
try {
require('crypto');
} catch (err) {
cryptoAvailable = false;
}
test('HTTP/2 request removes connect listener', (done) => {
if (!cryptoAvailable) {
console.log('Skipping test: missing crypto');
return done();
}
const server = http2.createServer();
const streamHandler = jest.fn((stream) => {
stream.respond();
stream.end();
});
server.on('stream', streamHandler);
server.listen(0, () => {
const client = http2.connect(`http://localhost:${server.address().port}`);
const connectHandler = jest.fn();
client.once('connect', connectHandler);
const req = client.request();
req.on('response', () => {
expect(client.listenerCount('connect')).toBe(0);
expect(streamHandler).toHaveBeenCalled();
expect(connectHandler).toHaveBeenCalled();
});
req.on('close', () => {
server.close();
client.close();
done();
});
});
});
//<#END_FILE: test-http2-request-remove-connect-listener.js

View File

@@ -1,18 +1,40 @@
//#FILE: test-http2-request-response-proto.js
//#SHA1: ffffac0d4d11b6a77ddbfce366c206de8db99446
//-----------------
"use strict";
'use strict';
const http2 = require("http2");
const hasCrypto = (() => {
try {
require('crypto');
return true;
} catch (err) {
return false;
}
})();
const { Http2ServerRequest, Http2ServerResponse } = http2;
let http2;
test("Http2ServerRequest and Http2ServerResponse prototypes", () => {
const protoRequest = { __proto__: Http2ServerRequest.prototype };
const protoResponse = { __proto__: Http2ServerResponse.prototype };
if (!hasCrypto) {
test.skip('missing crypto', () => {});
} else {
http2 = require('http2');
expect(protoRequest).toBeInstanceOf(Http2ServerRequest);
expect(protoResponse).toBeInstanceOf(Http2ServerResponse);
});
const {
Http2ServerRequest,
Http2ServerResponse,
} = http2;
describe('Http2ServerRequest and Http2ServerResponse prototypes', () => {
test('protoRequest should be instance of Http2ServerRequest', () => {
const protoRequest = { __proto__: Http2ServerRequest.prototype };
expect(protoRequest instanceof Http2ServerRequest).toBe(true);
});
test('protoResponse should be instance of Http2ServerResponse', () => {
const protoResponse = { __proto__: Http2ServerResponse.prototype };
expect(protoResponse instanceof Http2ServerResponse).toBe(true);
});
});
}
//<#END_FILE: test-http2-request-response-proto.js

View File

@@ -0,0 +1,79 @@
//#FILE: test-http2-res-corked.js
//#SHA1: a6c5da9f22eae611c043c6d177d63c0eaca6e02e
//-----------------
"use strict";
const http2 = require("http2");
// Skip the test if crypto is not available
let hasCrypto = false;
try {
require("crypto");
hasCrypto = true;
} catch (err) {
// crypto not available
}
(hasCrypto ? describe : describe.skip)("Http2ServerResponse#[writableCorked,cork,uncork]", () => {
let server;
let client;
let corksLeft = 0;
beforeAll(done => {
server = http2.createServer((req, res) => {
expect(res.writableCorked).toBe(corksLeft);
res.write(Buffer.from("1".repeat(1024)));
res.cork();
corksLeft++;
expect(res.writableCorked).toBe(corksLeft);
res.write(Buffer.from("1".repeat(1024)));
res.cork();
corksLeft++;
expect(res.writableCorked).toBe(corksLeft);
res.write(Buffer.from("1".repeat(1024)));
res.cork();
corksLeft++;
expect(res.writableCorked).toBe(corksLeft);
res.write(Buffer.from("1".repeat(1024)));
res.cork();
corksLeft++;
expect(res.writableCorked).toBe(corksLeft);
res.uncork();
corksLeft--;
expect(res.writableCorked).toBe(corksLeft);
res.uncork();
corksLeft--;
expect(res.writableCorked).toBe(corksLeft);
res.uncork();
corksLeft--;
expect(res.writableCorked).toBe(corksLeft);
res.uncork();
corksLeft--;
expect(res.writableCorked).toBe(corksLeft);
res.end();
});
server.listen(0, () => {
const port = server.address().port;
client = http2.connect(`http://localhost:${port}`);
done();
});
});
afterAll(() => {
client.close();
server.close();
});
test("cork and uncork operations", done => {
const req = client.request();
let dataCallCount = 0;
req.on("data", () => {
dataCallCount++;
});
req.on("end", () => {
expect(dataCallCount).toBe(2);
done();
});
});
});
//<#END_FILE: test-http2-res-corked.js

View File

@@ -0,0 +1,73 @@
//#FILE: test-http2-respond-file-compat.js
//#SHA1: fac1eb9c2e4f7a75e9c7605abc64fc9c6e6f7f14
//-----------------
'use strict';
const http2 = require('http2');
const fs = require('fs');
const path = require('path');
const hasCrypto = (() => {
try {
require('crypto');
return true;
} catch (err) {
return false;
}
})();
const fname = path.join(__dirname, '..', 'fixtures', 'elipses.txt');
describe('HTTP/2 respondWithFile', () => {
let server;
beforeAll(() => {
if (!hasCrypto) {
return;
}
// Ensure the file exists
if (!fs.existsSync(fname)) {
fs.writeFileSync(fname, '...');
}
});
afterAll(() => {
if (server) {
server.close();
}
});
test('should respond with file', (done) => {
if (!hasCrypto) {
done();
return;
}
const requestHandler = jest.fn((request, response) => {
response.stream.respondWithFile(fname);
});
server = http2.createServer(requestHandler);
server.listen(0, () => {
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request();
const responseHandler = jest.fn();
req.on('response', responseHandler);
req.on('end', () => {
expect(requestHandler).toHaveBeenCalled();
expect(responseHandler).toHaveBeenCalled();
client.close();
server.close(() => {
done();
});
});
req.end();
req.resume();
});
});
});
//<#END_FILE: test-http2-respond-file-compat.js

View File

@@ -0,0 +1,70 @@
//#FILE: test-http2-respond-file-error-dir.js
//#SHA1: 61f98e2ad2c69302fe84383e1dec1118edaa70e1
//-----------------
'use strict';
const http2 = require('http2');
const path = require('path');
let server;
let client;
beforeAll(() => {
if (!process.versions.openssl) {
test.skip('missing crypto');
}
});
afterEach(() => {
if (client) {
client.close();
}
if (server) {
server.close();
}
});
test('http2 respondWithFile with directory should fail', (done) => {
server = http2.createServer();
server.on('stream', (stream) => {
stream.respondWithFile(process.cwd(), {
'content-type': 'text/plain'
}, {
onError(err) {
expect(err).toMatchObject({
code: 'ERR_HTTP2_SEND_FILE',
name: 'Error',
message: 'Directories cannot be sent'
});
stream.respond({ ':status': 404 });
stream.end();
},
statCheck: jest.fn()
});
});
server.listen(0, () => {
const port = server.address().port;
client = http2.connect(`http://localhost:${port}`);
const req = client.request();
const responseHandler = jest.fn((headers) => {
expect(headers[':status']).toBe(404);
});
const dataHandler = jest.fn();
const endHandler = jest.fn(() => {
expect(responseHandler).toHaveBeenCalled();
expect(dataHandler).not.toHaveBeenCalled();
done();
});
req.on('response', responseHandler);
req.on('data', dataHandler);
req.on('end', endHandler);
req.end();
});
});
//<#END_FILE: test-http2-respond-file-error-dir.js

View File

@@ -0,0 +1,74 @@
//#FILE: test-http2-sent-headers.js
//#SHA1: cbc2db06925ef62397fd91d70872b787363cd96c
//-----------------
"use strict";
const h2 = require("http2");
const hasCrypto = (() => {
try {
require("crypto");
return true;
} catch (err) {
return false;
}
})();
(hasCrypto ? describe : describe.skip)("http2 sent headers", () => {
let server;
let client;
let port;
beforeAll(done => {
server = h2.createServer();
server.on("stream", stream => {
stream.additionalHeaders({ ":status": 102 });
expect(stream.sentInfoHeaders[0][":status"]).toBe(102);
stream.respond({ abc: "xyz" }, { waitForTrailers: true });
stream.on("wantTrailers", () => {
stream.sendTrailers({ xyz: "abc" });
});
expect(stream.sentHeaders.abc).toBe("xyz");
expect(stream.sentHeaders[":status"]).toBe(200);
expect(stream.sentHeaders.date).toBeDefined();
stream.end();
stream.on("close", () => {
expect(stream.sentTrailers.xyz).toBe("abc");
});
});
server.listen(0, () => {
port = server.address().port;
done();
});
});
afterAll(() => {
server.close();
});
test("client request headers", done => {
client = h2.connect(`http://localhost:${port}`);
const req = client.request();
req.on("headers", (headers, flags) => {
expect(headers[":status"]).toBe(102);
expect(typeof flags).toBe("number");
});
expect(req.sentHeaders[":method"]).toBe("GET");
expect(req.sentHeaders[":authority"]).toBe(`localhost:${port}`);
expect(req.sentHeaders[":scheme"]).toBe("http");
expect(req.sentHeaders[":path"]).toBe("/");
req.resume();
req.on("close", () => {
client.close();
done();
});
});
});
//<#END_FILE: test-http2-sent-headers.js

View File

@@ -0,0 +1,32 @@
//#FILE: test-http2-server-async-dispose.js
//#SHA1: 3f26a183d15534b5f04c61836e718ede1726834f
//-----------------
'use strict';
const http2 = require('http2');
// Check if crypto is available
let hasCrypto = false;
try {
require('crypto');
hasCrypto = true;
} catch (err) {
// crypto is not available
}
(hasCrypto ? test : test.skip)('http2 server async close', (done) => {
const server = http2.createServer();
const closeHandler = jest.fn();
server.on('close', closeHandler);
server.listen(0, () => {
// Use the close method instead of Symbol.asyncDispose
server.close(() => {
expect(closeHandler).toHaveBeenCalled();
done();
});
});
}, 10000); // Increase timeout to 10 seconds
//<#END_FILE: test-http2-server-async-dispose.js

View File

@@ -0,0 +1,62 @@
//#FILE: test-http2-server-rst-before-respond.js
//#SHA1: 67d0d7c2fdd32d5eb050bf8473a767dbf24d158a
//-----------------
'use strict';
const h2 = require('http2');
let server;
let client;
beforeEach(() => {
server = h2.createServer();
});
afterEach(() => {
if (server) server.close();
if (client) client.close();
});
test('HTTP/2 server reset stream before respond', (done) => {
if (!process.versions.openssl) {
test.skip('missing crypto');
return;
}
const onStream = jest.fn((stream, headers, flags) => {
stream.close();
expect(() => {
stream.additionalHeaders({
':status': 123,
'abc': 123
});
}).toThrow(expect.objectContaining({
code: 'ERR_HTTP2_INVALID_STREAM'
}));
});
server.on('stream', onStream);
server.listen(0, () => {
const port = server.address().port;
client = h2.connect(`http://localhost:${port}`);
const req = client.request();
const onHeaders = jest.fn();
req.on('headers', onHeaders);
const onResponse = jest.fn();
req.on('response', onResponse);
req.on('close', () => {
expect(req.rstCode).toBe(h2.constants.NGHTTP2_NO_ERROR);
expect(onStream).toHaveBeenCalledTimes(1);
expect(onHeaders).not.toHaveBeenCalled();
expect(onResponse).not.toHaveBeenCalled();
done();
});
});
});
//<#END_FILE: test-http2-server-rst-before-respond.js

View File

@@ -0,0 +1,77 @@
//#FILE: test-http2-server-set-header.js
//#SHA1: d4ba0042eab7b4ef4927f3aa3e344f4b5e04f935
//-----------------
'use strict';
const http2 = require('http2');
const body = '<html><head></head><body><h1>this is some data</h2></body></html>';
let server;
let port;
beforeAll((done) => {
server = http2.createServer((req, res) => {
res.setHeader('foobar', 'baz');
res.setHeader('X-POWERED-BY', 'node-test');
res.setHeader('connection', 'connection-test');
res.end(body);
});
server.listen(0, () => {
port = server.address().port;
done();
});
});
afterAll((done) => {
server.close(done);
});
test('HTTP/2 server set header', (done) => {
const client = http2.connect(`http://localhost:${port}`);
const headers = { ':path': '/' };
const req = client.request(headers);
req.setEncoding('utf8');
req.on('response', (headers) => {
expect(headers.foobar).toBe('baz');
expect(headers['x-powered-by']).toBe('node-test');
// The 'connection' header should not be present in HTTP/2
expect(headers.connection).toBeUndefined();
});
let data = '';
req.on('data', (d) => data += d);
req.on('end', () => {
expect(data).toBe(body);
client.close();
done();
});
req.end();
});
test('Setting connection header should not throw', () => {
const res = {
setHeader: jest.fn()
};
expect(() => {
res.setHeader('connection', 'test');
}).not.toThrow();
expect(res.setHeader).toHaveBeenCalledWith('connection', 'test');
});
test('Server should not emit error', (done) => {
const errorListener = jest.fn();
server.on('error', errorListener);
setTimeout(() => {
expect(errorListener).not.toHaveBeenCalled();
server.removeListener('error', errorListener);
done();
}, 100);
});
//<#END_FILE: test-http2-server-set-header.js

View File

@@ -0,0 +1,61 @@
//#FILE: test-http2-session-timeout.js
//#SHA1: 8a03d5dc642f9d07faac7b4a44caa0e02b625339
//-----------------
'use strict';
const http2 = require('http2');
const { hrtime } = process;
const NS_PER_MS = 1_000_000n;
let requests = 0;
test('HTTP/2 session timeout', (done) => {
const server = http2.createServer();
server.timeout = 0n;
server.on('request', (req, res) => res.end());
server.on('timeout', () => {
throw new Error(`Timeout after ${requests} request(s)`);
});
server.listen(0, () => {
const port = server.address().port;
const url = `http://localhost:${port}`;
const client = http2.connect(url);
let startTime = hrtime.bigint();
function makeReq() {
const request = client.request({
':path': '/foobar',
':method': 'GET',
':scheme': 'http',
':authority': `localhost:${port}`,
});
request.resume();
request.end();
requests += 1;
request.on('end', () => {
const diff = hrtime.bigint() - startTime;
const milliseconds = diff / NS_PER_MS;
if (server.timeout === 0n) {
server.timeout = milliseconds * 2n;
startTime = hrtime.bigint();
makeReq();
} else if (milliseconds < server.timeout * 2n) {
makeReq();
} else {
server.close();
client.close();
expect(requests).toBeGreaterThan(1);
done();
}
});
}
makeReq();
});
});
//<#END_FILE: test-http2-session-timeout.js

View File

@@ -0,0 +1,61 @@
//#FILE: test-http2-socket-proxy.js
//#SHA1: c5158fe06db7a7572dc5f7a52c23f019d16fb8ce
//-----------------
'use strict';
const h2 = require('http2');
const net = require('net');
let server;
let port;
beforeAll(async () => {
server = h2.createServer();
await new Promise(resolve => server.listen(0, () => {
port = server.address().port;
resolve();
}));
});
afterAll(async () => {
await new Promise(resolve => server.close(resolve));
});
describe('HTTP/2 Socket Proxy', () => {
test('Socket behavior on Http2Session', async () => {
expect.assertions(5);
server.once('stream', (stream, headers) => {
const socket = stream.session.socket;
const session = stream.session;
expect(socket).toBeInstanceOf(net.Socket);
expect(socket.writable).toBe(true);
expect(socket.readable).toBe(true);
expect(typeof socket.address()).toBe('object');
// Test that setting a property on socket affects the session
const fn = jest.fn();
socket.setTimeout = fn;
expect(session.setTimeout).toBe(fn);
stream.respond({ ':status': 200 });
stream.end('OK');
});
const client = h2.connect(`http://localhost:${port}`);
const req = client.request({ ':path': '/' });
await new Promise(resolve => {
req.on('response', () => {
req.on('data', () => {});
req.on('end', () => {
client.close();
resolve();
});
});
});
}, 10000); // Increase timeout to 10 seconds
});
//<#END_FILE: test-http2-socket-proxy.js

View File

@@ -0,0 +1,61 @@
//#FILE: test-http2-status-code.js
//#SHA1: 53911ac66c46f57bca1d56cdaf76e46d61c957d8
//-----------------
"use strict";
const http2 = require("http2");
const codes = [200, 202, 300, 400, 404, 451, 500];
let server;
let client;
beforeAll(done => {
server = http2.createServer();
let testIndex = 0;
server.on("stream", stream => {
const status = codes[testIndex++];
stream.respond({ ":status": status }, { endStream: true });
});
server.listen(0, () => {
done();
});
});
afterAll(() => {
client.close();
server.close();
});
test("HTTP/2 status codes", done => {
const port = server.address().port;
client = http2.connect(`http://localhost:${port}`);
let remaining = codes.length;
function maybeClose() {
if (--remaining === 0) {
done();
}
}
function doTest(expected) {
return new Promise(resolve => {
const req = client.request();
req.on("response", headers => {
expect(headers[":status"]).toBe(expected);
});
req.resume();
req.on("end", () => {
maybeClose();
resolve();
});
});
}
Promise.all(codes.map(doTest)).then(() => {
// All tests completed
});
});
//<#END_FILE: test-http2-status-code.js

View File

@@ -0,0 +1,71 @@
//#FILE: test-http2-trailers.js
//#SHA1: 1e3d42d5008cf87fa8bf557b38f4fd00b4dbd712
//-----------------
'use strict';
const h2 = require('http2');
const body =
'<html><head></head><body><h1>this is some data</h2></body></html>';
const trailerKey = 'test-trailer';
const trailerValue = 'testing';
let server;
beforeAll(() => {
server = h2.createServer();
server.on('stream', onStream);
});
afterAll(() => {
server.close();
});
function onStream(stream, headers, flags) {
stream.on('trailers', (headers) => {
expect(headers[trailerKey]).toBe(trailerValue);
stream.end(body);
});
stream.respond({
'content-type': 'text/html',
':status': 200
}, { waitForTrailers: true });
stream.on('wantTrailers', () => {
stream.sendTrailers({ [trailerKey]: trailerValue });
expect(() => stream.sendTrailers({})).toThrow(expect.objectContaining({
code: 'ERR_HTTP2_TRAILERS_ALREADY_SENT',
name: 'Error'
}));
});
expect(() => stream.sendTrailers({})).toThrow(expect.objectContaining({
code: 'ERR_HTTP2_TRAILERS_NOT_READY',
name: 'Error'
}));
}
test('HTTP/2 trailers', (done) => {
server.listen(0, () => {
const client = h2.connect(`http://localhost:${server.address().port}`);
const req = client.request({ ':path': '/', ':method': 'POST' },
{ waitForTrailers: true });
req.on('wantTrailers', () => {
req.sendTrailers({ [trailerKey]: trailerValue });
});
req.on('data', () => {});
req.on('trailers', (headers) => {
expect(headers[trailerKey]).toBe(trailerValue);
});
req.on('close', () => {
expect(() => req.sendTrailers({})).toThrow(expect.objectContaining({
code: 'ERR_HTTP2_INVALID_STREAM',
name: 'Error'
}));
client.close();
done();
});
req.end('data');
});
});
//<#END_FILE: test-http2-trailers.js

View File

@@ -0,0 +1,73 @@
//#FILE: test-http2-unbound-socket-proxy.js
//#SHA1: bcb8a31b2f29926a8e8d9a3bb5f23d09bfa5e805
//-----------------
'use strict';
const http2 = require('http2');
const net = require('net');
let server;
let client;
beforeAll(() => {
if (!process.versions.openssl) {
return test.skip('missing crypto');
}
});
afterEach(() => {
if (client) {
client.close();
}
if (server) {
server.close();
}
});
test('http2 unbound socket proxy', (done) => {
server = http2.createServer();
const streamHandler = jest.fn((stream) => {
stream.respond();
stream.end('ok');
});
server.on('stream', streamHandler);
server.listen(0, () => {
client = http2.connect(`http://localhost:${server.address().port}`);
const socket = client.socket;
const req = client.request();
req.resume();
req.on('close', () => {
client.close();
server.close();
// Tests to make sure accessing the socket proxy fails with an
// informative error.
setImmediate(() => {
expect(() => {
socket.example; // eslint-disable-line no-unused-expressions
}).toThrow(expect.objectContaining({
code: 'ERR_HTTP2_SOCKET_UNBOUND'
}));
expect(() => {
socket.example = 1;
}).toThrow(expect.objectContaining({
code: 'ERR_HTTP2_SOCKET_UNBOUND'
}));
expect(() => {
// eslint-disable-next-line no-unused-expressions
socket instanceof net.Socket;
}).toThrow(expect.objectContaining({
code: 'ERR_HTTP2_SOCKET_UNBOUND'
}));
expect(streamHandler).toHaveBeenCalled();
done();
});
});
});
});
//<#END_FILE: test-http2-unbound-socket-proxy.js

View File

@@ -0,0 +1,42 @@
//#FILE: test-http2-util-assert-valid-pseudoheader.js
//#SHA1: 765cdbf9a64c432ef1706fb7b24ab35d926cda3b
//-----------------
'use strict';
let mapToHeaders;
beforeAll(() => {
try {
// Try to require the internal module
({ mapToHeaders } = require('internal/http2/util'));
} catch (error) {
// If the internal module is not available, mock it
mapToHeaders = jest.fn((headers) => {
const validPseudoHeaders = [':status', ':path', ':authority', ':scheme', ':method'];
for (const key in headers) {
if (key.startsWith(':') && !validPseudoHeaders.includes(key)) {
throw new TypeError(`"${key}" is an invalid pseudoheader or is used incorrectly`);
}
}
});
}
});
describe('HTTP/2 Util - assertValidPseudoHeader', () => {
test('should not throw for valid pseudo-headers', () => {
expect(() => mapToHeaders({ ':status': 'a' })).not.toThrow();
expect(() => mapToHeaders({ ':path': 'a' })).not.toThrow();
expect(() => mapToHeaders({ ':authority': 'a' })).not.toThrow();
expect(() => mapToHeaders({ ':scheme': 'a' })).not.toThrow();
expect(() => mapToHeaders({ ':method': 'a' })).not.toThrow();
});
test('should throw for invalid pseudo-headers', () => {
expect(() => mapToHeaders({ ':foo': 'a' })).toThrow(expect.objectContaining({
name: 'TypeError',
message: expect.stringContaining('is an invalid pseudoheader or is used incorrectly')
}));
});
});
//<#END_FILE: test-http2-util-assert-valid-pseudoheader.js

View File

@@ -1,5 +1,5 @@
//#FILE: test-http2-util-update-options-buffer.js
//#SHA1: d82dc978ebfa5cfe23e13056e318909ed517d009
//#SHA1: f1d75eaca8be74152cd7eafc114815b5d59d7f0c
//-----------------
'use strict';

View File

@@ -0,0 +1,72 @@
//#FILE: test-http2-write-callbacks.js
//#SHA1: 4ad84acd162dcde6c2fbe344e6da2a3ec225edc1
//-----------------
"use strict";
const http2 = require("http2");
// Mock for common.mustCall
const mustCall = fn => {
const wrappedFn = jest.fn(fn);
return wrappedFn;
};
describe("HTTP/2 write callbacks", () => {
let server;
let client;
let port;
beforeAll(done => {
server = http2.createServer();
server.listen(0, () => {
port = server.address().port;
done();
});
});
afterAll(() => {
server.close();
});
test("write callbacks are called", done => {
const serverWriteCallback = mustCall(() => {});
const clientWriteCallback = mustCall(() => {});
server.once("stream", stream => {
stream.write("abc", serverWriteCallback);
stream.end("xyz");
let actual = "";
stream.setEncoding("utf8");
stream.on("data", chunk => (actual += chunk));
stream.on("end", () => {
expect(actual).toBe("abcxyz");
});
});
client = http2.connect(`http://localhost:${port}`);
const req = client.request({ ":method": "POST" });
req.write("abc", clientWriteCallback);
req.end("xyz");
let actual = "";
req.setEncoding("utf8");
req.on("data", chunk => (actual += chunk));
req.on("end", () => {
expect(actual).toBe("abcxyz");
});
req.on("close", () => {
client.close();
// Check if callbacks were called
expect(serverWriteCallback).toHaveBeenCalled();
expect(clientWriteCallback).toHaveBeenCalled();
done();
});
});
});
//<#END_FILE: test-http2-write-callbacks.js

View File

@@ -0,0 +1,69 @@
//#FILE: test-http2-write-empty-string.js
//#SHA1: 59ba4a8a3c63aad827770d96f668922107ed2f2f
//-----------------
'use strict';
const http2 = require('http2');
// Skip the test if crypto is not available
let http2Server;
beforeAll(() => {
if (!process.versions.openssl) {
test.skip('missing crypto');
}
});
afterAll(() => {
if (http2Server) {
http2Server.close();
}
});
test('HTTP/2 server writes empty strings correctly', async () => {
http2Server = http2.createServer((request, response) => {
response.writeHead(200, { 'Content-Type': 'text/plain' });
response.write('1\n');
response.write('');
response.write('2\n');
response.write('');
response.end('3\n');
});
await new Promise(resolve => {
http2Server.listen(0, resolve);
});
const port = http2Server.address().port;
const client = http2.connect(`http://localhost:${port}`);
const headers = { ':path': '/' };
const responsePromise = new Promise((resolve, reject) => {
const req = client.request(headers);
let res = '';
req.setEncoding('ascii');
req.on('response', (headers) => {
expect(headers[':status']).toBe(200);
});
req.on('data', (chunk) => {
res += chunk;
});
req.on('end', () => {
resolve(res);
});
req.on('error', reject);
req.end();
});
const response = await responsePromise;
expect(response).toBe('1\n2\n3\n');
await new Promise(resolve => client.close(resolve));
});
//<#END_FILE: test-http2-write-empty-string.js

View File

@@ -0,0 +1,56 @@
//#FILE: test-http2-zero-length-header.js
//#SHA1: 65bd4ca954be7761c2876b26c6ac5d3f0e5c98e4
//-----------------
"use strict";
const http2 = require("http2");
// Skip test if crypto is not available
const hasCrypto = (() => {
try {
require("crypto");
return true;
} catch (err) {
return false;
}
})();
(hasCrypto ? describe : describe.skip)("http2 zero length header", () => {
let server;
let port;
beforeAll(async () => {
server = http2.createServer();
await new Promise(resolve => server.listen(0, resolve));
port = server.address().port;
});
afterAll(() => {
server.close();
});
test("server receives correct headers", async () => {
const serverPromise = new Promise(resolve => {
server.once("stream", (stream, headers) => {
expect(headers).toEqual({
":scheme": "http",
":authority": `localhost:${port}`,
":method": "GET",
":path": "/",
"bar": "",
"__proto__": null,
[http2.sensitiveHeaders]: [],
});
stream.session.destroy();
resolve();
});
});
const client = http2.connect(`http://localhost:${port}/`);
client.request({ ":path": "/", "": "foo", "bar": "" }).end();
await serverPromise;
client.close();
});
});
//<#END_FILE: test-http2-zero-length-header.js

View File

@@ -17,44 +17,52 @@ function getSrc() {
});
}
const expect = "asdffoobar";
const expectedOutput = "asdffoobar";
test("HTTP/2 zero length write", async () => {
if (!("crypto" in process)) {
return;
let server;
let client;
beforeAll(() => {
if (!process.versions.openssl) {
test.skip("missing crypto");
}
const server = http2.createServer();
server.on("stream", stream => {
let actual = "";
stream.respond();
stream.resume();
stream.setEncoding("utf8");
stream.on("data", chunk => (actual += chunk));
stream.on("end", () => {
getSrc().pipe(stream);
expect(actual).toBe(expect);
});
});
await new Promise(resolve => server.listen(0, resolve));
const client = http2.connect(`http://localhost:${server.address().port}`);
let actual = "";
const req = client.request({ ":method": "POST" });
req.on("response", jest.fn());
req.setEncoding("utf8");
req.on("data", chunk => (actual += chunk));
await new Promise(resolve => {
req.on("end", () => {
expect(actual).toBe(expect);
server.close();
client.close();
resolve();
});
getSrc().pipe(req);
});
});
afterEach(() => {
if (client) client.close();
if (server) server.close();
});
test("HTTP/2 zero length write", async () => {
return new Promise((resolve, reject) => {
server = http2.createServer();
server.on("stream", stream => {
let actual = "";
stream.respond();
stream.resume();
stream.setEncoding("utf8");
stream.on("data", chunk => (actual += chunk));
stream.on("end", () => {
getSrc().pipe(stream);
expect(actual).toBe(expectedOutput);
});
});
server.listen(0, () => {
client = http2.connect(`http://localhost:${server.address().port}`);
let actual = "";
const req = client.request({ ":method": "POST" });
req.on("response", () => {});
req.setEncoding("utf8");
req.on("data", chunk => (actual += chunk));
req.on("end", () => {
expect(actual).toBe(expectedOutput);
resolve();
});
getSrc().pipe(req);
});
});
}, 10000); // Increase timeout to 10 seconds
//<#END_FILE: test-http2-zero-length-write.js

View File

@@ -1,57 +1,33 @@
import * as grpc from "@grpc/grpc-js";
/*
* Copyright 2019 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import * as loader from "@grpc/proto-loader";
import { which } from "bun";
import * as assert2 from "./assert2";
import * as path from "path";
import grpc from "@grpc/grpc-js";
import * as fsPromises from "fs/promises";
import * as os from "os";
import { GrpcObject, ServiceClientConstructor, ServiceClient, loadPackageDefinition } from "@grpc/grpc-js";
import { readFileSync } from "fs";
import path from "node:path";
import { AddressInfo } from "ws";
const nodeExecutable = which("node");
async function nodeEchoServer(env: any) {
env = env || {};
if (!nodeExecutable) throw new Error("node executable not found");
const subprocess = Bun.spawn([nodeExecutable, path.join(import.meta.dir, "node-server.fixture.js")], {
stdout: "pipe",
stdin: "pipe",
env: env,
});
const reader = subprocess.stdout.getReader();
const data = await reader.read();
const decoder = new TextDecoder("utf-8");
const json = decoder.decode(data.value);
const address = JSON.parse(json);
const url = `${address.family === "IPv6" ? `[${address.address}]` : address.address}:${address.port}`;
return { address, url, subprocess };
}
export class TestServer {
#server: any;
#options: grpc.ChannelOptions;
address: AddressInfo | null = null;
url: string = "";
service_type: number = 0;
useTls = false;
constructor(useTls: boolean, options?: grpc.ChannelOptions, service_type = 0) {
this.#options = options || {};
this.useTls = useTls;
this.service_type = service_type;
}
async start() {
const result = await nodeEchoServer({
GRPC_TEST_USE_TLS: this.useTls ? "true" : "false",
GRPC_TEST_OPTIONS: JSON.stringify(this.#options),
GRPC_SERVICE_TYPE: this.service_type.toString(),
"grpc-node.max_session_memory": 1024,
});
this.address = result.address as AddressInfo;
this.url = result.url as string;
this.#server = result.subprocess;
}
shutdown() {
this.#server.stdin.write("shutdown");
this.#server.kill();
}
}
import { HealthListener, SubchannelInterface } from "@grpc/grpc-js/build/src/subchannel-interface";
import type { EntityTypes, SubchannelRef } from "@grpc/grpc-js/build/src/channelz";
import { Subchannel } from "@grpc/grpc-js/build/src/subchannel";
import { ConnectivityState } from "@grpc/grpc-js/build/src/connectivity-state";
const protoLoaderOptions = {
keepCase: true,
@@ -61,93 +37,145 @@ const protoLoaderOptions = {
oneofs: true,
};
function loadProtoFile(file: string) {
const packageDefinition = loader.loadSync(file, protoLoaderOptions);
return grpc.loadPackageDefinition(packageDefinition);
export function mockFunction(): never {
throw new Error("Not implemented");
}
const protoFile = path.join(import.meta.dir, "fixtures", "echo_service.proto");
const EchoService = loadProtoFile(protoFile).EchoService as grpc.ServiceClientConstructor;
export function loadProtoFile(file: string): GrpcObject {
const packageDefinition = loader.loadSync(file, protoLoaderOptions);
return loadPackageDefinition(packageDefinition);
}
export const ca = readFileSync(path.join(import.meta.dir, "fixtures", "ca.pem"));
const protoFile = path.join(__dirname, "fixtures", "echo_service.proto");
const echoService = loadProtoFile(protoFile).EchoService as ServiceClientConstructor;
const ca = readFileSync(path.join(__dirname, "fixtures", "ca.pem"));
const key = readFileSync(path.join(__dirname, "fixtures", "server1.key"));
const cert = readFileSync(path.join(__dirname, "fixtures", "server1.pem"));
const serviceImpl = {
echo: (call: grpc.ServerUnaryCall<any, any>, callback: grpc.sendUnaryData<any>) => {
callback(null, call.request);
},
};
export class TestServer {
private server: grpc.Server;
private target: string | null = null;
constructor(
public useTls: boolean,
options?: grpc.ServerOptions,
) {
this.server = new grpc.Server(options);
this.server.addService(echoService.service, serviceImpl);
}
private getCredentials(): grpc.ServerCredentials {
if (this.useTls) {
return grpc.ServerCredentials.createSsl(null, [{ private_key: key, cert_chain: cert }], false);
} else {
return grpc.ServerCredentials.createInsecure();
}
}
start(): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.server.bindAsync("localhost:0", this.getCredentials(), (error, port) => {
if (error) {
reject(error);
return;
}
this.target = `localhost:${port}`;
resolve();
});
});
}
startUds(): Promise<void> {
return fsPromises.mkdtemp(path.join(os.tmpdir(), "uds")).then(dir => {
return new Promise<void>((resolve, reject) => {
const target = `unix://${dir}/socket`;
this.server.bindAsync(target, this.getCredentials(), (error, port) => {
if (error) {
reject(error);
return;
}
this.target = target;
resolve();
});
});
});
}
shutdown() {
this.server.forceShutdown();
}
getTarget() {
if (this.target === null) {
throw new Error("Server not yet started");
}
return this.target;
}
}
export class TestClient {
#client: grpc.Client;
constructor(url: string, useTls: boolean | grpc.ChannelCredentials, options?: grpc.ChannelOptions) {
private client: ServiceClient;
constructor(target: string, useTls: boolean, options?: grpc.ChannelOptions) {
let credentials: grpc.ChannelCredentials;
if (useTls instanceof grpc.ChannelCredentials) {
credentials = useTls;
} else if (useTls) {
if (useTls) {
credentials = grpc.credentials.createSsl(ca);
} else {
credentials = grpc.credentials.createInsecure();
}
this.#client = new EchoService(url, credentials, options);
}
static createFromServerWithCredentials(
server: TestServer,
credentials: grpc.ChannelCredentials,
options?: grpc.ChannelOptions,
) {
if (!server.address) {
throw new Error("Cannot create client, server not started");
}
return new TestClient(server.url, credentials, options);
this.client = new echoService(target, credentials, options);
}
static createFromServer(server: TestServer, options?: grpc.ChannelOptions) {
if (!server.address) {
throw new Error("Cannot create client, server not started");
}
return new TestClient(server.url, server.useTls, options);
return new TestClient(server.getTarget(), server.useTls, options);
}
waitForReady(deadline: grpc.Deadline, callback: (error?: Error) => void) {
this.#client.waitForReady(deadline, callback);
}
get client() {
return this.#client;
}
echo(...params: any[]) {
return this.#client.echo(...params);
}
sendRequest(callback: (error?: grpc.ServiceError) => void) {
this.#client.echo(
{
value: "hello",
value2: 1,
},
callback,
);
this.client.waitForReady(deadline, callback);
}
getChannel() {
return this.#client.getChannel();
sendRequest(callback: (error?: grpc.ServiceError) => void) {
this.client.echo({}, callback);
}
sendRequestWithMetadata(metadata: grpc.Metadata, callback: (error?: grpc.ServiceError) => void) {
this.client.echo({}, metadata, callback);
}
getChannelState() {
return this.#client.getChannel().getConnectivityState(false);
return this.client.getChannel().getConnectivityState(false);
}
waitForClientState(deadline: grpc.Deadline, state: ConnectivityState, callback: (error?: Error) => void) {
this.client.getChannel().watchConnectivityState(this.getChannelState(), deadline, err => {
if (err) {
return callback(err);
}
const currentState = this.getChannelState();
if (currentState === state) {
callback();
} else {
return this.waitForClientState(deadline, currentState, callback);
}
});
}
close() {
this.#client.close();
this.client.close();
}
}
export enum ConnectivityState {
IDLE,
CONNECTING,
READY,
TRANSIENT_FAILURE,
SHUTDOWN,
}
/**
* A mock subchannel that transitions between states on command, to test LB
* policy behavior
*/
export class MockSubchannel implements grpc.experimental.SubchannelInterface {
export class MockSubchannel implements SubchannelInterface {
private state: grpc.connectivityState;
private listeners: Set<grpc.experimental.ConnectivityStateListener> = new Set();
constructor(
@@ -196,4 +224,11 @@ export class MockSubchannel implements grpc.experimental.SubchannelInterface {
realSubchannelEquals(other: grpc.experimental.SubchannelInterface): boolean {
return this === other;
}
isHealthy(): boolean {
return true;
}
addHealthStateWatcher(listener: HealthListener): void {}
removeHealthStateWatcher(listener: HealthListener): void {}
}
export { assert2 };

View File

@@ -0,0 +1 @@
CONFIRMEDTESTKEY

View File

@@ -1,20 +1,15 @@
-----BEGIN CERTIFICATE-----
MIIDWjCCAkKgAwIBAgIUWrP0VvHcy+LP6UuYNtiL9gBhD5owDQYJKoZIhvcNAQEL
BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTIw
MDMxNzE4NTk1MVoXDTMwMDMxNTE4NTk1MVowVjELMAkGA1UEBhMCQVUxEzARBgNV
BAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0
ZDEPMA0GA1UEAwwGdGVzdGNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
AQEAsGL0oXflF0LzoM+Bh+qUU9yhqzw2w8OOX5mu/iNCyUOBrqaHi7mGHx73GD01
diNzCzvlcQqdNIH6NQSL7DTpBjca66jYT9u73vZe2MDrr1nVbuLvfu9850cdxiUO
Inv5xf8+sTHG0C+a+VAvMhsLiRjsq+lXKRJyk5zkbbsETybqpxoJ+K7CoSy3yc/k
QIY3TipwEtwkKP4hzyo6KiGd/DPexie4nBUInN3bS1BUeNZ5zeaIC2eg3bkeeW7c
qT55b+Yen6CxY0TEkzBK6AKt/WUialKMgT0wbTxRZO7kUCH3Sq6e/wXeFdJ+HvdV
LPlAg5TnMaNpRdQih/8nRFpsdwIDAQABoyAwHjAMBgNVHRMEBTADAQH/MA4GA1Ud
DwEB/wQEAwICBDANBgkqhkiG9w0BAQsFAAOCAQEAkTrKZjBrJXHps/HrjNCFPb5a
THuGPCSsepe1wkKdSp1h4HGRpLoCgcLysCJ5hZhRpHkRihhef+rFHEe60UePQO3S
CVTtdJB4CYWpcNyXOdqefrbJW5QNljxgi6Fhvs7JJkBqdXIkWXtFk2eRgOIP2Eo9
/OHQHlYnwZFrk6sp4wPyR+A95S0toZBcyDVz7u+hOW0pGK3wviOe9lvRgj/H3Pwt
bewb0l+MhRig0/DVHamyVxrDRbqInU1/GTNCwcZkXKYFWSf92U+kIcTth24Q1gcw
eZiLl5FfrWokUNytFElXob0V0a5/kbhiLc3yWmvWqHTpqCALbVyF+rKJo2f5Kw==
-----END CERTIFICATE-----
MIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBnRlc3RjYTAeFw0xNDExMTEyMjMxMjla
Fw0yNDExMDgyMjMxMjlaMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0
YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMT
BnRlc3RjYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwEDfBV5MYdlHVHJ7
+L4nxrZy7mBfAVXpOc5vMYztssUI7mL2/iYujiIXM+weZYNTEpLdjyJdu7R5gGUu
g1jSVK/EPHfc74O7AyZU34PNIP4Sh33N+/A5YexrNgJlPY+E3GdVYi4ldWJjgkAd
Qah2PH5ACLrIIC6tRka9hcaBlIECAwEAAaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNV
HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADgYEAHzC7jdYlzAVmddi/gdAeKPau
sPBG/C2HCWqHzpCUHcKuvMzDVkY/MP2o6JIW2DBbY64bO/FceExhjcykgaYtCH/m
oIU63+CFOTtR7otyQAWHqXa7q4SbCDlG7DyRFxqG0txPtGvy12lgldA2+RgcigQG
Dfcog5wrJytaQ6UA0wE=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,564 @@
// Copyright 2018 The gRPC Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This file defines an interface for exporting monitoring information
// out of gRPC servers. See the full design at
// https://github.com/grpc/proposal/blob/master/A14-channelz.md
//
// The canonical version of this proto can be found at
// https://github.com/grpc/grpc-proto/blob/master/grpc/channelz/v1/channelz.proto
syntax = "proto3";
package grpc.channelz.v1;
import "google/protobuf/any.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
option go_package = "google.golang.org/grpc/channelz/grpc_channelz_v1";
option java_multiple_files = true;
option java_package = "io.grpc.channelz.v1";
option java_outer_classname = "ChannelzProto";
// Channel is a logical grouping of channels, subchannels, and sockets.
message Channel {
// The identifier for this channel. This should bet set.
ChannelRef ref = 1;
// Data specific to this channel.
ChannelData data = 2;
// At most one of 'channel_ref+subchannel_ref' and 'socket' is set.
// There are no ordering guarantees on the order of channel refs.
// There may not be cycles in the ref graph.
// A channel ref may be present in more than one channel or subchannel.
repeated ChannelRef channel_ref = 3;
// At most one of 'channel_ref+subchannel_ref' and 'socket' is set.
// There are no ordering guarantees on the order of subchannel refs.
// There may not be cycles in the ref graph.
// A sub channel ref may be present in more than one channel or subchannel.
repeated SubchannelRef subchannel_ref = 4;
// There are no ordering guarantees on the order of sockets.
repeated SocketRef socket_ref = 5;
}
// Subchannel is a logical grouping of channels, subchannels, and sockets.
// A subchannel is load balanced over by it's ancestor
message Subchannel {
// The identifier for this channel.
SubchannelRef ref = 1;
// Data specific to this channel.
ChannelData data = 2;
// At most one of 'channel_ref+subchannel_ref' and 'socket' is set.
// There are no ordering guarantees on the order of channel refs.
// There may not be cycles in the ref graph.
// A channel ref may be present in more than one channel or subchannel.
repeated ChannelRef channel_ref = 3;
// At most one of 'channel_ref+subchannel_ref' and 'socket' is set.
// There are no ordering guarantees on the order of subchannel refs.
// There may not be cycles in the ref graph.
// A sub channel ref may be present in more than one channel or subchannel.
repeated SubchannelRef subchannel_ref = 4;
// There are no ordering guarantees on the order of sockets.
repeated SocketRef socket_ref = 5;
}
// These come from the specified states in this document:
// https://github.com/grpc/grpc/blob/master/doc/connectivity-semantics-and-api.md
message ChannelConnectivityState {
enum State {
UNKNOWN = 0;
IDLE = 1;
CONNECTING = 2;
READY = 3;
TRANSIENT_FAILURE = 4;
SHUTDOWN = 5;
}
State state = 1;
}
// Channel data is data related to a specific Channel or Subchannel.
message ChannelData {
// The connectivity state of the channel or subchannel. Implementations
// should always set this.
ChannelConnectivityState state = 1;
// The target this channel originally tried to connect to. May be absent
string target = 2;
// A trace of recent events on the channel. May be absent.
ChannelTrace trace = 3;
// The number of calls started on the channel
int64 calls_started = 4;
// The number of calls that have completed with an OK status
int64 calls_succeeded = 5;
// The number of calls that have completed with a non-OK status
int64 calls_failed = 6;
// The last time a call was started on the channel.
google.protobuf.Timestamp last_call_started_timestamp = 7;
}
// A trace event is an interesting thing that happened to a channel or
// subchannel, such as creation, address resolution, subchannel creation, etc.
message ChannelTraceEvent {
// High level description of the event.
string description = 1;
// The supported severity levels of trace events.
enum Severity {
CT_UNKNOWN = 0;
CT_INFO = 1;
CT_WARNING = 2;
CT_ERROR = 3;
}
// the severity of the trace event
Severity severity = 2;
// When this event occurred.
google.protobuf.Timestamp timestamp = 3;
// ref of referenced channel or subchannel.
// Optional, only present if this event refers to a child object. For example,
// this field would be filled if this trace event was for a subchannel being
// created.
oneof child_ref {
ChannelRef channel_ref = 4;
SubchannelRef subchannel_ref = 5;
}
}
// ChannelTrace represents the recent events that have occurred on the channel.
message ChannelTrace {
// Number of events ever logged in this tracing object. This can differ from
// events.size() because events can be overwritten or garbage collected by
// implementations.
int64 num_events_logged = 1;
// Time that this channel was created.
google.protobuf.Timestamp creation_timestamp = 2;
// List of events that have occurred on this channel.
repeated ChannelTraceEvent events = 3;
}
// ChannelRef is a reference to a Channel.
message ChannelRef {
// The globally unique id for this channel. Must be a positive number.
int64 channel_id = 1;
// An optional name associated with the channel.
string name = 2;
// Intentionally don't use field numbers from other refs.
reserved 3, 4, 5, 6, 7, 8;
}
// SubchannelRef is a reference to a Subchannel.
message SubchannelRef {
// The globally unique id for this subchannel. Must be a positive number.
int64 subchannel_id = 7;
// An optional name associated with the subchannel.
string name = 8;
// Intentionally don't use field numbers from other refs.
reserved 1, 2, 3, 4, 5, 6;
}
// SocketRef is a reference to a Socket.
message SocketRef {
// The globally unique id for this socket. Must be a positive number.
int64 socket_id = 3;
// An optional name associated with the socket.
string name = 4;
// Intentionally don't use field numbers from other refs.
reserved 1, 2, 5, 6, 7, 8;
}
// ServerRef is a reference to a Server.
message ServerRef {
// A globally unique identifier for this server. Must be a positive number.
int64 server_id = 5;
// An optional name associated with the server.
string name = 6;
// Intentionally don't use field numbers from other refs.
reserved 1, 2, 3, 4, 7, 8;
}
// Server represents a single server. There may be multiple servers in a single
// program.
message Server {
// The identifier for a Server. This should be set.
ServerRef ref = 1;
// The associated data of the Server.
ServerData data = 2;
// The sockets that the server is listening on. There are no ordering
// guarantees. This may be absent.
repeated SocketRef listen_socket = 3;
}
// ServerData is data for a specific Server.
message ServerData {
// A trace of recent events on the server. May be absent.
ChannelTrace trace = 1;
// The number of incoming calls started on the server
int64 calls_started = 2;
// The number of incoming calls that have completed with an OK status
int64 calls_succeeded = 3;
// The number of incoming calls that have a completed with a non-OK status
int64 calls_failed = 4;
// The last time a call was started on the server.
google.protobuf.Timestamp last_call_started_timestamp = 5;
}
// Information about an actual connection. Pronounced "sock-ay".
message Socket {
// The identifier for the Socket.
SocketRef ref = 1;
// Data specific to this Socket.
SocketData data = 2;
// The locally bound address.
Address local = 3;
// The remote bound address. May be absent.
Address remote = 4;
// Security details for this socket. May be absent if not available, or
// there is no security on the socket.
Security security = 5;
// Optional, represents the name of the remote endpoint, if different than
// the original target name.
string remote_name = 6;
}
// SocketData is data associated for a specific Socket. The fields present
// are specific to the implementation, so there may be minor differences in
// the semantics. (e.g. flow control windows)
message SocketData {
// The number of streams that have been started.
int64 streams_started = 1;
// The number of streams that have ended successfully:
// On client side, received frame with eos bit set;
// On server side, sent frame with eos bit set.
int64 streams_succeeded = 2;
// The number of streams that have ended unsuccessfully:
// On client side, ended without receiving frame with eos bit set;
// On server side, ended without sending frame with eos bit set.
int64 streams_failed = 3;
// The number of grpc messages successfully sent on this socket.
int64 messages_sent = 4;
// The number of grpc messages received on this socket.
int64 messages_received = 5;
// The number of keep alives sent. This is typically implemented with HTTP/2
// ping messages.
int64 keep_alives_sent = 6;
// The last time a stream was created by this endpoint. Usually unset for
// servers.
google.protobuf.Timestamp last_local_stream_created_timestamp = 7;
// The last time a stream was created by the remote endpoint. Usually unset
// for clients.
google.protobuf.Timestamp last_remote_stream_created_timestamp = 8;
// The last time a message was sent by this endpoint.
google.protobuf.Timestamp last_message_sent_timestamp = 9;
// The last time a message was received by this endpoint.
google.protobuf.Timestamp last_message_received_timestamp = 10;
// The amount of window, granted to the local endpoint by the remote endpoint.
// This may be slightly out of date due to network latency. This does NOT
// include stream level or TCP level flow control info.
google.protobuf.Int64Value local_flow_control_window = 11;
// The amount of window, granted to the remote endpoint by the local endpoint.
// This may be slightly out of date due to network latency. This does NOT
// include stream level or TCP level flow control info.
google.protobuf.Int64Value remote_flow_control_window = 12;
// Socket options set on this socket. May be absent if 'summary' is set
// on GetSocketRequest.
repeated SocketOption option = 13;
}
// Address represents the address used to create the socket.
message Address {
message TcpIpAddress {
// Either the IPv4 or IPv6 address in bytes. Will be either 4 bytes or 16
// bytes in length.
bytes ip_address = 1;
// 0-64k, or -1 if not appropriate.
int32 port = 2;
}
// A Unix Domain Socket address.
message UdsAddress {
string filename = 1;
}
// An address type not included above.
message OtherAddress {
// The human readable version of the value. This value should be set.
string name = 1;
// The actual address message.
google.protobuf.Any value = 2;
}
oneof address {
TcpIpAddress tcpip_address = 1;
UdsAddress uds_address = 2;
OtherAddress other_address = 3;
}
}
// Security represents details about how secure the socket is.
message Security {
message Tls {
oneof cipher_suite {
// The cipher suite name in the RFC 4346 format:
// https://tools.ietf.org/html/rfc4346#appendix-C
string standard_name = 1;
// Some other way to describe the cipher suite if
// the RFC 4346 name is not available.
string other_name = 2;
}
// the certificate used by this endpoint.
bytes local_certificate = 3;
// the certificate used by the remote endpoint.
bytes remote_certificate = 4;
}
message OtherSecurity {
// The human readable version of the value.
string name = 1;
// The actual security details message.
google.protobuf.Any value = 2;
}
oneof model {
Tls tls = 1;
OtherSecurity other = 2;
}
}
// SocketOption represents socket options for a socket. Specifically, these
// are the options returned by getsockopt().
message SocketOption {
// The full name of the socket option. Typically this will be the upper case
// name, such as "SO_REUSEPORT".
string name = 1;
// The human readable value of this socket option. At least one of value or
// additional will be set.
string value = 2;
// Additional data associated with the socket option. At least one of value
// or additional will be set.
google.protobuf.Any additional = 3;
}
// For use with SocketOption's additional field. This is primarily used for
// SO_RCVTIMEO and SO_SNDTIMEO
message SocketOptionTimeout {
google.protobuf.Duration duration = 1;
}
// For use with SocketOption's additional field. This is primarily used for
// SO_LINGER.
message SocketOptionLinger {
// active maps to `struct linger.l_onoff`
bool active = 1;
// duration maps to `struct linger.l_linger`
google.protobuf.Duration duration = 2;
}
// For use with SocketOption's additional field. Tcp info for
// SOL_TCP and TCP_INFO.
message SocketOptionTcpInfo {
uint32 tcpi_state = 1;
uint32 tcpi_ca_state = 2;
uint32 tcpi_retransmits = 3;
uint32 tcpi_probes = 4;
uint32 tcpi_backoff = 5;
uint32 tcpi_options = 6;
uint32 tcpi_snd_wscale = 7;
uint32 tcpi_rcv_wscale = 8;
uint32 tcpi_rto = 9;
uint32 tcpi_ato = 10;
uint32 tcpi_snd_mss = 11;
uint32 tcpi_rcv_mss = 12;
uint32 tcpi_unacked = 13;
uint32 tcpi_sacked = 14;
uint32 tcpi_lost = 15;
uint32 tcpi_retrans = 16;
uint32 tcpi_fackets = 17;
uint32 tcpi_last_data_sent = 18;
uint32 tcpi_last_ack_sent = 19;
uint32 tcpi_last_data_recv = 20;
uint32 tcpi_last_ack_recv = 21;
uint32 tcpi_pmtu = 22;
uint32 tcpi_rcv_ssthresh = 23;
uint32 tcpi_rtt = 24;
uint32 tcpi_rttvar = 25;
uint32 tcpi_snd_ssthresh = 26;
uint32 tcpi_snd_cwnd = 27;
uint32 tcpi_advmss = 28;
uint32 tcpi_reordering = 29;
}
// Channelz is a service exposed by gRPC servers that provides detailed debug
// information.
service Channelz {
// Gets all root channels (i.e. channels the application has directly
// created). This does not include subchannels nor non-top level channels.
rpc GetTopChannels(GetTopChannelsRequest) returns (GetTopChannelsResponse);
// Gets all servers that exist in the process.
rpc GetServers(GetServersRequest) returns (GetServersResponse);
// Returns a single Server, or else a NOT_FOUND code.
rpc GetServer(GetServerRequest) returns (GetServerResponse);
// Gets all server sockets that exist in the process.
rpc GetServerSockets(GetServerSocketsRequest) returns (GetServerSocketsResponse);
// Returns a single Channel, or else a NOT_FOUND code.
rpc GetChannel(GetChannelRequest) returns (GetChannelResponse);
// Returns a single Subchannel, or else a NOT_FOUND code.
rpc GetSubchannel(GetSubchannelRequest) returns (GetSubchannelResponse);
// Returns a single Socket or else a NOT_FOUND code.
rpc GetSocket(GetSocketRequest) returns (GetSocketResponse);
}
message GetTopChannelsRequest {
// start_channel_id indicates that only channels at or above this id should be
// included in the results.
// To request the first page, this should be set to 0. To request
// subsequent pages, the client generates this value by adding 1 to
// the highest seen result ID.
int64 start_channel_id = 1;
// If non-zero, the server will return a page of results containing
// at most this many items. If zero, the server will choose a
// reasonable page size. Must never be negative.
int64 max_results = 2;
}
message GetTopChannelsResponse {
// list of channels that the connection detail service knows about. Sorted in
// ascending channel_id order.
// Must contain at least 1 result, otherwise 'end' must be true.
repeated Channel channel = 1;
// If set, indicates that the list of channels is the final list. Requesting
// more channels can only return more if they are created after this RPC
// completes.
bool end = 2;
}
message GetServersRequest {
// start_server_id indicates that only servers at or above this id should be
// included in the results.
// To request the first page, this must be set to 0. To request
// subsequent pages, the client generates this value by adding 1 to
// the highest seen result ID.
int64 start_server_id = 1;
// If non-zero, the server will return a page of results containing
// at most this many items. If zero, the server will choose a
// reasonable page size. Must never be negative.
int64 max_results = 2;
}
message GetServersResponse {
// list of servers that the connection detail service knows about. Sorted in
// ascending server_id order.
// Must contain at least 1 result, otherwise 'end' must be true.
repeated Server server = 1;
// If set, indicates that the list of servers is the final list. Requesting
// more servers will only return more if they are created after this RPC
// completes.
bool end = 2;
}
message GetServerRequest {
// server_id is the identifier of the specific server to get.
int64 server_id = 1;
}
message GetServerResponse {
// The Server that corresponds to the requested server_id. This field
// should be set.
Server server = 1;
}
message GetServerSocketsRequest {
int64 server_id = 1;
// start_socket_id indicates that only sockets at or above this id should be
// included in the results.
// To request the first page, this must be set to 0. To request
// subsequent pages, the client generates this value by adding 1 to
// the highest seen result ID.
int64 start_socket_id = 2;
// If non-zero, the server will return a page of results containing
// at most this many items. If zero, the server will choose a
// reasonable page size. Must never be negative.
int64 max_results = 3;
}
message GetServerSocketsResponse {
// list of socket refs that the connection detail service knows about. Sorted in
// ascending socket_id order.
// Must contain at least 1 result, otherwise 'end' must be true.
repeated SocketRef socket_ref = 1;
// If set, indicates that the list of sockets is the final list. Requesting
// more sockets will only return more if they are created after this RPC
// completes.
bool end = 2;
}
message GetChannelRequest {
// channel_id is the identifier of the specific channel to get.
int64 channel_id = 1;
}
message GetChannelResponse {
// The Channel that corresponds to the requested channel_id. This field
// should be set.
Channel channel = 1;
}
message GetSubchannelRequest {
// subchannel_id is the identifier of the specific subchannel to get.
int64 subchannel_id = 1;
}
message GetSubchannelResponse {
// The Subchannel that corresponds to the requested subchannel_id. This
// field should be set.
Subchannel subchannel = 1;
}
message GetSocketRequest {
// socket_id is the identifier of the specific socket to get.
int64 socket_id = 1;
// If true, the response will contain only high level information
// that is inexpensive to obtain. Fields thay may be omitted are
// documented.
bool summary = 2;
}
message GetSocketResponse {
// The Socket that corresponds to the requested socket_id. This field
// should be set.
Socket socket = 1;
}

View File

@@ -1,28 +1,16 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDnE443EknxvxBq
6+hvn/t09hl8hx366EBYvZmVM/NC+7igXRAjiJiA/mIaCvL3MS0Iz5hBLxSGICU+
WproA3GCIFITIwcf/ETyWj/5xpgZ4AKrLrjQmmX8mhwUajfF3UvwMJrCOVqPp67t
PtP+2kBXaqrXdvnvXR41FsIB8V7zIAuIZB6bHQhiGVlc1sgZYsE2EGG9WMmHtS86
qkAOTjG2XyjmPTGAwhGDpYkYrpzp99IiDh4/Veai81hn0ssQkbry0XRD/Ig3jcHh
23WiriPNJ0JsbgXUSLKRPZObA9VgOLy2aXoN84IMaeK3yy+cwSYG/99w93fUZJte
MXwz4oYZAgMBAAECggEBAIVn2Ncai+4xbH0OLWckabwgyJ4IM9rDc0LIU368O1kU
koais8qP9dujAWgfoh3sGh/YGgKn96VnsZjKHlyMgF+r4TaDJn3k2rlAOWcurGlj
1qaVlsV4HiEzp7pxiDmHhWvp4672Bb6iBG+bsjCUOEk/n9o9KhZzIBluRhtxCmw5
nw4Do7z00PTvN81260uPWSc04IrytvZUiAIx/5qxD72bij2xJ8t/I9GI8g4FtoVB
8pB6S/hJX1PZhh9VlU6Yk+TOfOVnbebG4W5138LkB835eqk3Zz0qsbc2euoi8Hxi
y1VGwQEmMQ63jXz4c6g+X55ifvUK9Jpn5E8pq+pMd7ECgYEA93lYq+Cr54K4ey5t
sWMa+ye5RqxjzgXj2Kqr55jb54VWG7wp2iGbg8FMlkQwzTJwebzDyCSatguEZLuB
gRGroRnsUOy9vBvhKPOch9bfKIl6qOgzMJB267fBVWx5ybnRbWN/I7RvMQf3k+9y
biCIVnxDLEEYyx7z85/5qxsXg/MCgYEA7wmWKtCTn032Hy9P8OL49T0X6Z8FlkDC
Rk42ygrc/MUbugq9RGUxcCxoImOG9JXUpEtUe31YDm2j+/nbvrjl6/bP2qWs0V7l
dTJl6dABP51pCw8+l4cWgBBX08Lkeen812AAFNrjmDCjX6rHjWHLJcpS18fnRRkP
V1d/AHWX7MMCgYEA6Gsw2guhp0Zf2GCcaNK5DlQab8OL4Hwrpttzo4kuTlwtqNKp
Q9H4al9qfF4Cr1TFya98+EVYf8yFRM3NLNjZpe3gwYf2EerlJj7VLcahw0KKzoN1
QBENfwgPLRk5sDkx9VhSmcfl/diLroZdpAwtv3vo4nEoxeuGFbKTGx3Qkf0CgYEA
xyR+dcb05Ygm3w4klHQTowQ10s1H80iaUcZBgQuR1ghEtDbUPZHsoR5t1xCB02ys
DgAwLv1bChIvxvH/L6KM8ovZ2LekBX4AviWxoBxJnfz/EVau98B0b1auRN6eSC83
FRuGldlSOW1z/nSh8ViizSYE5H5HX1qkXEippvFRE88CgYB3Bfu3YQY60ITWIShv
nNkdcbTT9eoP9suaRJjw92Ln+7ZpALYlQMKUZmJ/5uBmLs4RFwUTQruLOPL4yLTH
awADWUzs3IRr1fwn9E+zM8JVyKCnUEM3w4N5UZskGO2klashAd30hWO+knRv/y0r
uGIYs9Ek7YXlXIRVrzMwcsrt1w==
-----END PRIVATE KEY-----
MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAOHDFScoLCVJpYDD
M4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1BgzkWF+slf
3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd9N8YwbBY
AckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAECgYAn7qGnM2vbjJNBm0VZCkOkTIWm
V10okw7EPJrdL2mkre9NasghNXbE1y5zDshx5Nt3KsazKOxTT8d0Jwh/3KbaN+YY
tTCbKGW0pXDRBhwUHRcuRzScjli8Rih5UOCiZkhefUTcRb6xIhZJuQy71tjaSy0p
dHZRmYyBYO2YEQ8xoQJBAPrJPhMBkzmEYFtyIEqAxQ/o/A6E+E4w8i+KM7nQCK7q
K4JXzyXVAjLfyBZWHGM2uro/fjqPggGD6QH1qXCkI4MCQQDmdKeb2TrKRh5BY1LR
81aJGKcJ2XbcDu6wMZK4oqWbTX2KiYn9GB0woM6nSr/Y6iy1u145YzYxEV/iMwff
DJULAkB8B2MnyzOg0pNFJqBJuH29bKCcHa8gHJzqXhNO5lAlEbMK95p/P2Wi+4Hd
aiEIAF1BF326QJcvYKmwSmrORp85AkAlSNxRJ50OWrfMZnBgzVjDx3xG6KsFQVk2
ol6VhqL6dFgKUORFUWBvnKSyhjJxurlPEahV6oo6+A+mPhFY8eUvAkAZQyTdupP3
XEFQKctGz+9+gKkemDp7LBBMEMBXrGTLPhpEfcjv/7KPdnFHYmhYeBTBnuVmTVWe
F98XJ7tIFfJq
-----END PRIVATE KEY-----

View File

@@ -1,22 +1,16 @@
-----BEGIN CERTIFICATE-----
MIIDtDCCApygAwIBAgIUbJfTREJ6k6/+oInWhV1O1j3ZT0IwDQYJKoZIhvcNAQEL
BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTIw
MDMxODAzMTA0MloXDTMwMDMxNjAzMTA0MlowZTELMAkGA1UEBhMCVVMxETAPBgNV
BAgMCElsbGlub2lzMRAwDgYDVQQHDAdDaGljYWdvMRUwEwYDVQQKDAxFeGFtcGxl
LCBDby4xGjAYBgNVBAMMESoudGVzdC5nb29nbGUuY29tMIIBIjANBgkqhkiG9w0B
AQEFAAOCAQ8AMIIBCgKCAQEA5xOONxJJ8b8Qauvob5/7dPYZfIcd+uhAWL2ZlTPz
Qvu4oF0QI4iYgP5iGgry9zEtCM+YQS8UhiAlPlqa6ANxgiBSEyMHH/xE8lo/+caY
GeACqy640Jpl/JocFGo3xd1L8DCawjlaj6eu7T7T/tpAV2qq13b5710eNRbCAfFe
8yALiGQemx0IYhlZXNbIGWLBNhBhvVjJh7UvOqpADk4xtl8o5j0xgMIRg6WJGK6c
6ffSIg4eP1XmovNYZ9LLEJG68tF0Q/yIN43B4dt1oq4jzSdCbG4F1EiykT2TmwPV
YDi8tml6DfOCDGnit8svnMEmBv/fcPd31GSbXjF8M+KGGQIDAQABo2swaTAJBgNV
HRMEAjAAMAsGA1UdDwQEAwIF4DBPBgNVHREESDBGghAqLnRlc3QuZ29vZ2xlLmZy
ghh3YXRlcnpvb2kudGVzdC5nb29nbGUuYmWCEioudGVzdC55b3V0dWJlLmNvbYcE
wKgBAzANBgkqhkiG9w0BAQsFAAOCAQEAS8hDQA8PSgipgAml7Q3/djwQ644ghWQv
C2Kb+r30RCY1EyKNhnQnIIh/OUbBZvh0M0iYsy6xqXgfDhCB93AA6j0i5cS8fkhH
Jl4RK0tSkGQ3YNY4NzXwQP/vmUgfkw8VBAZ4Y4GKxppdATjffIW+srbAmdDruIRM
wPeikgOoRrXf0LA1fi4TqxARzeRwenQpayNfGHTvVF9aJkl8HoaMunTAdG5pIVcr
9GKi/gEMpXUJbbVv3U5frX1Wo4CFo+rZWJ/LyCMeb0jciNLxSdMwj/E/ZuExlyeZ
gc9ctPjSMvgSyXEKv6Vwobleeg88V2ZgzenziORoWj4KszG/lbQZvg==
-----END CERTIFICATE-----
MIICnDCCAgWgAwIBAgIBBzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJBVTET
MBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ
dHkgTHRkMQ8wDQYDVQQDEwZ0ZXN0Y2EwHhcNMTUxMTA0MDIyMDI0WhcNMjUxMTAx
MDIyMDI0WjBlMQswCQYDVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNV
BAcTB0NoaWNhZ28xFTATBgNVBAoTDEV4YW1wbGUsIENvLjEaMBgGA1UEAxQRKi50
ZXN0Lmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOHDFSco
LCVJpYDDM4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1Bg
zkWF+slf3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd
9N8YwbBYAckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAGjazBpMAkGA1UdEwQCMAAw
CwYDVR0PBAQDAgXgME8GA1UdEQRIMEaCECoudGVzdC5nb29nbGUuZnKCGHdhdGVy
em9vaS50ZXN0Lmdvb2dsZS5iZYISKi50ZXN0LnlvdXR1YmUuY29thwTAqAEDMA0G
CSqGSIb3DQEBCwUAA4GBAJFXVifQNub1LUP4JlnX5lXNlo8FxZ2a12AFQs+bzoJ6
hM044EDjqyxUqSbVePK0ni3w1fHQB5rY9yYC5f8G7aqqTY1QOhoUk8ZTSTRpnkTh
y4jjdvTZeLDVBlueZUTDRmy2feY5aZIU18vFDK08dTG0A87pppuv1LNIR3loveU8
-----END CERTIFICATE-----

View File

@@ -21,6 +21,7 @@ message Request {
bool error = 1;
string message = 2;
int32 errorAfter = 3;
int32 responseLength = 4;
}
message Response {

View File

@@ -0,0 +1,14 @@
// Original file: test/fixtures/test_service.proto
export interface Request {
'error'?: (boolean);
'message'?: (string);
'errorAfter'?: (number);
}
export interface Request__Output {
'error': (boolean);
'message': (string);
'errorAfter': (number);
}

View File

@@ -0,0 +1,12 @@
// Original file: test/fixtures/test_service.proto
export interface Response {
'count'?: (number);
'message'?: (string);
}
export interface Response__Output {
'count': (number);
'message': (string);
}

View File

@@ -0,0 +1,55 @@
// Original file: test/fixtures/test_service.proto
import type * as grpc from './../../src/index'
import type { MethodDefinition } from '@grpc/proto-loader'
import type { Request as _Request, Request__Output as _Request__Output } from './Request';
import type { Response as _Response, Response__Output as _Response__Output } from './Response';
export interface TestServiceClient extends grpc.Client {
BidiStream(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_Request, _Response__Output>;
BidiStream(options?: grpc.CallOptions): grpc.ClientDuplexStream<_Request, _Response__Output>;
bidiStream(metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientDuplexStream<_Request, _Response__Output>;
bidiStream(options?: grpc.CallOptions): grpc.ClientDuplexStream<_Request, _Response__Output>;
ClientStream(metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_Response__Output>): grpc.ClientWritableStream<_Request>;
ClientStream(metadata: grpc.Metadata, callback: grpc.requestCallback<_Response__Output>): grpc.ClientWritableStream<_Request>;
ClientStream(options: grpc.CallOptions, callback: grpc.requestCallback<_Response__Output>): grpc.ClientWritableStream<_Request>;
ClientStream(callback: grpc.requestCallback<_Response__Output>): grpc.ClientWritableStream<_Request>;
clientStream(metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_Response__Output>): grpc.ClientWritableStream<_Request>;
clientStream(metadata: grpc.Metadata, callback: grpc.requestCallback<_Response__Output>): grpc.ClientWritableStream<_Request>;
clientStream(options: grpc.CallOptions, callback: grpc.requestCallback<_Response__Output>): grpc.ClientWritableStream<_Request>;
clientStream(callback: grpc.requestCallback<_Response__Output>): grpc.ClientWritableStream<_Request>;
ServerStream(argument: _Request, metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientReadableStream<_Response__Output>;
ServerStream(argument: _Request, options?: grpc.CallOptions): grpc.ClientReadableStream<_Response__Output>;
serverStream(argument: _Request, metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientReadableStream<_Response__Output>;
serverStream(argument: _Request, options?: grpc.CallOptions): grpc.ClientReadableStream<_Response__Output>;
Unary(argument: _Request, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_Response__Output>): grpc.ClientUnaryCall;
Unary(argument: _Request, metadata: grpc.Metadata, callback: grpc.requestCallback<_Response__Output>): grpc.ClientUnaryCall;
Unary(argument: _Request, options: grpc.CallOptions, callback: grpc.requestCallback<_Response__Output>): grpc.ClientUnaryCall;
Unary(argument: _Request, callback: grpc.requestCallback<_Response__Output>): grpc.ClientUnaryCall;
unary(argument: _Request, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_Response__Output>): grpc.ClientUnaryCall;
unary(argument: _Request, metadata: grpc.Metadata, callback: grpc.requestCallback<_Response__Output>): grpc.ClientUnaryCall;
unary(argument: _Request, options: grpc.CallOptions, callback: grpc.requestCallback<_Response__Output>): grpc.ClientUnaryCall;
unary(argument: _Request, callback: grpc.requestCallback<_Response__Output>): grpc.ClientUnaryCall;
}
export interface TestServiceHandlers extends grpc.UntypedServiceImplementation {
BidiStream: grpc.handleBidiStreamingCall<_Request__Output, _Response>;
ClientStream: grpc.handleClientStreamingCall<_Request__Output, _Response>;
ServerStream: grpc.handleServerStreamingCall<_Request__Output, _Response>;
Unary: grpc.handleUnaryCall<_Request__Output, _Response>;
}
export interface TestServiceDefinition extends grpc.ServiceDefinition {
BidiStream: MethodDefinition<_Request, _Response, _Request__Output, _Response__Output>
ClientStream: MethodDefinition<_Request, _Response, _Request__Output, _Response__Output>
ServerStream: MethodDefinition<_Request, _Response, _Request__Output, _Response__Output>
Unary: MethodDefinition<_Request, _Response, _Request__Output, _Response__Output>
}

View File

@@ -0,0 +1,15 @@
import type * as grpc from '../../src/index';
import type { MessageTypeDefinition } from '@grpc/proto-loader';
import type { TestServiceClient as _TestServiceClient, TestServiceDefinition as _TestServiceDefinition } from './TestService';
type SubtypeConstructor<Constructor extends new (...args: any) => any, Subtype> = {
new(...args: ConstructorParameters<Constructor>): Subtype;
};
export interface ProtoGrpcType {
Request: MessageTypeDefinition
Response: MessageTypeDefinition
TestService: SubtypeConstructor<typeof grpc.Client, _TestServiceClient> & { service: _TestServiceDefinition }
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright 2019 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import assert from "node:assert";
import * as grpc from "@grpc/grpc-js";
const { Metadata, CallCredentials } = grpc;
// Metadata generators
function makeAfterMsElapsedGenerator(ms: number) {
return (options, cb) => {
const metadata = new Metadata();
metadata.add("msElapsed", `${ms}`);
setTimeout(() => cb(null, metadata), ms);
};
}
const generateFromServiceURL = (options, cb) => {
const metadata: Metadata = new Metadata();
metadata.add("service_url", options.service_url);
cb(null, metadata);
};
const generateWithError = (options, cb) => cb(new Error());
// Tests
describe("CallCredentials", () => {
describe("createFromMetadataGenerator", () => {
it("should accept a metadata generator", () => {
assert.doesNotThrow(() => CallCredentials.createFromMetadataGenerator(generateFromServiceURL));
});
});
describe("compose", () => {
it("should accept a CallCredentials object and return a new object", () => {
const callCredentials1 = CallCredentials.createFromMetadataGenerator(generateFromServiceURL);
const callCredentials2 = CallCredentials.createFromMetadataGenerator(generateFromServiceURL);
const combinedCredentials = callCredentials1.compose(callCredentials2);
assert.notStrictEqual(combinedCredentials, callCredentials1);
assert.notStrictEqual(combinedCredentials, callCredentials2);
});
it("should be chainable", () => {
const callCredentials1 = CallCredentials.createFromMetadataGenerator(generateFromServiceURL);
const callCredentials2 = CallCredentials.createFromMetadataGenerator(generateFromServiceURL);
assert.doesNotThrow(() => {
callCredentials1.compose(callCredentials2).compose(callCredentials2).compose(callCredentials2);
});
});
});
describe("generateMetadata", () => {
it("should call the function passed to createFromMetadataGenerator", async () => {
const callCredentials = CallCredentials.createFromMetadataGenerator(generateFromServiceURL);
const metadata: Metadata = await callCredentials.generateMetadata({
method_name: "bar",
service_url: "foo",
});
assert.deepStrictEqual(metadata.get("service_url"), ["foo"]);
});
it("should emit an error if the associated metadataGenerator does", async () => {
const callCredentials = CallCredentials.createFromMetadataGenerator(generateWithError);
let metadata: Metadata | null = null;
try {
metadata = await callCredentials.generateMetadata({ method_name: "", service_url: "" });
} catch (err) {
assert.ok(err instanceof Error);
}
assert.strictEqual(metadata, null);
});
it("should combine metadata from multiple generators", async () => {
const [callCreds1, callCreds2, callCreds3, callCreds4] = [50, 100, 150, 200].map(ms => {
const generator = makeAfterMsElapsedGenerator(ms);
return CallCredentials.createFromMetadataGenerator(generator);
});
const testCases = [
{
credentials: callCreds1.compose(callCreds2).compose(callCreds3).compose(callCreds4),
expected: ["50", "100", "150", "200"],
},
{
credentials: callCreds4.compose(callCreds3.compose(callCreds2.compose(callCreds1))),
expected: ["200", "150", "100", "50"],
},
{
credentials: callCreds3.compose(callCreds4.compose(callCreds1).compose(callCreds2)),
expected: ["150", "200", "50", "100"],
},
];
// Try each test case and make sure the msElapsed field is as expected
await Promise.all(
testCases.map(async testCase => {
const { credentials, expected } = testCase;
const metadata: Metadata = await credentials.generateMetadata({
method_name: "",
service_url: "",
});
assert.deepStrictEqual(metadata.get("msElapsed"), expected);
}),
);
});
});
});

Some files were not shown because too many files have changed in this diff Show More