From de1fbc292fb817f25ed691e1286c17c8d7848207 Mon Sep 17 00:00:00 2001 From: Kohei Ueno Date: Fri, 19 Jul 2024 14:00:30 +0900 Subject: [PATCH] inspector: add initial support for network inspection PR-URL: https://github.com/nodejs/node/pull/53593 Reviewed-By: Benjamin Gruenbaum Reviewed-By: James M Snell Reviewed-By: Matteo Collina Reviewed-By: Stephen Belanger Reviewed-By: Joyee Cheung Reviewed-By: Chengzhong Wu Reviewed-By: Paolo Insogna --- doc/api/cli.md | 11 ++ doc/api/inspector.md | 69 ++++++++++ lib/inspector.js | 16 +++ lib/internal/inspector_network_tracking.js | 63 +++++++++ lib/internal/process/pre_execution.js | 11 ++ src/env_properties.h | 2 + src/inspector/network_agent.cc | 84 ++++++++++++ src/inspector/network_agent.h | 49 +++++++ src/inspector/network_inspector.cc | 48 +++++++ src/inspector/network_inspector.h | 38 ++++++ src/inspector/node_inspector.gypi | 6 + src/inspector/node_protocol.pdl | 51 ++++++++ src/inspector/node_string.cc | 2 +- src/inspector_agent.cc | 92 +++++++++++++- src/inspector_agent.h | 13 ++ src/inspector_js_api.cc | 28 ++++ src/node_options.cc | 3 + src/node_options.h | 1 + .../test-inspector-emit-protocol-event.js | 85 +++++++++++++ .../parallel/test-inspector-network-domain.js | 120 ++++++++++++++++++ 20 files changed, 789 insertions(+), 3 deletions(-) create mode 100644 lib/internal/inspector_network_tracking.js create mode 100644 src/inspector/network_agent.cc create mode 100644 src/inspector/network_agent.h create mode 100644 src/inspector/network_inspector.cc create mode 100644 src/inspector/network_inspector.h create mode 100644 test/parallel/test-inspector-emit-protocol-event.js create mode 100644 test/parallel/test-inspector-network-domain.js diff --git a/doc/api/cli.md b/doc/api/cli.md index 651ac02467ae45..320ad45b50aa58 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1028,6 +1028,17 @@ added: Enable experimental support for the `https:` protocol in `import` specifiers. +### `--experimental-network-inspection` + + + +> Stability: 1 - Experimental + +Enable experimental support for the network inspection with Chrome DevTools. + ### `--experimental-permission` + +> Stability: 1 - Experimental + +* `params` {Object} + +This feature is only available with the `--experimental-network-inspection` flag enabled. + +Broadcasts the `Network.requestWillBeSent` event to connected frontends. This event indicates that +the application is about to send an HTTP request. + +### `inspector.Network.responseReceived([params])` + + + +> Stability: 1 - Experimental + +* `params` {Object} + +This feature is only available with the `--experimental-network-inspection` flag enabled. + +Broadcasts the `Network.responseReceived` event to connected frontends. This event indicates that +HTTP response is available. + +### `inspector.Network.loadingFinished([params])` + + + +> Stability: 1 - Experimental + +* `params` {Object} + +This feature is only available with the `--experimental-network-inspection` flag enabled. + +Broadcasts the `Network.loadingFinished` event to connected frontends. This event indicates that +HTTP request has finished loading. + ## Support of breakpoints The Chrome DevTools Protocol [`Debugger` domain][] allows an diff --git a/lib/inspector.js b/lib/inspector.js index e51bcf2f3cd977..b38bb1af974819 100644 --- a/lib/inspector.js +++ b/lib/inspector.js @@ -42,6 +42,7 @@ const { isEnabled, waitForDebugger, console, + emitProtocolEvent, } = internalBinding('inspector'); class Session extends EventEmitter { @@ -188,6 +189,20 @@ function inspectorWaitForDebugger() { throw new ERR_INSPECTOR_NOT_ACTIVE(); } +function broadcastToFrontend(eventName, params) { + validateString(eventName, 'eventName'); + if (params) { + validateObject(params, 'params'); + } + emitProtocolEvent(eventName, JSONStringify(params ?? {})); +} + +const Network = { + requestWillBeSent: (params) => broadcastToFrontend('Network.requestWillBeSent', params), + responseReceived: (params) => broadcastToFrontend('Network.responseReceived', params), + loadingFinished: (params) => broadcastToFrontend('Network.loadingFinished', params), +}; + module.exports = { open: inspectorOpen, close: _debugEnd, @@ -195,4 +210,5 @@ module.exports = { waitForDebugger: inspectorWaitForDebugger, console, Session, + Network, }; diff --git a/lib/internal/inspector_network_tracking.js b/lib/internal/inspector_network_tracking.js new file mode 100644 index 00000000000000..4865537e37b7d5 --- /dev/null +++ b/lib/internal/inspector_network_tracking.js @@ -0,0 +1,63 @@ +'use strict'; + +const { + DateNow, +} = primordials; + +let dc; +let Network; + +let requestId = 0; +const getNextRequestId = () => `node-network-event-${++requestId}`; + +function onClientRequestStart({ request }) { + const url = `${request.protocol}//${request.host}${request.path}`; + const wallTime = DateNow(); + const timestamp = wallTime / 1000; + request._inspectorRequestId = getNextRequestId(); + Network.requestWillBeSent({ + requestId: request._inspectorRequestId, + timestamp, + wallTime, + request: { + url, + method: request.method, + }, + }); +} + +function onClientResponseFinish({ request }) { + if (typeof request._inspectorRequestId !== 'string') { + return; + } + const timestamp = DateNow() / 1000; + Network.responseReceived({ + requestId: request._inspectorRequestId, + timestamp, + }); + Network.loadingFinished({ + requestId: request._inspectorRequestId, + timestamp, + }); +} + +function enable() { + if (!dc) { + dc = require('diagnostics_channel'); + } + if (!Network) { + Network = require('inspector').Network; + } + dc.subscribe('http.client.request.start', onClientRequestStart); + dc.subscribe('http.client.response.finish', onClientResponseFinish); +} + +function disable() { + dc.unsubscribe('http.client.request.start', onClientRequestStart); + dc.unsubscribe('http.client.response.finish', onClientResponseFinish); +} + +module.exports = { + enable, + disable, +}; diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js index 383dee4e6988e4..080e6d55bb2a29 100644 --- a/lib/internal/process/pre_execution.js +++ b/lib/internal/process/pre_execution.js @@ -106,6 +106,7 @@ function prepareExecution(options) { const mainEntry = patchProcessObject(expandArgv1); setupTraceCategoryState(); setupInspectorHooks(); + setupNetworkInspection(); setupNavigator(); setupWarningHandler(); setupUndici(); @@ -513,6 +514,16 @@ function setupInspectorHooks() { } } +function setupNetworkInspection() { + if (internalBinding('config').hasInspector && getOptionValue('--experimental-network-inspection')) { + const { + enable, + disable, + } = require('internal/inspector_network_tracking'); + internalBinding('inspector').setupNetworkTracking(enable, disable); + } +} + // 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/env_properties.h b/src/env_properties.h index c6262a79504e61..1ed8ab4d116313 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -448,7 +448,9 @@ V(immediate_callback_function, v8::Function) \ V(inspector_console_extension_installer, v8::Function) \ V(inspector_disable_async_hooks, v8::Function) \ + V(inspector_disable_network_tracking, v8::Function) \ V(inspector_enable_async_hooks, v8::Function) \ + V(inspector_enable_network_tracking, v8::Function) \ V(maybe_cache_generated_source_map, v8::Function) \ V(messaging_deserialize_create_object, v8::Function) \ V(message_port, v8::Object) \ diff --git a/src/inspector/network_agent.cc b/src/inspector/network_agent.cc new file mode 100644 index 00000000000000..de17ff0ecb2041 --- /dev/null +++ b/src/inspector/network_agent.cc @@ -0,0 +1,84 @@ +#include "network_agent.h" +#include "network_inspector.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(NetworkInspector* inspector) + : inspector_(inspector) { + event_notifier_map_["requestWillBeSent"] = &NetworkAgent::requestWillBeSent; + event_notifier_map_["responseReceived"] = &NetworkAgent::responseReceived; + event_notifier_map_["loadingFinished"] = &NetworkAgent::loadingFinished; +} + +void NetworkAgent::emitNotification( + const String& event, std::unique_ptr params) { + if (!inspector_->IsEnabled()) return; + auto it = event_notifier_map_.find(event); + if (it != event_notifier_map_.end()) { + (this->*(it->second))(std::move(params)); + } +} + +void NetworkAgent::Wire(UberDispatcher* dispatcher) { + frontend_ = std::make_unique(dispatcher->channel()); + Network::Dispatcher::wire(dispatcher, this); +} + +DispatchResponse NetworkAgent::enable() { + inspector_->Enable(); + return DispatchResponse::OK(); +} + +DispatchResponse NetworkAgent::disable() { + inspector_->Disable(); + return DispatchResponse::OK(); +} + +void NetworkAgent::requestWillBeSent( + std::unique_ptr params) { + String request_id; + params->getString("requestId", &request_id); + double timestamp; + params->getDouble("timestamp", ×tamp); + double wall_time; + params->getDouble("wallTime", &wall_time); + auto request = params->getObject("request"); + String url; + request->getString("url", &url); + String method; + request->getString("method", &method); + + frontend_->requestWillBeSent( + request_id, Request(url, method), timestamp, wall_time); +} + +void NetworkAgent::responseReceived( + std::unique_ptr params) { + String request_id; + params->getString("requestId", &request_id); + double timestamp; + params->getDouble("timestamp", ×tamp); + + frontend_->responseReceived(request_id, timestamp); +} + +void NetworkAgent::loadingFinished( + std::unique_ptr params) { + String request_id; + params->getString("requestId", &request_id); + double timestamp; + params->getDouble("timestamp", ×tamp); + + frontend_->loadingFinished(request_id, timestamp); +} + +} // 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..e2ca447b6e9480 --- /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 + +namespace node { + +namespace inspector { +class NetworkInspector; + +namespace protocol { + +std::unique_ptr Request(const String& url, + const String& method); + +class NetworkAgent : public Network::Backend { + public: + explicit NetworkAgent(NetworkInspector* inspector); + + void Wire(UberDispatcher* dispatcher); + + DispatchResponse enable() override; + + DispatchResponse disable() override; + + void emitNotification(const String& event, + std::unique_ptr params); + + void requestWillBeSent(std::unique_ptr params); + + void responseReceived(std::unique_ptr params); + + void loadingFinished(std::unique_ptr params); + + private: + NetworkInspector* inspector_; + std::shared_ptr frontend_; + using EventNotifier = + void (NetworkAgent::*)(std::unique_ptr); + std::unordered_map event_notifier_map_; +}; + +} // namespace protocol +} // namespace inspector +} // namespace node + +#endif // SRC_INSPECTOR_NETWORK_AGENT_H_ diff --git a/src/inspector/network_inspector.cc b/src/inspector/network_inspector.cc new file mode 100644 index 00000000000000..a03a66d461e527 --- /dev/null +++ b/src/inspector/network_inspector.cc @@ -0,0 +1,48 @@ +#include "network_inspector.h" + +namespace node { +namespace inspector { + +NetworkInspector::NetworkInspector(Environment* env) + : enabled_(false), env_(env) { + network_agent_ = std::make_unique(this); +} +NetworkInspector::~NetworkInspector() { + network_agent_.reset(); +} + +void NetworkInspector::Wire(protocol::UberDispatcher* dispatcher) { + network_agent_->Wire(dispatcher); +} + +bool NetworkInspector::canEmit(const std::string& domain) { + return domain == "Network"; +} + +void NetworkInspector::emitNotification( + const std::string& domain, + const std::string& method, + std::unique_ptr params) { + if (domain == "Network") { + network_agent_->emitNotification(method, std::move(params)); + } else { + UNREACHABLE("Unknown domain"); + } +} + +void NetworkInspector::Enable() { + if (auto agent = env_->inspector_agent()) { + agent->EnableNetworkTracking(); + } + enabled_ = true; +} + +void NetworkInspector::Disable() { + if (auto agent = env_->inspector_agent()) { + agent->DisableNetworkTracking(); + } + enabled_ = false; +} + +} // namespace inspector +} // namespace node diff --git a/src/inspector/network_inspector.h b/src/inspector/network_inspector.h new file mode 100644 index 00000000000000..1a30997bad98f1 --- /dev/null +++ b/src/inspector/network_inspector.h @@ -0,0 +1,38 @@ +#ifndef SRC_INSPECTOR_NETWORK_INSPECTOR_H_ +#define SRC_INSPECTOR_NETWORK_INSPECTOR_H_ + +#include "env.h" +#include "network_agent.h" + +namespace node { +class Environment; + +namespace inspector { + +class NetworkInspector { + public: + explicit NetworkInspector(Environment* env); + ~NetworkInspector(); + + void Wire(protocol::UberDispatcher* dispatcher); + + bool canEmit(const std::string& domain); + + void emitNotification(const std::string& domain, + const std::string& method, + std::unique_ptr params); + + void Enable(); + void Disable(); + bool IsEnabled() const { return enabled_; } + + private: + bool enabled_; + Environment* env_; + std::unique_ptr network_agent_; +}; + +} // namespace inspector +} // namespace node + +#endif // SRC_INSPECTOR_NETWORK_INSPECTOR_H_ diff --git a/src/inspector/node_inspector.gypi b/src/inspector/node_inspector.gypi index a2dfdcb42db196..d559004be80944 100644 --- a/src/inspector/node_inspector.gypi +++ b/src/inspector/node_inspector.gypi @@ -23,6 +23,10 @@ 'src/inspector/tracing_agent.h', 'src/inspector/worker_agent.cc', 'src/inspector/worker_agent.h', + 'src/inspector/network_inspector.cc', + 'src/inspector/network_inspector.h', + 'src/inspector/network_agent.cc', + 'src/inspector/network_agent.h', 'src/inspector/worker_inspector.cc', 'src/inspector/worker_inspector.h', ], @@ -36,6 +40,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..ab7b6a5414c846 100644 --- a/src/inspector/node_protocol.pdl +++ b/src/inspector/node_protocol.pdl @@ -98,6 +98,57 @@ 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 + + # Disables network tracking, prevents network events from being sent to the client. + command disable + + # Enables network tracking, network events will now be delivered to the client. + command enable + + # 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 loadingFinished + parameters + # Request identifier. + RequestId requestId + # Timestamp. + MonotonicTime timestamp + # 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 4cab7dea04379c..bb39a0cb42a7be 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_inspector.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_inspector_ = std::make_unique(env); + network_inspector_->Wire(node_dispatcher_.get()); } ~ChannelImpl() override { @@ -242,6 +245,24 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel, } runtime_agent_->disable(); runtime_agent_.reset(); // Dispose before the dispatchers + network_inspector_->Disable(); + network_inspector_.reset(); // Dispose before the dispatchers + } + + void emitNotificationFromBackend(const StringView& event, + const StringView& params) { + std::unique_ptr value = + protocol::DictionaryValue::cast( + protocol::StringUtil::parseJSON(params)); + std::string raw_event = protocol::StringUtil::StringViewToUtf8(event); + std::string domain_name = raw_event.substr(0, raw_event.find('.')); + std::string event_name = raw_event.substr(raw_event.find('.') + 1); + if (network_inspector_->canEmit(domain_name)) { + network_inspector_->emitNotification( + domain_name, event_name, std::move(value)); + } else { + UNREACHABLE("Unknown domain for emitNotificationFromBackend"); + } } void dispatchProtocolMessage(const StringView& message) { @@ -259,8 +280,8 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel, Utf8ToStringView(method)->string())) { session_->dispatchProtocolMessage(message); } else { - node_dispatcher_->dispatch(call_id, method, std::move(value), - raw_message); + node_dispatcher_->dispatch( + call_id, method, std::move(value), raw_message); } } @@ -335,6 +356,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_inspector_; std::unique_ptr delegate_; std::unique_ptr session_; std::unique_ptr node_dispatcher_; @@ -631,6 +653,12 @@ class NodeInspectorClient : public V8InspectorClient { return retaining_context; } + void emitNotification(const StringView& event, const StringView& params) { + for (const auto& id_channel : channels_) { + id_channel.second->emitNotificationFromBackend(event, params); + } + } + std::shared_ptr getThreadHandle() { if (!interface_) { interface_ = std::make_shared( @@ -844,6 +872,66 @@ std::unique_ptr Agent::ConnectToMainThread( prevent_shutdown); } +void Agent::EmitProtocolEvent(const StringView& event, + const StringView& params) { + if (!env()->options()->experimental_network_inspection) return; + client_->emitNotification(event, params); +} + +void Agent::SetupNetworkTracking(Local enable_function, + Local disable_function) { + parent_env_->set_inspector_enable_network_tracking(enable_function); + parent_env_->set_inspector_disable_network_tracking(disable_function); + if (pending_enable_network_tracking) { + pending_enable_network_tracking = false; + EnableNetworkTracking(); + } else if (pending_disable_network_tracking) { + pending_disable_network_tracking = false; + DisableNetworkTracking(); + } +} + +void Agent::EnableNetworkTracking() { + if (network_tracking_enabled_) { + return; + } + HandleScope scope(parent_env_->isolate()); + Local enable = parent_env_->inspector_enable_network_tracking(); + if (enable.IsEmpty()) { + pending_enable_network_tracking = true; + } else { + ToggleNetworkTracking(parent_env_->isolate(), enable); + network_tracking_enabled_ = true; + } +} + +void Agent::DisableNetworkTracking() { + if (!network_tracking_enabled_) { + return; + } + HandleScope scope(parent_env_->isolate()); + Local disable = parent_env_->inspector_disable_network_tracking(); + if (disable.IsEmpty()) { + pending_disable_network_tracking = true; + } else if (!client_->hasConnectedSessions()) { + ToggleNetworkTracking(parent_env_->isolate(), disable); + network_tracking_enabled_ = false; + } +} + +void Agent::ToggleNetworkTracking(Isolate* isolate, Local fn) { + if (!parent_env_->can_call_into_js()) return; + auto context = parent_env_->context(); + HandleScope scope(isolate); + CHECK(!fn.IsEmpty()); + v8::TryCatch try_catch(isolate); + USE(fn->Call(context, Undefined(isolate), 0, nullptr)); + if (try_catch.HasCaught() && !try_catch.HasTerminated()) { + PrintCaughtException(isolate, context, try_catch); + UNREACHABLE("Cannot toggle network tracking, please report this."); + } +} + 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 725275e43c7135..2ddffdb63ffee3 100644 --- a/src/inspector_agent.h +++ b/src/inspector_agent.h @@ -69,6 +69,14 @@ class Agent { void ReportUncaughtException(v8::Local error, v8::Local message); + void EmitProtocolEvent(const v8_inspector::StringView& event, + const v8_inspector::StringView& params); + + void SetupNetworkTracking(v8::Local enable_function, + v8::Local disable_function); + void EnableNetworkTracking(); + void DisableNetworkTracking(); + // Async stack traces instrumentation. void AsyncTaskScheduled(const v8_inspector::StringView& taskName, void* task, bool recurring); @@ -121,6 +129,7 @@ class Agent { private: void ToggleAsyncHook(v8::Isolate* isolate, v8::Local fn); + void ToggleNetworkTracking(v8::Isolate* isolate, v8::Local fn); node::Environment* parent_env_; // Encapsulates majority of the Inspector functionality @@ -139,6 +148,10 @@ class Agent { bool pending_enable_async_hook_ = false; bool pending_disable_async_hook_ = false; + + bool network_tracking_enabled_ = false; + bool pending_enable_network_tracking = false; + bool pending_disable_network_tracking = false; }; } // namespace inspector diff --git a/src/inspector_js_api.cc b/src/inspector_js_api.cc index 5700f8c5efc698..282575601545d1 100644 --- a/src/inspector_js_api.cc +++ b/src/inspector_js_api.cc @@ -270,6 +270,30 @@ static void RegisterAsyncHookWrapper(const FunctionCallbackInfo& args) { enable_function, disable_function); } +void EmitProtocolEvent(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args[0]->IsString()); + Local eventName = args[0].As(); + CHECK(args[1]->IsString()); + Local params = args[1].As(); + + env->inspector_agent()->EmitProtocolEvent( + ToProtocolString(env->isolate(), eventName)->string(), + ToProtocolString(env->isolate(), params)->string()); +} + +void SetupNetworkTracking(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + CHECK(args[0]->IsFunction()); + Local enable_function = args[0].As(); + CHECK(args[1]->IsFunction()); + Local disable_function = args[1].As(); + + env->inspector_agent()->SetupNetworkTracking(enable_function, + disable_function); +} + void IsEnabled(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); args.GetReturnValue().Set(env->inspector_agent()->IsListening()); @@ -354,6 +378,8 @@ void Initialize(Local target, Local unused, SetMethod(context, target, "registerAsyncHook", RegisterAsyncHookWrapper); SetMethodNoSideEffect(context, target, "isEnabled", IsEnabled); + SetMethod(context, target, "emitProtocolEvent", EmitProtocolEvent); + SetMethod(context, target, "setupNetworkTracking", SetupNetworkTracking); Local console_string = FIXED_ONE_BYTE_STRING(isolate, "console"); @@ -387,6 +413,8 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(RegisterAsyncHookWrapper); registry->Register(IsEnabled); + registry->Register(EmitProtocolEvent); + registry->Register(SetupNetworkTracking); registry->Register(JSBindingsConnection::New); registry->Register(JSBindingsConnection::Dispatch); diff --git a/src/node_options.cc b/src/node_options.cc index 8557cbac2ae1a3..18d0ec4fdd0405 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -607,6 +607,9 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { "Directory where the V8 profiles generated by --cpu-prof will be " "placed. Does not affect --prof.", &EnvironmentOptions::cpu_prof_dir); + AddOption("--experimental-network-inspection", + "experimental network inspection support", + &EnvironmentOptions::experimental_network_inspection); AddOption( "--heap-prof", "Start the V8 heap profiler on start up, and write the heap profile " diff --git a/src/node_options.h b/src/node_options.h index b8a2f6b6dd21d8..3ff665f807594a 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -163,6 +163,7 @@ class EnvironmentOptions : public Options { uint64_t cpu_prof_interval = kDefaultCpuProfInterval; std::string cpu_prof_name; bool cpu_prof = false; + bool experimental_network_inspection = false; std::string heap_prof_dir; std::string heap_prof_name; static const uint64_t kDefaultHeapProfInterval = 512 * 1024; diff --git a/test/parallel/test-inspector-emit-protocol-event.js b/test/parallel/test-inspector-emit-protocol-event.js new file mode 100644 index 00000000000000..1a4e622c78881b --- /dev/null +++ b/test/parallel/test-inspector-emit-protocol-event.js @@ -0,0 +1,85 @@ +// Flags: --inspect=0 --experimental-network-inspection +'use strict'; +const common = require('../common'); + +common.skipIfInspectorDisabled(); + +const inspector = require('node:inspector/promises'); +const assert = require('node:assert'); + +const EXPECTED_EVENTS = { + Network: [ + { + name: 'requestWillBeSent', + params: { + requestId: 'request-id-1', + request: { + url: 'https://nodejs.org/en', + method: 'GET' + }, + timestamp: 1000, + wallTime: 1000, + } + }, + { + name: 'responseReceived', + params: { + requestId: 'request-id-1', + timestamp: 1000, + } + }, + { + name: 'loadingFinished', + params: { + requestId: 'request-id-1', + timestamp: 1000, + } + }, + ] +}; + +// Check that all domains and events are present in the inspector object. +for (const [domain, events] of Object.entries(EXPECTED_EVENTS)) { + if (!(domain in inspector)) { + assert.fail(`Expected domain ${domain} to be present in inspector`); + } + const actualEventNames = Object.keys(inspector[domain]); + const expectedEventNames = events.map((event) => event.name); + assert.deepStrictEqual(actualEventNames, expectedEventNames, `Expected ${domain} to have events ${expectedEventNames}, but got ${actualEventNames}`); +} + +// Check that all events throw when called with a non-object argument. +for (const [domain, events] of Object.entries(EXPECTED_EVENTS)) { + for (const event of events) { + assert.throws(() => inspector[domain][event.name]('params'), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "params" argument must be of type object. Received type string (\'params\')' + }); + } +} + +const runAsyncTest = async () => { + const session = new inspector.Session(); + session.connect(); + + // Check that all events emit the expected parameters. + await session.post('Network.enable'); + for (const [domain, events] of Object.entries(EXPECTED_EVENTS)) { + for (const event of events) { + session.on(`${domain}.${event.name}`, common.mustCall(({ params }) => { + assert.deepStrictEqual(params, event.params); + })); + inspector[domain][event.name](event.params); + } + } + + // Check tht no events are emitted after disabling the domain. + await session.post('Network.disable'); + session.on('Network.requestWillBeSent', common.mustNotCall()); + inspector.Network.requestWillBeSent({}); +}; + +runAsyncTest().then(common.mustCall()).catch((e) => { + assert.fail(e); +}); diff --git a/test/parallel/test-inspector-network-domain.js b/test/parallel/test-inspector-network-domain.js new file mode 100644 index 00000000000000..1dc0d4f65a216e --- /dev/null +++ b/test/parallel/test-inspector-network-domain.js @@ -0,0 +1,120 @@ +// Flags: --inspect=0 --experimental-network-inspection +'use strict'; +const common = require('../common'); + +common.skipIfInspectorDisabled(); + +const assert = require('node:assert'); +const fixtures = require('../common/fixtures'); +const http = require('node:http'); +const https = require('node:https'); +const inspector = require('node:inspector/promises'); + +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.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.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 testNetworkInspection = async () => { + await testHttpGet(); + session.removeAllListeners(); + await testHttpsGet(); + session.removeAllListeners(); +}; + +httpServer.listen(0, () => { + httpsServer.listen(0, async () => { + try { + await session.post('Network.enable'); + await testNetworkInspection(); + await session.post('Network.disable'); + } catch (e) { + assert.fail(e); + } finally { + terminate(); + } + }); +});