From 1c754b4083673a30dc6778d52dbf9bee0963ebb4 Mon Sep 17 00:00:00 2001 From: cola119 Date: Thu, 27 Jun 2024 20:06:07 +0900 Subject: [PATCH] inspector: add initial support for network inspection --- lib/internal/process/pre_execution.js | 47 +++++++ src/inspector/network_agent.cc | 61 ++++++++ src/inspector/network_agent.h | 49 +++++++ src/inspector/node_inspector.gypi | 4 + src/inspector/node_protocol.pdl | 65 +++++++++ src/inspector/node_string.cc | 2 +- src/inspector_agent.cc | 103 ++++++++++++++ src/inspector_agent.h | 15 ++ src/inspector_js_api.cc | 76 ++++++++++ .../test-inspector-network-domain.mjs | 130 ++++++++++++++++++ 10 files changed, 551 insertions(+), 1 deletion(-) create mode 100644 src/inspector/network_agent.cc create mode 100644 src/inspector/network_agent.h create mode 100644 test/parallel/test-inspector-network-domain.mjs diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js index 27df0a9440a03c..b996e417ee41cc 100644 --- a/lib/internal/process/pre_execution.js +++ b/lib/internal/process/pre_execution.js @@ -3,6 +3,7 @@ const { ArrayPrototypeForEach, Date, + DateNow, DatePrototypeGetDate, DatePrototypeGetFullYear, DatePrototypeGetHours, @@ -98,6 +99,7 @@ function prepareExecution(options) { const mainEntry = patchProcessObject(expandArgv1); setupTraceCategoryState(); setupInspectorHooks(); + setupNetworkInspection(); setupNavigator(); setupWarningHandler(); setupWebStorage(); @@ -438,6 +440,51 @@ function setupInspectorHooks() { } } +function setupNetworkInspection() { + if (!internalBinding('config').hasInspector) { + return; + } + const dc = require('diagnostics_channel'); + const { + dataReceived, + loadingFinished, + requestWillBeSent, + responseReceived, + } = internalBinding('inspector'); + + let requestId = 0; + const getNextRequestId = () => `node-network-event-${++requestId}`; + + dc.subscribe('http.client.request.start', ({ request }) => { + const url = `${request.protocol}//${request.host}${request.path}`; + const wallTime = DateNow(); + const timestamp = wallTime / 1000; + request._inspectorRequestId = getNextRequestId(); + requestWillBeSent(request._inspectorRequestId, url, request.method, timestamp, wallTime); + }); + dc.subscribe('http.client.response.finish', ({ request, response }) => { + responseReceived(request._inspectorRequestId, DateNow() / 1000); + let responseString = ''; + const onData = (chunk) => { + dataReceived(request._inspectorRequestId, DateNow() / 1000, chunk.length); + responseString += chunk.toString(); + }; + response.on('data', onData); + response.on('end', () => { + loadingFinished(request._inspectorRequestId, responseString, DateNow() / 1000, responseString.length); + response.removeListener('data', onData); + }); + }); + + dc.subscribe('undici:request:create', ({ request }) => { + const url = `${request.origin}${request.path}`; + const wallTime = DateNow(); + const timestamp = wallTime / 1000; + request._inspectorRequestId = getNextRequestId(); + requestWillBeSent(request._inspectorRequestId, url, request.method, timestamp, wallTime); + }); +} + // In general deprecations are initialized wherever the APIs are implemented, // this is used to deprecate APIs implemented in C++ where the deprecation // utilities are not easily accessible. diff --git a/src/inspector/network_agent.cc b/src/inspector/network_agent.cc new file mode 100644 index 00000000000000..76d7eeec6ed89f --- /dev/null +++ b/src/inspector/network_agent.cc @@ -0,0 +1,61 @@ +#include "network_agent.h" + +#include "inspector_agent.h" + +namespace node { +namespace inspector { +namespace protocol { + +std::unique_ptr Request(const String& url, + const String& method) { + return Network::Request::create().setUrl(url).setMethod(method).build(); +} + +NetworkAgent::NetworkAgent() {} + +void NetworkAgent::Wire(UberDispatcher* dispatcher) { + frontend_ = std::make_unique(dispatcher->channel()); + Network::Dispatcher::wire(dispatcher, this); +} + +DispatchResponse NetworkAgent::getResponseBody(const String& in_requestId, + String* out_body) { + auto it = request_id_to_response_.find(in_requestId); + if (it != request_id_to_response_.end()) { + *out_body = it->second; + request_id_to_response_.erase(it); + } + return DispatchResponse::OK(); +} + +void NetworkAgent::requestWillBeSent(const String& request_id, + const String& url, + const String& method, + double timestamp, + double wall_time) { + frontend_->requestWillBeSent( + request_id, Request(url, method), timestamp, wall_time); +} + +void NetworkAgent::responseReceived(const String& request_id, + double timestamp) { + frontend_->responseReceived(request_id, timestamp); +} + +void NetworkAgent::dataReceived(const String& request_id, + double timestamp, + int data_length) { + frontend_->dataReceived(request_id, timestamp, data_length); +} + +void NetworkAgent::loadingFinished(const String& request_id, + const String& response, + double timestamp, + int encoded_data_length) { + request_id_to_response_[request_id] = response; + frontend_->loadingFinished(request_id, timestamp, encoded_data_length); +} + +} // namespace protocol +} // namespace inspector +} // namespace node diff --git a/src/inspector/network_agent.h b/src/inspector/network_agent.h new file mode 100644 index 00000000000000..d4b9482657653e --- /dev/null +++ b/src/inspector/network_agent.h @@ -0,0 +1,49 @@ +#ifndef SRC_INSPECTOR_NETWORK_AGENT_H_ +#define SRC_INSPECTOR_NETWORK_AGENT_H_ + +#include "node/inspector/protocol/Network.h" +#include "v8.h" + +#include + +namespace node { + +namespace inspector { +namespace protocol { + +class NetworkAgent : public Network::Backend { + public: + NetworkAgent(); + + void Wire(UberDispatcher* dispatcher); + + DispatchResponse getResponseBody(const String& in_requestId, + String* out_body) override; + + void requestWillBeSent(const String& request_id, + const String& url, + const String& method, + double timestamp, + double wall_time); + + void responseReceived(const String& request_id, double timestamp); + + void dataReceived(const String& request_id, + double timestamp, + int data_length); + + void loadingFinished(const String& request_id, + const String& response, + double timestamp, + int encoded_data_length); + + private: + std::shared_ptr frontend_; + std::unordered_map request_id_to_response_; +}; + +} // namespace protocol +} // namespace inspector +} // namespace node + +#endif // SRC_INSPECTOR_NETWORK_AGENT_H_ diff --git a/src/inspector/node_inspector.gypi b/src/inspector/node_inspector.gypi index a2dfdcb42db196..45aac95ee449b3 100644 --- a/src/inspector/node_inspector.gypi +++ b/src/inspector/node_inspector.gypi @@ -23,6 +23,8 @@ 'src/inspector/tracing_agent.h', 'src/inspector/worker_agent.cc', 'src/inspector/worker_agent.h', + 'src/inspector/network_agent.cc', + 'src/inspector/network_agent.h', 'src/inspector/worker_inspector.cc', 'src/inspector/worker_inspector.h', ], @@ -36,6 +38,8 @@ '<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/NodeTracing.h', '<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/NodeRuntime.cpp', '<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/NodeRuntime.h', + '<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/Network.cpp', + '<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/Network.h', ], 'node_protocol_files': [ '<(protocol_tool_path)/lib/Allocator_h.template', diff --git a/src/inspector/node_protocol.pdl b/src/inspector/node_protocol.pdl index d8a873de263f23..266145330c852a 100644 --- a/src/inspector/node_protocol.pdl +++ b/src/inspector/node_protocol.pdl @@ -98,6 +98,71 @@ experimental domain NodeWorker SessionID sessionId string message +# Partial support for Network domain of ChromeDevTools Protocol. +# https://chromedevtools.github.io/devtools-protocol/tot/Network +experimental domain Network + # Unique request identifier. + type RequestId extends string + + # UTC time in seconds, counted from January 1, 1970. + type TimeSinceEpoch extends number + + # Monotonically increasing time in seconds since an arbitrary point in the past. + type MonotonicTime extends number + + # HTTP request data. + type Request extends object + properties + string url + string method + + # Returns content served for the given request. + command getResponseBody + parameters + # Identifier of the network request to get content for. + RequestId requestId + returns + # Response body. + string body + + # Fired when page is about to send HTTP request. + event requestWillBeSent + parameters + # Request identifier. + RequestId requestId + # Request data. + Request request + # Timestamp. + MonotonicTime timestamp + # Timestamp. + TimeSinceEpoch wallTime + + # Fired when HTTP response is available. + event responseReceived + parameters + # Request identifier. + RequestId requestId + # Timestamp. + MonotonicTime timestamp + + event dataReceived + parameters + # Request identifier. + RequestId requestId + # Timestamp. + MonotonicTime timestamp + # Data chunk length. + integer dataLength + + event loadingFinished + parameters + # Request identifier. + RequestId requestId + # Timestamp. + MonotonicTime timestamp + # Total number of bytes received for this request. + number encodedDataLength + # Support for inspecting node process state. experimental domain NodeRuntime # Enable the NodeRuntime events except by `NodeRuntime.waitingForDisconnect`. diff --git a/src/inspector/node_string.cc b/src/inspector/node_string.cc index 7960971a094fd4..c62e7ed30c4e19 100644 --- a/src/inspector/node_string.cc +++ b/src/inspector/node_string.cc @@ -84,7 +84,7 @@ String StringViewToUtf8(v8_inspector::StringView view) { String fromDouble(double d) { std::ostringstream stream; stream.imbue(std::locale::classic()); // Ignore current locale - stream << d; + stream << std::fixed << d; return stream.str(); } diff --git a/src/inspector_agent.cc b/src/inspector_agent.cc index f298ab73285f4e..54845ba5f289f8 100644 --- a/src/inspector_agent.cc +++ b/src/inspector_agent.cc @@ -2,6 +2,7 @@ #include "env-inl.h" #include "inspector/main_thread_interface.h" +#include "inspector/network_agent.h" #include "inspector/node_string.h" #include "inspector/runtime_agent.h" #include "inspector/tracing_agent.h" @@ -231,6 +232,8 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel, } runtime_agent_ = std::make_unique(); runtime_agent_->Wire(node_dispatcher_.get()); + network_agent_ = std::make_unique(); + network_agent_->Wire(node_dispatcher_.get()); } ~ChannelImpl() override { @@ -242,6 +245,8 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel, } runtime_agent_->disable(); runtime_agent_.reset(); // Dispose before the dispatchers + network_agent_->disable(); + network_agent_.reset(); // Dispose before the dispatchers } void dispatchProtocolMessage(const StringView& message) { @@ -282,6 +287,33 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel, void unsetWaitingForDebugger() { runtime_agent_->unsetWaitingForDebugger(); } + void requestWillBeSent(const std::string& request_id, + const std::string& url, + const std::string& method, + double timestamp, + double wall_time) { + network_agent_->requestWillBeSent( + request_id, url, method, timestamp, wall_time); + } + + void responseReceived(const std::string& request_id, double timestamp) { + network_agent_->responseReceived(request_id, timestamp); + } + + void dataReceived(const std::string& request_id, + double timestamp, + int data_length) { + network_agent_->dataReceived(request_id, timestamp, data_length); + } + + void loadingFinished(const std::string& request_id, + const std::string& response, + double timestamp, + int encoded_data_length) { + network_agent_->loadingFinished( + request_id, response, timestamp, encoded_data_length); + } + bool retainingContext() { return retaining_context_; } @@ -335,6 +367,7 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel, std::unique_ptr runtime_agent_; std::unique_ptr tracing_agent_; std::unique_ptr worker_agent_; + std::unique_ptr network_agent_; std::unique_ptr delegate_; std::unique_ptr session_; std::unique_ptr node_dispatcher_; @@ -629,6 +662,41 @@ class NodeInspectorClient : public V8InspectorClient { return retaining_context; } + void requestWillBeSent(const std::string& request_id, + const std::string& url, + const std::string& method, + double timestamp, + double wall_time) { + for (const auto& id_channel : channels_) { + id_channel.second->requestWillBeSent( + request_id, url, method, timestamp, wall_time); + } + } + + void responseReceived(const std::string& request_id, double timestamp) { + for (const auto& id_channel : channels_) { + id_channel.second->responseReceived(request_id, timestamp); + } + } + + void dataReceived(const std::string& request_id, + double timestamp, + int data_length) { + for (const auto& id_channel : channels_) { + id_channel.second->dataReceived(request_id, timestamp, data_length); + } + } + + void loadingFinished(const std::string& request_id, + const std::string& response, + double timestamp, + int encoded_data_length) { + for (const auto& id_channel : channels_) { + id_channel.second->loadingFinished( + request_id, response, timestamp, encoded_data_length); + } + } + std::shared_ptr getThreadHandle() { if (!interface_) { interface_ = std::make_shared( @@ -853,6 +921,41 @@ std::unique_ptr Agent::ConnectToMainThread( prevent_shutdown); } +void Agent::RequestWillBeSent(const StringView& request_id, + const StringView& url, + const StringView& method, + double timestamp, + double wall_time) { + client_->requestWillBeSent(protocol::StringUtil::StringViewToUtf8(request_id), + protocol::StringUtil::StringViewToUtf8(url), + protocol::StringUtil::StringViewToUtf8(method), + timestamp, + wall_time); +} + +void Agent::ResponseReceived(const StringView& request_id, double timestamp) { + client_->responseReceived(protocol::StringUtil::StringViewToUtf8(request_id), + timestamp); +} + +void Agent::DataReceived(const StringView& request_id, + double timestamp, + int data_length) { + client_->dataReceived(protocol::StringUtil::StringViewToUtf8(request_id), + timestamp, + data_length); +} + +void Agent::LoadingFinished(const StringView& request_id, + const StringView& response, + double timestamp, + int encoded_data_length) { + client_->loadingFinished(protocol::StringUtil::StringViewToUtf8(request_id), + protocol::StringUtil::StringViewToUtf8(response), + timestamp, + encoded_data_length); +} + void Agent::WaitForDisconnect() { THROW_IF_INSUFFICIENT_PERMISSIONS(parent_env_, permission::PermissionScope::kInspector, diff --git a/src/inspector_agent.h b/src/inspector_agent.h index 0f27aff61a3955..7e466225e31e7a 100644 --- a/src/inspector_agent.h +++ b/src/inspector_agent.h @@ -68,6 +68,21 @@ class Agent { void ReportUncaughtException(v8::Local error, v8::Local message); + void RequestWillBeSent(const v8_inspector::StringView& request_id, + const v8_inspector::StringView& url, + const v8_inspector::StringView& method, + double timestamp, + double wall_time); + void ResponseReceived(const v8_inspector::StringView& request_id, + double timestamp); + void DataReceived(const v8_inspector::StringView& request_id, + double timestamp, + int data_length); + void LoadingFinished(const v8_inspector::StringView& request_id, + const v8_inspector::StringView& response, + double timestamp, + int encoded_data_length); + // Async stack traces instrumentation. void AsyncTaskScheduled(const v8_inspector::StringView& taskName, void* task, bool recurring); diff --git a/src/inspector_js_api.cc b/src/inspector_js_api.cc index 5700f8c5efc698..076ce08b07282c 100644 --- a/src/inspector_js_api.cc +++ b/src/inspector_js_api.cc @@ -23,6 +23,7 @@ using v8::Isolate; using v8::Local; using v8::MaybeLocal; using v8::NewStringType; +using v8::Number; using v8::Object; using v8::String; using v8::Uint32; @@ -270,6 +271,71 @@ static void RegisterAsyncHookWrapper(const FunctionCallbackInfo& args) { enable_function, disable_function); } +void RequestWillBeSent(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args[0]->IsString()); + Local request_id = args[0].As(); + CHECK(args[1]->IsString()); + Local url = args[1].As(); + CHECK(args[2]->IsString()); + Local method = args[2].As(); + CHECK(args[3]->IsNumber()); + double timestamp = args[3].As()->Value(); + CHECK(args[4]->IsNumber()); + double wall_time = args[4].As()->Value(); + + env->inspector_agent()->RequestWillBeSent( + ToProtocolString(env->isolate(), request_id)->string(), + ToProtocolString(env->isolate(), url)->string(), + ToProtocolString(env->isolate(), method)->string(), + timestamp, + wall_time); +} + +void ResponseReceived(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args[0]->IsString()); + Local request_id = args[0].As(); + CHECK(args[1]->IsNumber()); + double timestamp = args[1].As()->Value(); + + env->inspector_agent()->ResponseReceived( + ToProtocolString(env->isolate(), request_id)->string(), timestamp); +} + +void DataReceived(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args[0]->IsString()); + Local request_id = args[0].As(); + CHECK(args[1]->IsNumber()); + double timestamp = args[1].As()->Value(); + CHECK(args[2]->IsNumber()); + double data_length = args[2].As()->Value(); + + env->inspector_agent()->DataReceived( + ToProtocolString(env->isolate(), request_id)->string(), + timestamp, + static_cast(data_length)); +} + +void LoadingFinished(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args[0]->IsString()); + Local request_id = args[0].As(); + CHECK(args[1]->IsString()); + Local response = args[1].As(); + CHECK(args[2]->IsNumber()); + double timestamp = args[2].As()->Value(); + CHECK(args[3]->IsNumber()); + double encoded_data_length = args[3].As()->Value(); + + env->inspector_agent()->LoadingFinished( + ToProtocolString(env->isolate(), request_id)->string(), + ToProtocolString(env->isolate(), response)->string(), + timestamp, + static_cast(encoded_data_length)); +} + void IsEnabled(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); args.GetReturnValue().Set(env->inspector_agent()->IsListening()); @@ -355,6 +421,11 @@ void Initialize(Local target, Local unused, SetMethod(context, target, "registerAsyncHook", RegisterAsyncHookWrapper); SetMethodNoSideEffect(context, target, "isEnabled", IsEnabled); + SetMethod(context, target, "requestWillBeSent", RequestWillBeSent); + SetMethod(context, target, "responseReceived", ResponseReceived); + SetMethod(context, target, "dataReceived", DataReceived); + SetMethod(context, target, "loadingFinished", LoadingFinished); + Local console_string = FIXED_ONE_BYTE_STRING(isolate, "console"); // Grab the console from the binding object and expose those to our binding @@ -388,6 +459,11 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(RegisterAsyncHookWrapper); registry->Register(IsEnabled); + registry->Register(RequestWillBeSent); + registry->Register(ResponseReceived); + registry->Register(DataReceived); + registry->Register(LoadingFinished); + registry->Register(JSBindingsConnection::New); registry->Register(JSBindingsConnection::Dispatch); registry->Register(JSBindingsConnection::Disconnect); diff --git a/test/parallel/test-inspector-network-domain.mjs b/test/parallel/test-inspector-network-domain.mjs new file mode 100644 index 00000000000000..8fcfaf61e26186 --- /dev/null +++ b/test/parallel/test-inspector-network-domain.mjs @@ -0,0 +1,130 @@ +import * as common from '../common/index.mjs'; + +common.skipIfInspectorDisabled(); + +import assert from 'node:assert'; +import * as fixtures from '../common/fixtures.mjs'; +import http from 'node:http'; +import https from 'node:https'; +import inspector from 'node:inspector'; + +const session = new inspector.Session(); +session.connect(); + +const httpServer = http.createServer((req, res) => { + const path = req.url; + switch (path) { + case '/hello-world': + res.writeHead(200); + res.end('hello world\n'); + break; + default: + assert(false, `Unexpected path: ${path}`); + } +}); + +const httpsServer = https.createServer({ + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') +}, (req, res) => { + const path = req.url; + switch (path) { + case '/hello-world': + res.writeHead(200); + res.end('hello world\n'); + break; + default: + assert(false, `Unexpected path: ${path}`); + } +}); + +const terminate = () => { + session.disconnect(); + httpServer.close(); + httpsServer.close(); + inspector.close(); +}; + +const testHttpGet = () => new Promise((resolve, reject) => { + session.on('Network.requestWillBeSent', common.mustCall(({ params }) => { + assert.ok(params.requestId.startsWith('node-network-event-')); + assert.strictEqual(params.request.url, 'http://127.0.0.1/hello-world'); + assert.strictEqual(params.request.method, 'GET'); + assert.strictEqual(typeof params.timestamp, 'number'); + assert.strictEqual(typeof params.wallTime, 'number'); + })); + + session.on('Network.responseReceived', common.mustCall(({ params }) => { + assert.ok(params.requestId.startsWith('node-network-event-')); + assert.strictEqual(typeof params.timestamp, 'number'); + })); + + session.on('Network.dataReceived', common.mustCall(({ params }) => { + assert.ok(params.requestId.startsWith('node-network-event-')); + assert.strictEqual(typeof params.timestamp, 'number'); + assert.strictEqual(typeof params.dataLength, 'number'); + })); + + session.on('Network.loadingFinished', common.mustCall(({ params }) => { + assert.ok(params.requestId.startsWith('node-network-event-')); + assert.strictEqual(typeof params.timestamp, 'number'); + resolve(); + })); + + http.get({ + host: '127.0.0.1', + port: httpServer.address().port, + path: '/hello-world', + }, common.mustCall()); +}); + +const testHttpsGet = () => new Promise((resolve, reject) => { + session.on('Network.requestWillBeSent', common.mustCall(({ params }) => { + assert.ok(params.requestId.startsWith('node-network-event-')); + assert.strictEqual(params.request.url, 'https://127.0.0.1/hello-world'); + assert.strictEqual(params.request.method, 'GET'); + assert.strictEqual(typeof params.timestamp, 'number'); + assert.strictEqual(typeof params.wallTime, 'number'); + })); + + session.on('Network.responseReceived', common.mustCall(({ params }) => { + assert.ok(params.requestId.startsWith('node-network-event-')); + assert.strictEqual(typeof params.timestamp, 'number'); + })); + + session.on('Network.dataReceived', common.mustCall(({ params }) => { + assert.ok(params.requestId.startsWith('node-network-event-')); + assert.strictEqual(typeof params.timestamp, 'number'); + assert.strictEqual(typeof params.dataLength, 'number'); + })); + + session.on('Network.loadingFinished', common.mustCall(({ params }) => { + assert.ok(params.requestId.startsWith('node-network-event-')); + assert.strictEqual(typeof params.timestamp, 'number'); + resolve(); + })); + + https.get({ + host: '127.0.0.1', + port: httpsServer.address().port, + path: '/hello-world', + rejectUnauthorized: false, + }, common.mustCall()); +}); + +const test = async () => { + await testHttpGet(); + session.removeAllListeners(); + await testHttpsGet(); + session.removeAllListeners(); +}; + +httpServer.listen(0, () => { + httpsServer.listen(0, () => { + test().then(common.mustCall()).catch(() => { + assert.fail(); + }).finally(() => { + terminate(); + }); + }); +});