diff --git a/doc/api/cli.md b/doc/api/cli.md index f8edcd28f54e5a..e423c51ae2cc1b 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1172,6 +1172,17 @@ added: Use this flag to enable [ShadowRealm][] support. +### `--experimental-storage-inspection` + + + +> Stability: 1.1 - Active Development + +Enable experimental support for storage inspection + ### `--experimental-test-coverage` + +* `params` {Object} + * `storageId` {Object} + * `securityOrigin` {string} + * `storageKey` {string} + * `isLocalStorage` {boolean} + * `key` {string} + * `newValue` {string} + +This feature is only available with the +`--experimental-storage-inspection` flag enabled. + +Broadcasts the `DOMStorage.domStorageItemAdded` event to connected frontends. +This event indicates that a new item has been added to the storage. + +### `inspector.DOMStorage.domStorageItemRemoved` + + + +* `params` {Object} + * `storageId` {Object} + * `securityOrigin` {string} + * `storageKey` {string} + * `isLocalStorage` {boolean} + * `key` {string} + +This feature is only available with the +`--experimental-storage-inspection` flag enabled. + +Broadcasts the `DOMStorage.domStorageItemRemoved` event to connected frontends. +This event indicates that an item has been removed from the storage. + +### `inspector.DOMStorage.domStorageItemUpdated` + + + +* `params` {Object} + * `storageId` {Object} + * `securityOrigin` {string} + * `storageKey` {string} + * `isLocalStorage` {boolean} + * `key` {string} + * `oldValue` {string} + * `newValue` {string} + +This feature is only available with the +`--experimental-storage-inspection` flag enabled. + +Broadcasts the `DOMStorage.domStorageItemUpdated` event to connected frontends. +This event indicates that a storage item has been updated. + +### `inspector.DOMStorage.domStorageItemsCleared` + + + +* `params` {Object} + * `storageId` {Object} + * `securityOrigin` {string} + * `storageKey` {string} + * `isLocalStorage` {boolean} + +This feature is only available with the +`--experimental-storage-inspection` flag enabled. + +Broadcasts the `DOMStorage.domStorageItemsCleared` event to connected +frontends. This event indicates that all items have been cleared from the +storage. + +### `inspector.DOMStorage.registerStorage` + + + +* `params` {Object} + * `isLocalStorage` {boolean} + * `storageMap` {Object} + +This feature is only available with the +`--experimental-storage-inspection` flag enabled. + ## Support of breakpoints The Chrome DevTools Protocol [`Debugger` domain][] allows an diff --git a/doc/node.1 b/doc/node.1 index bed0774b43a21a..48e31c11d15573 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -231,6 +231,10 @@ Enable the experimental QUIC support. . .It Fl -experimental-inspector-network-resource Enable experimental support for inspector network resources. + +.It Fl -experimental-storage-inspection +Enable experimental support for storage inspection. + . .It Fl -force-context-aware Disable loading native addons that are not context-aware. diff --git a/eslint.config.mjs b/eslint.config.mjs index fb6f9623587583..4b76072dde46cc 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -70,6 +70,7 @@ export default [ // Filtering tsc output files (i.e. if there a foo.ts, we ignore foo.js): (f, _, files) => f.endsWith('js') && files.includes(f.replace(/(\.[cm]?)js$/, '$1ts')), ), + '!test/fixtures/test-inspector-dom-storage.mjs', '!test/fixtures/test-runner', 'test/fixtures/test-runner/*', '!test/fixtures/test-runner/output', diff --git a/lib/inspector.js b/lib/inspector.js index a6b835bbc7d529..1c440794f3932f 100644 --- a/lib/inspector.js +++ b/lib/inspector.js @@ -229,6 +229,16 @@ const NetworkResources = { put, }; +const DOMStorage = { + domStorageItemAdded: (params) => broadcastToFrontend('DOMStorage.domStorageItemAdded', params), + domStorageItemRemoved: (params) => broadcastToFrontend('DOMStorage.domStorageItemRemoved', params), + domStorageItemUpdated: (params) => broadcastToFrontend('DOMStorage.domStorageItemUpdated', params), + domStorageItemsCleared: (params) => broadcastToFrontend('DOMStorage.domStorageItemsCleared', params), + // Pseudo-event: not part of the CDP DOMStorage domain. + // Call DOMStorageAgent::registerStorage in inspector/dom_storage_agent.cc. + registerStorage: (params) => broadcastToFrontend('DOMStorage.registerStorage', params), +}; + module.exports = { open: inspectorOpen, close: _debugEnd, @@ -238,4 +248,5 @@ module.exports = { Session, Network, NetworkResources, + DOMStorage, }; diff --git a/lib/internal/inspector/webstorage.js b/lib/internal/inspector/webstorage.js new file mode 100644 index 00000000000000..6cba87b236ed12 --- /dev/null +++ b/lib/internal/inspector/webstorage.js @@ -0,0 +1,113 @@ +'use strict'; + +const { Storage } = internalBinding('webstorage'); +const { DOMStorage } = require('inspector'); +const path = require('path'); +const { getOptionValue } = require('internal/options'); + +class InspectorLocalStorage extends Storage { + setItem(key, value) { + key = `${key}`; + value = `${value}`; + const oldValue = this.getItem(key); + super.setItem(key, value); + if (oldValue == null) { + itemAdded(key, value, true); + } else { + itemUpdated(key, oldValue, value, true); + } + } + + removeItem(key) { + key = `${key}`; + super.removeItem(key); + itemRemoved(key, true); + } + + clear() { + super.clear(); + itemsCleared(true); + } +} + +const InspectorSessionStorage = class extends Storage { + setItem(key, value) { + key = `${key}`; + value = `${value}`; + const oldValue = this.getItem(key); + super.setItem(key, value); + if (oldValue == null) { + itemAdded(key, value, false); + } else { + itemUpdated(key, oldValue, value, false); + } + } + + removeItem(key) { + key = `${key}`; + super.removeItem(key); + itemRemoved(key, false); + } + + clear() { + super.clear(); + itemsCleared(false); + } +}; + +function itemAdded(key, value, isLocalStorage) { + DOMStorage.domStorageItemAdded({ + key, + newValue: value, + storageId: { + securityOrigin: '', + isLocalStorage, + storageKey: getStorageKey(), + }, + }); +} + +function itemUpdated(key, oldValue, newValue, isLocalStorage) { + DOMStorage.domStorageItemUpdated({ + key, + oldValue, + newValue, + storageId: { + securityOrigin: '', + isLocalStorage, + storageKey: getStorageKey(), + }, + }); +} + +function itemRemoved(key, isLocalStorage) { + DOMStorage.domStorageItemRemoved({ + key, + storageId: { + securityOrigin: '', + isLocalStorage, + storageKey: getStorageKey(), + }, + }); +} + +function itemsCleared(isLocalStorage) { + DOMStorage.domStorageItemsCleared({ + storageId: { + securityOrigin: '', + isLocalStorage, + storageKey: getStorageKey(), + }, + }); +} + +function getStorageKey() { + const localStorageFile = getOptionValue('--localstorage-file'); + const resolvedAbsolutePath = path.resolve(localStorageFile); + return 'file://' + resolvedAbsolutePath; +} + +module.exports = { + InspectorLocalStorage, + InspectorSessionStorage, +}; diff --git a/lib/internal/webstorage.js b/lib/internal/webstorage.js index 7f58299a1e835f..6ea4585d0cc9ea 100644 --- a/lib/internal/webstorage.js +++ b/lib/internal/webstorage.js @@ -3,6 +3,7 @@ const { ObjectDefineProperties, } = primordials; const { ERR_INVALID_ARG_VALUE } = require('internal/errors').codes; +const { hasInspector } = internalBinding('config'); const { getOptionValue } = require('internal/options'); const { emitExperimentalWarning } = require('internal/util'); const { kConstructorKey, Storage } = internalBinding('webstorage'); @@ -15,6 +16,17 @@ module.exports = { Storage }; let lazyLocalStorage; let lazySessionStorage; +let lazyInspectorStorage; + +const experimentalStorageInspection = + hasInspector && getOptionValue('--experimental-storage-inspection'); + +function getInspectorStorage() { + if (lazyInspectorStorage === undefined) { + lazyInspectorStorage = require('internal/inspector/webstorage'); + } + return lazyInspectorStorage; +} ObjectDefineProperties(module.exports, { __proto__: null, @@ -32,7 +44,12 @@ ObjectDefineProperties(module.exports, { 'is an invalid localStorage location'); } - lazyLocalStorage = new Storage(kConstructorKey, getValidatedPath(location)); + if (experimentalStorageInspection) { + const { InspectorLocalStorage } = getInspectorStorage(); + lazyLocalStorage = new InspectorLocalStorage(kConstructorKey, getValidatedPath(location), true); + } else { + lazyLocalStorage = new Storage(kConstructorKey, getValidatedPath(location)); + } } return lazyLocalStorage; @@ -44,7 +61,12 @@ ObjectDefineProperties(module.exports, { enumerable: true, get() { if (lazySessionStorage === undefined) { - lazySessionStorage = new Storage(kConstructorKey, kInMemoryPath); + if (experimentalStorageInspection) { + const { InspectorSessionStorage } = getInspectorStorage(); + lazySessionStorage = new InspectorSessionStorage(kConstructorKey, kInMemoryPath, false); + } else { + lazySessionStorage = new Storage(kConstructorKey, kInMemoryPath); + } } return lazySessionStorage; diff --git a/src/inspector/dom_storage_agent.cc b/src/inspector/dom_storage_agent.cc new file mode 100644 index 00000000000000..3708d3b59975eb --- /dev/null +++ b/src/inspector/dom_storage_agent.cc @@ -0,0 +1,314 @@ +#include "dom_storage_agent.h" +#include +#include "env-inl.h" +#include "inspector/inspector_object_utils.h" +#include "util.h" +#include "v8-exception.h" +#include "v8-isolate.h" + +namespace node { +namespace inspector { + +using v8::Array; +using v8::Context; +using v8::HandleScope; +using v8::Isolate; +using v8::Local; +using v8::Object; +using v8::Value; + +static void ThrowEventError(v8::Isolate* isolate, const std::string& message) { + isolate->ThrowException(v8::Exception::TypeError( + v8::String::NewFromUtf8(isolate, message.c_str()).ToLocalChecked())); +} + +std::unique_ptr createStorageIdFromObject( + Local context, Local storage_id_obj) { + protocol::String security_origin; + Isolate* isolate = Isolate::GetCurrent(); + if (!ObjectGetProtocolString(context, storage_id_obj, "securityOrigin") + .To(&security_origin)) { + ThrowEventError(isolate, "Missing securityOrigin in storageId"); + return {}; + } + bool is_local_storage = + ObjectGetBool(context, storage_id_obj, "isLocalStorage").FromMaybe(false); + protocol::String storageKey; + if (!ObjectGetProtocolString(context, storage_id_obj, "storageKey") + .To(&storageKey)) { + ThrowEventError(isolate, "Missing storageKey in storageId"); + return {}; + } + + return protocol::DOMStorage::StorageId::create() + .setSecurityOrigin(security_origin) + .setIsLocalStorage(is_local_storage) + .setStorageKey(storageKey) + .build(); +} + +DOMStorageAgent::DOMStorageAgent(Environment* env) : env_(env) {} + +DOMStorageAgent::~DOMStorageAgent() {} + +void DOMStorageAgent::Wire(protocol::UberDispatcher* dispatcher) { + frontend_ = + std::make_unique(dispatcher->channel()); + protocol::DOMStorage::Dispatcher::wire(dispatcher, this); + addEventNotifier("domStorageItemAdded", + [this](v8::Local ctx, v8::Local p) { + this->domStorageItemAdded(ctx, p); + }); + addEventNotifier("domStorageItemRemoved", + [this](v8::Local ctx, v8::Local p) { + this->domStorageItemRemoved(ctx, p); + }); + addEventNotifier("domStorageItemUpdated", + [this](v8::Local ctx, v8::Local p) { + this->domStorageItemUpdated(ctx, p); + }); + addEventNotifier("domStorageItemsCleared", + [this](v8::Local ctx, v8::Local p) { + this->domStorageItemsCleared(ctx, p); + }); + addEventNotifier("registerStorage", + [this](v8::Local ctx, v8::Local p) { + this->registerStorage(ctx, p); + }); +} + +protocol::DispatchResponse DOMStorageAgent::enable() { + this->enabled_ = true; + return protocol::DispatchResponse::Success(); +} + +protocol::DispatchResponse DOMStorageAgent::disable() { + this->enabled_ = false; + return protocol::DispatchResponse::Success(); +} + +protocol::DispatchResponse DOMStorageAgent::getDOMStorageItems( + std::unique_ptr storageId, + std::unique_ptr>>* + items) { + if (!enabled_) { + return protocol::DispatchResponse::ServerError( + "DOMStorage domain is not enabled"); + } + bool is_local_storage = storageId->getIsLocalStorage(); + const StorageMap* storage_map = + is_local_storage ? &local_storage_map_ : &session_storage_map_; + std::optional storage_map_fallback; + if (storage_map->empty()) { + auto web_storage_obj = getWebStorage(is_local_storage); + if (web_storage_obj) { + storage_map_fallback = web_storage_obj.value()->GetAll(); + storage_map = &storage_map_fallback.value(); + } + } + + auto result = + std::make_unique>>(); + for (const auto& pair : *storage_map) { + auto item = std::make_unique>(); + item->push_back(protocol::StringUtil::fromUTF16( + reinterpret_cast(pair.first.data()), + pair.first.size())); + item->push_back(protocol::StringUtil::fromUTF16( + reinterpret_cast(pair.second.data()), + pair.second.size())); + result->push_back(std::move(item)); + } + *items = std::move(result); + return protocol::DispatchResponse::Success(); +} + +protocol::DispatchResponse DOMStorageAgent::setDOMStorageItem( + std::unique_ptr storageId, + const std::string& key, + const std::string& value) { + return protocol::DispatchResponse::ServerError("Not implemented"); +} + +protocol::DispatchResponse DOMStorageAgent::removeDOMStorageItem( + std::unique_ptr storageId, + const std::string& key) { + return protocol::DispatchResponse::ServerError("Not implemented"); +} + +protocol::DispatchResponse DOMStorageAgent::clear( + std::unique_ptr storageId) { + return protocol::DispatchResponse::ServerError("Not implemented"); +} + +void DOMStorageAgent::domStorageItemAdded(Local context, + Local params) { + Isolate* isolate = env_->isolate(); + Local storage_id_obj; + if (!ObjectGetObject(context, params, "storageId").ToLocal(&storage_id_obj)) { + ThrowEventError(isolate, "Missing storageId in event"); + return; + } + + std::unique_ptr storage_id = + createStorageIdFromObject(context, storage_id_obj); + if (!storage_id) { + return; + } + + protocol::String key; + if (!ObjectGetProtocolString(context, params, "key").To(&key)) { + ThrowEventError(isolate, "Missing key in event"); + return; + } + protocol::String new_value; + if (!ObjectGetProtocolString(context, params, "newValue").To(&new_value)) { + ThrowEventError(isolate, "Missing newValue in event"); + return; + } + frontend_->domStorageItemAdded(std::move(storage_id), key, new_value); +} + +void DOMStorageAgent::domStorageItemRemoved(Local context, + Local params) { + Isolate* isolate = env_->isolate(); + Local storage_id_obj; + if (!ObjectGetObject(context, params, "storageId").ToLocal(&storage_id_obj)) { + ThrowEventError(isolate, "Missing storageId in event"); + return; + } + std::unique_ptr storage_id = + createStorageIdFromObject(context, storage_id_obj); + + if (!storage_id) { + return; + } + + protocol::String key; + if (!ObjectGetProtocolString(context, params, "key").To(&key)) { + ThrowEventError(isolate, "Missing key in event"); + return; + } + frontend_->domStorageItemRemoved(std::move(storage_id), key); +} + +void DOMStorageAgent::domStorageItemUpdated(Local context, + Local params) { + Isolate* isolate = env_->isolate(); + Local storage_id_obj; + if (!ObjectGetObject(context, params, "storageId").ToLocal(&storage_id_obj)) { + ThrowEventError(isolate, "Missing storageId in event"); + return; + } + + std::unique_ptr storage_id = + createStorageIdFromObject(context, storage_id_obj); + + if (!storage_id) { + return; + } + + protocol::String key; + if (!ObjectGetProtocolString(context, params, "key").To(&key)) { + ThrowEventError(isolate, "Missing key in event"); + return; + } + protocol::String old_value; + if (!ObjectGetProtocolString(context, params, "oldValue").To(&old_value)) { + ThrowEventError(isolate, "Missing oldValue in event"); + return; + } + protocol::String new_value; + if (!ObjectGetProtocolString(context, params, "newValue").To(&new_value)) { + ThrowEventError(isolate, "Missing newValue in event"); + return; + } + frontend_->domStorageItemUpdated( + std::move(storage_id), key, old_value, new_value); +} + +void DOMStorageAgent::domStorageItemsCleared(Local context, + Local params) { + Isolate* isolate = env_->isolate(); + Local storage_id_obj; + if (!ObjectGetObject(context, params, "storageId").ToLocal(&storage_id_obj)) { + ThrowEventError(isolate, "Missing storageId in event"); + return; + } + std::unique_ptr storage_id = + createStorageIdFromObject(context, storage_id_obj); + + if (!storage_id) { + return; + } + frontend_->domStorageItemsCleared(std::move(storage_id)); +} + +void DOMStorageAgent::registerStorage(Local context, + Local params) { + Isolate* isolate = env_->isolate(); + HandleScope handle_scope(isolate); + bool is_local_storage; + if (!ObjectGetBool(context, params, "isLocalStorage").To(&is_local_storage)) { + ThrowEventError(isolate, "Missing isLocalStorage in event"); + return; + } + Local storage_map_obj; + if (!ObjectGetObject(context, params, "storageMap") + .ToLocal(&storage_map_obj)) { + ThrowEventError(isolate, "Missing storageMap in event"); + return; + } + StorageMap& storage_map = + is_local_storage ? local_storage_map_ : session_storage_map_; + Local property_names; + if (!storage_map_obj->GetOwnPropertyNames(context).ToLocal(&property_names)) { + ThrowEventError(isolate, "Failed to get property names from storageMap"); + return; + } + uint32_t length = property_names->Length(); + for (uint32_t i = 0; i < length; ++i) { + Local key_value; + if (!property_names->Get(context, i).ToLocal(&key_value)) { + ThrowEventError(isolate, "Failed to get key from storageMap"); + return; + } + Local value_value; + if (!storage_map_obj->Get(context, key_value).ToLocal(&value_value)) { + ThrowEventError(isolate, "Failed to get value from storageMap"); + return; + } + node::TwoByteValue key_utf16(isolate, key_value); + node::TwoByteValue value_utf16(isolate, value_value); + storage_map[key_utf16.ToU16String()] = value_utf16.ToU16String(); + } +} + +std::optional DOMStorageAgent::getWebStorage( + bool is_local_storage) { + v8::Isolate* isolate = env_->isolate(); + v8::HandleScope handle_scope(isolate); + v8::Local global = env_->context()->Global(); + v8::Local web_storage_val; + v8::TryCatch try_catch(isolate); + if (!global + ->Get(env_->context(), + is_local_storage + ? FIXED_ONE_BYTE_STRING(isolate, "localStorage") + : FIXED_ONE_BYTE_STRING(isolate, "sessionStorage")) + .ToLocal(&web_storage_val) || + !web_storage_val->IsObject() || try_catch.HasCaught()) { + return std::nullopt; + } else { + node::webstorage::Storage* storage; + ASSIGN_OR_RETURN_UNWRAP( + &storage, web_storage_val.As(), std::nullopt); + return storage; + } +} + +bool DOMStorageAgent::canEmit(const std::string& domain) { + return domain == "DOMStorage"; +} +} // namespace inspector +} // namespace node diff --git a/src/inspector/dom_storage_agent.h b/src/inspector/dom_storage_agent.h new file mode 100644 index 00000000000000..a6eecbb8d37c95 --- /dev/null +++ b/src/inspector/dom_storage_agent.h @@ -0,0 +1,68 @@ +#ifndef SRC_INSPECTOR_DOM_STORAGE_AGENT_H_ +#define SRC_INSPECTOR_DOM_STORAGE_AGENT_H_ + +#include +#include +#include "env.h" +#include "node/inspector/protocol/DOMStorage.h" +#include "node_webstorage.h" +#include "notification_emitter.h" +#include "v8.h" + +namespace node { +namespace inspector { + +class DOMStorageAgent : public protocol::DOMStorage::Backend, + public NotificationEmitter { + public: + explicit DOMStorageAgent(Environment* env); + ~DOMStorageAgent() override; + + void Wire(protocol::UberDispatcher* dispatcher); + + protocol::DispatchResponse enable() override; + protocol::DispatchResponse disable() override; + protocol::DispatchResponse getDOMStorageItems( + std::unique_ptr storageId, + std::unique_ptr>>* + items) override; + protocol::DispatchResponse setDOMStorageItem( + std::unique_ptr storageId, + const std::string& key, + const std::string& value) override; + protocol::DispatchResponse removeDOMStorageItem( + std::unique_ptr storageId, + const std::string& key) override; + protocol::DispatchResponse clear( + std::unique_ptr storageId) override; + + void domStorageItemAdded(v8::Local context, + v8::Local params); + void domStorageItemRemoved(v8::Local context, + v8::Local params); + void domStorageItemUpdated(v8::Local context, + v8::Local params); + void domStorageItemsCleared(v8::Local context, + v8::Local params); + void registerStorage(v8::Local context, + v8::Local params); + bool canEmit(const std::string& domain) override; + + DOMStorageAgent(const DOMStorageAgent&) = delete; + DOMStorageAgent& operator=(const DOMStorageAgent&) = delete; + + private: + typedef std::unordered_map StorageMap; + std::optional getWebStorage( + bool is_local_storage); + std::unique_ptr frontend_; + StorageMap local_storage_map_ = {}; + StorageMap session_storage_map_ = {}; + bool enabled_ = false; + Environment* env_; +}; + +} // namespace inspector +} // namespace node + +#endif // SRC_INSPECTOR_DOM_STORAGE_AGENT_H_ diff --git a/src/inspector/domain_dom_storage.pdl b/src/inspector/domain_dom_storage.pdl new file mode 100644 index 00000000000000..64993819533d9c --- /dev/null +++ b/src/inspector/domain_dom_storage.pdl @@ -0,0 +1,67 @@ +experimental domain DOMStorage + + type SerializedStorageKey extends string + + # DOM Storage identifier. + type StorageId extends object + properties + # Security origin for the storage. + optional string securityOrigin + # Represents a key by which DOM Storage keys its CachedStorageAreas + optional SerializedStorageKey storageKey + # Whether the storage is local storage (not session storage). + boolean isLocalStorage + + # DOM Storage item. + type Item extends array of string + + command clear + parameters + StorageId storageId + + # Disables storage tracking, prevents storage events from being sent to the client. + command disable + + # Enables storage tracking, storage events will now be delivered to the client. + command enable + + command getDOMStorageItems + parameters + StorageId storageId + returns + array of Item entries + + command removeDOMStorageItem + parameters + StorageId storageId + string key + + command setDOMStorageItem + parameters + StorageId storageId + string key + string value + + event domStorageItemAdded + parameters + StorageId storageId + string key + string newValue + + event domStorageItemRemoved + parameters + StorageId storageId + string key + + event domStorageItemUpdated + parameters + StorageId storageId + string key + string oldValue + string newValue + + event domStorageItemsCleared + parameters + StorageId storageId + + diff --git a/src/inspector/domain_storage.pdl b/src/inspector/domain_storage.pdl new file mode 100644 index 00000000000000..837def94db6e70 --- /dev/null +++ b/src/inspector/domain_storage.pdl @@ -0,0 +1,7 @@ +experimental domain Storage + type SerializedStorageKey extends string + experimental command getStorageKey + parameters + optional string frameId + returns + SerializedStorageKey storageKey diff --git a/src/inspector/inspector_object_utils.cc b/src/inspector/inspector_object_utils.cc new file mode 100644 index 00000000000000..8acfcbbd5c1e38 --- /dev/null +++ b/src/inspector/inspector_object_utils.cc @@ -0,0 +1,102 @@ +#include "inspector_object_utils.h" +#include "inspector/protocol_helper.h" +#include "util-inl.h" + +using v8::Boolean; +using v8::Context; +using v8::EscapableHandleScope; +using v8::HandleScope; +using v8::Int32; +using v8::Isolate; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::MaybeLocal; +using v8::Nothing; +using v8::Number; +using v8::Object; +using v8::String; +using v8::Value; + +namespace node { +namespace inspector { +// Get a protocol string property from the object. +Maybe ObjectGetProtocolString(Local context, + Local object, + Local property) { + HandleScope handle_scope(Isolate::GetCurrent()); + Local value; + if (!object->Get(context, property).ToLocal(&value) || !value->IsString()) { + return Nothing(); + } + Local str = value.As(); + return Just(ToProtocolString(Isolate::GetCurrent(), str)); +} + +// Get a protocol string property from the object. +Maybe ObjectGetProtocolString(Local context, + Local object, + const char* property) { + HandleScope handle_scope(Isolate::GetCurrent()); + return ObjectGetProtocolString( + context, object, OneByteString(Isolate::GetCurrent(), property)); +} + +// Get a protocol double property from the object. +Maybe ObjectGetDouble(Local context, + Local object, + const char* property) { + HandleScope handle_scope(Isolate::GetCurrent()); + Local value; + if (!object->Get(context, OneByteString(Isolate::GetCurrent(), property)) + .ToLocal(&value) || + !value->IsNumber()) { + return Nothing(); + } + return Just(value.As()->Value()); +} + +// Get a protocol int property from the object. +Maybe ObjectGetInt(Local context, + Local object, + const char* property) { + HandleScope handle_scope(Isolate::GetCurrent()); + Local value; + if (!object->Get(context, OneByteString(Isolate::GetCurrent(), property)) + .ToLocal(&value) || + !value->IsInt32()) { + return Nothing(); + } + return Just(value.As()->Value()); +} + +// Get a protocol bool property from the object. +Maybe ObjectGetBool(Local context, + Local object, + const char* property) { + HandleScope handle_scope(Isolate::GetCurrent()); + Local value; + if (!object->Get(context, OneByteString(Isolate::GetCurrent(), property)) + .ToLocal(&value) || + !value->IsBoolean()) { + return Nothing(); + } + return Just(value.As()->Value()); +} + +// Get an object property from the object. +MaybeLocal ObjectGetObject(Local context, + Local object, + const char* property) { + EscapableHandleScope handle_scope(Isolate::GetCurrent()); + Local value; + if (!object->Get(context, OneByteString(Isolate::GetCurrent(), property)) + .ToLocal(&value) || + !value->IsObject()) { + return {}; + } + return handle_scope.Escape(value.As()); +} + +} // namespace inspector +} // namespace node diff --git a/src/inspector/inspector_object_utils.h b/src/inspector/inspector_object_utils.h new file mode 100644 index 00000000000000..f69933a8bb55c4 --- /dev/null +++ b/src/inspector/inspector_object_utils.h @@ -0,0 +1,38 @@ +#ifndef SRC_INSPECTOR_INSPECTOR_OBJECT_UTILS_H_ +#define SRC_INSPECTOR_INSPECTOR_OBJECT_UTILS_H_ + +#include +#include "node/inspector/protocol/Protocol.h" + +namespace node { +namespace inspector { + +v8::Maybe ObjectGetProtocolString( + v8::Local context, + v8::Local object, + v8::Local property); + +v8::Maybe ObjectGetProtocolString( + v8::Local context, + v8::Local object, + const char* property); + +v8::Maybe ObjectGetDouble(v8::Local context, + v8::Local object, + const char* property); + +v8::Maybe ObjectGetInt(v8::Local context, + v8::Local object, + const char* property); + +v8::Maybe ObjectGetBool(v8::Local context, + v8::Local object, + const char* property); + +v8::MaybeLocal ObjectGetObject(v8::Local context, + v8::Local object, + const char* property); + +} // namespace inspector +} // namespace node +#endif // SRC_INSPECTOR_INSPECTOR_OBJECT_UTILS_H_ diff --git a/src/inspector/network_agent.cc b/src/inspector/network_agent.cc index aacd4e8cb83c88..a19c9ec16375ce 100644 --- a/src/inspector/network_agent.cc +++ b/src/inspector/network_agent.cc @@ -2,6 +2,7 @@ #include #include "debug_utils-inl.h" #include "env-inl.h" +#include "inspector/inspector_object_utils.h" #include "inspector/network_resource_manager.h" #include "inspector/protocol_helper.h" #include "network_inspector.h" @@ -14,106 +15,32 @@ namespace node { namespace inspector { -using v8::EscapableHandleScope; using v8::HandleScope; -using v8::Just; +using v8::Isolate; using v8::Local; -using v8::Maybe; -using v8::MaybeLocal; -using v8::Nothing; using v8::Object; using v8::Uint8Array; using v8::Value; constexpr size_t kDefaultMaxTotalBufferSize = 100 * 1024 * 1024; // 100MB -// Get a protocol string property from the object. -Maybe ObjectGetProtocolString(v8::Local context, - Local object, - Local property) { - HandleScope handle_scope(context->GetIsolate()); - Local value; - if (!object->Get(context, property).ToLocal(&value) || !value->IsString()) { - return Nothing(); - } - Local str = value.As(); - return Just(ToProtocolString(context->GetIsolate(), str)); -} - -// Get a protocol string property from the object. -Maybe ObjectGetProtocolString(v8::Local context, - Local object, - const char* property) { - HandleScope handle_scope(context->GetIsolate()); - return ObjectGetProtocolString( - context, object, OneByteString(context->GetIsolate(), property)); -} - -// Get a protocol double property from the object. -Maybe ObjectGetDouble(v8::Local context, - Local object, - const char* property) { - HandleScope handle_scope(context->GetIsolate()); - Local value; - if (!object->Get(context, OneByteString(context->GetIsolate(), property)) - .ToLocal(&value) || - !value->IsNumber()) { - return Nothing(); - } - return Just(value.As()->Value()); -} - -// Get a protocol int property from the object. -Maybe ObjectGetInt(v8::Local context, - Local object, - const char* property) { - HandleScope handle_scope(context->GetIsolate()); - Local value; - if (!object->Get(context, OneByteString(context->GetIsolate(), property)) - .ToLocal(&value) || - !value->IsInt32()) { - return Nothing(); - } - return Just(value.As()->Value()); -} - -// Get a protocol bool property from the object. -Maybe ObjectGetBool(v8::Local context, - Local object, - const char* property) { - HandleScope handle_scope(context->GetIsolate()); - Local value; - if (!object->Get(context, OneByteString(context->GetIsolate(), property)) - .ToLocal(&value) || - !value->IsBoolean()) { - return Nothing(); - } - return Just(value.As()->Value()); -} - -// Get an object property from the object. -MaybeLocal ObjectGetObject(v8::Local context, - Local object, - const char* property) { - EscapableHandleScope handle_scope(context->GetIsolate()); - Local value; - if (!object->Get(context, OneByteString(context->GetIsolate(), property)) - .ToLocal(&value) || - !value->IsObject()) { - return {}; - } - return handle_scope.Escape(value.As()); +static void ThrowEventError(v8::Isolate* isolate, const std::string& message) { + isolate->ThrowException(v8::Exception::TypeError( + v8::String::NewFromUtf8(isolate, message.c_str()).ToLocalChecked())); } // Create a protocol::Network::Headers from the v8 object. -std::unique_ptr createHeadersFromObject( - v8::Local context, Local headers_obj) { +std::unique_ptr +NetworkAgent::createHeadersFromObject(v8::Local context, + Local headers_obj) { HandleScope handle_scope(context->GetIsolate()); + Isolate* isolate = env_->isolate(); std::unique_ptr dict = protocol::DictionaryValue::create(); Local property_names; if (!headers_obj->GetOwnPropertyNames(context).ToLocal(&property_names)) { + ThrowEventError(isolate, "Missing response headers in event"); return {}; } @@ -121,12 +48,14 @@ std::unique_ptr createHeadersFromObject( Local property_name_val; if (!property_names->Get(context, idx).ToLocal(&property_name_val) || !property_name_val->IsString()) { + ThrowEventError(isolate, "Invalid header name in event"); return {}; } Local property_name = property_name_val.As(); protocol::String property_value; if (!ObjectGetProtocolString(context, headers_obj, property_name) .To(&property_value)) { + ThrowEventError(isolate, "Invalid header value in event"); return {}; } dict->setString(ToProtocolString(context->GetIsolate(), property_name), @@ -137,19 +66,24 @@ std::unique_ptr createHeadersFromObject( } // Create a protocol::Network::Request from the v8 object. -std::unique_ptr createRequestFromObject( - v8::Local context, Local request) { +std::unique_ptr +NetworkAgent::createRequestFromObject(v8::Local context, + Local request) { HandleScope handle_scope(context->GetIsolate()); + Isolate* isolate = env_->isolate(); protocol::String url; if (!ObjectGetProtocolString(context, request, "url").To(&url)) { + ThrowEventError(isolate, "Missing request.url in event"); return {}; } protocol::String method; if (!ObjectGetProtocolString(context, request, "method").To(&method)) { + ThrowEventError(isolate, "Missing request.method in event"); return {}; } Local headers_obj; if (!ObjectGetObject(context, request, "headers").ToLocal(&headers_obj)) { + ThrowEventError(isolate, "Missing request.headers in event"); return {}; } std::unique_ptr headers = @@ -169,24 +103,30 @@ std::unique_ptr createRequestFromObject( } // Create a protocol::Network::Response from the v8 object. -std::unique_ptr createResponseFromObject( - v8::Local context, Local response) { +std::unique_ptr +NetworkAgent::createResponseFromObject(v8::Local context, + Local response) { HandleScope handle_scope(context->GetIsolate()); + Isolate* isolate = env_->isolate(); protocol::String url; if (!ObjectGetProtocolString(context, response, "url").To(&url)) { + ThrowEventError(isolate, "Missing response.url in event"); return {}; } int status; if (!ObjectGetInt(context, response, "status").To(&status)) { + ThrowEventError(isolate, "Missing response.status in event"); return {}; } protocol::String statusText; if (!ObjectGetProtocolString(context, response, "statusText") .To(&statusText)) { + ThrowEventError(isolate, "Missing response.statusText in event"); return {}; } Local headers_obj; if (!ObjectGetObject(context, response, "headers").ToLocal(&headers_obj)) { + ThrowEventError(isolate, "Missing response.headers in event"); return {}; } std::unique_ptr headers = @@ -210,20 +150,25 @@ std::unique_ptr createResponseFromObject( .build(); } -std::unique_ptr createWebSocketResponse( - v8::Local context, Local response) { +std::unique_ptr +NetworkAgent::createWebSocketResponse(v8::Local context, + Local response) { HandleScope handle_scope(context->GetIsolate()); + Isolate* isolate = env_->isolate(); int status; if (!ObjectGetInt(context, response, "status").To(&status)) { + ThrowEventError(isolate, "Missing response.status in event"); return {}; } protocol::String statusText; if (!ObjectGetProtocolString(context, response, "statusText") .To(&statusText)) { + ThrowEventError(isolate, "Missing response.statusText in event"); return {}; } Local headers_obj; if (!ObjectGetObject(context, response, "headers").ToLocal(&headers_obj)) { + ThrowEventError(isolate, "Missing response.headers in event"); return {}; } std::unique_ptr headers = @@ -263,12 +208,15 @@ NetworkAgent::NetworkAgent( void NetworkAgent::webSocketCreated(v8::Local context, v8::Local params) { + Isolate* isolate = env_->isolate(); protocol::String request_id; if (!ObjectGetProtocolString(context, params, "requestId").To(&request_id)) { + ThrowEventError(isolate, "Missing requestId in event"); return; } protocol::String url; if (!ObjectGetProtocolString(context, params, "url").To(&url)) { + ThrowEventError(isolate, "Missing url in event"); return; } std::unique_ptr initiator = @@ -282,12 +230,15 @@ void NetworkAgent::webSocketCreated(v8::Local context, void NetworkAgent::webSocketClosed(v8::Local context, v8::Local params) { + Isolate* isolate = env_->isolate(); protocol::String request_id; if (!ObjectGetProtocolString(context, params, "requestId").To(&request_id)) { + ThrowEventError(isolate, "Missing requestId in event"); return; } double timestamp; if (!ObjectGetDouble(context, params, "timestamp").To(×tamp)) { + ThrowEventError(isolate, "Missing timestamp in event"); return; } frontend_->webSocketClosed(request_id, timestamp); @@ -295,16 +246,20 @@ void NetworkAgent::webSocketClosed(v8::Local context, void NetworkAgent::webSocketHandshakeResponseReceived( v8::Local context, v8::Local params) { + Isolate* isolate = env_->isolate(); protocol::String request_id; if (!ObjectGetProtocolString(context, params, "requestId").To(&request_id)) { + ThrowEventError(isolate, "Missing requestId in event"); return; } double timestamp; if (!ObjectGetDouble(context, params, "timestamp").To(×tamp)) { + ThrowEventError(isolate, "Missing timestamp in event"); return; } Local response_obj; if (!ObjectGetObject(context, params, "response").ToLocal(&response_obj)) { + ThrowEventError(isolate, "Missing response in event"); return; } auto response = createWebSocketResponse(context, response_obj); @@ -476,22 +431,27 @@ protocol::DispatchResponse NetworkAgent::loadNetworkResource( void NetworkAgent::requestWillBeSent(v8::Local context, v8::Local params) { + Isolate* isolate = env_->isolate(); protocol::String request_id; if (!ObjectGetProtocolString(context, params, "requestId").To(&request_id)) { + ThrowEventError(isolate, "Missing requestId in event"); return; } double timestamp; if (!ObjectGetDouble(context, params, "timestamp").To(×tamp)) { + ThrowEventError(isolate, "Missing timestamp in event"); return; } double wall_time; if (!ObjectGetDouble(context, params, "wallTime").To(&wall_time)) { + ThrowEventError(isolate, "Missing wallTime in event"); return; } protocol::String charset = ObjectGetProtocolString(context, params, "charset").FromMaybe(""); Local request_obj; if (!ObjectGetObject(context, params, "request").ToLocal(&request_obj)) { + ThrowEventError(isolate, "Missing request in event"); return; } std::unique_ptr request = @@ -527,20 +487,25 @@ void NetworkAgent::requestWillBeSent(v8::Local context, void NetworkAgent::responseReceived(v8::Local context, v8::Local params) { + Isolate* isolate = env_->isolate(); protocol::String request_id; if (!ObjectGetProtocolString(context, params, "requestId").To(&request_id)) { + ThrowEventError(isolate, "Missing requestId in event"); return; } double timestamp; if (!ObjectGetDouble(context, params, "timestamp").To(×tamp)) { + ThrowEventError(isolate, "Missing timestamp in event"); return; } protocol::String type; if (!ObjectGetProtocolString(context, params, "type").To(&type)) { + ThrowEventError(isolate, "Missing type in event"); return; } Local response_obj; if (!ObjectGetObject(context, params, "response").ToLocal(&response_obj)) { + ThrowEventError(isolate, "Missing response in event"); return; } auto response = createResponseFromObject(context, response_obj); @@ -560,20 +525,25 @@ void NetworkAgent::responseReceived(v8::Local context, void NetworkAgent::loadingFailed(v8::Local context, v8::Local params) { + Isolate* isolate = env_->isolate(); protocol::String request_id; if (!ObjectGetProtocolString(context, params, "requestId").To(&request_id)) { + ThrowEventError(isolate, "Missing requestId in event"); return; } double timestamp; if (!ObjectGetDouble(context, params, "timestamp").To(×tamp)) { + ThrowEventError(isolate, "Missing timestamp in event"); return; } protocol::String type; if (!ObjectGetProtocolString(context, params, "type").To(&type)) { + ThrowEventError(isolate, "Missing type in event"); return; } protocol::String error_text; if (!ObjectGetProtocolString(context, params, "errorText").To(&error_text)) { + ThrowEventError(isolate, "Missing errorText in event"); return; } @@ -584,12 +554,15 @@ void NetworkAgent::loadingFailed(v8::Local context, void NetworkAgent::loadingFinished(v8::Local context, Local params) { + Isolate* isolate = env_->isolate(); protocol::String request_id; if (!ObjectGetProtocolString(context, params, "requestId").To(&request_id)) { + ThrowEventError(isolate, "Missing requestId in event"); return; } double timestamp; if (!ObjectGetDouble(context, params, "timestamp").To(×tamp)) { + ThrowEventError(isolate, "Missing timestamp in event"); return; } @@ -611,8 +584,10 @@ void NetworkAgent::loadingFinished(v8::Local context, void NetworkAgent::dataSent(v8::Local context, v8::Local params) { + Isolate* isolate = env_->isolate(); protocol::String request_id; if (!ObjectGetProtocolString(context, params, "requestId").To(&request_id)) { + ThrowEventError(isolate, "Missing requestId in event"); return; } @@ -631,17 +606,21 @@ void NetworkAgent::dataSent(v8::Local context, double timestamp; if (!ObjectGetDouble(context, params, "timestamp").To(×tamp)) { + ThrowEventError(isolate, "Missing timestamp in event"); return; } int data_length; if (!ObjectGetInt(context, params, "dataLength").To(&data_length)) { + ThrowEventError(isolate, "Missing dataLength in event"); return; } Local data_obj; if (!ObjectGetObject(context, params, "data").ToLocal(&data_obj)) { + ThrowEventError(isolate, "Missing data in event"); return; } if (!data_obj->IsUint8Array()) { + ThrowEventError(isolate, "Expected data to be Uint8Array in event"); return; } Local data = data_obj.As(); @@ -651,28 +630,35 @@ void NetworkAgent::dataSent(v8::Local context, void NetworkAgent::dataReceived(v8::Local context, v8::Local params) { + Isolate* isolate = env_->isolate(); protocol::String request_id; if (!ObjectGetProtocolString(context, params, "requestId").To(&request_id)) { + ThrowEventError(isolate, "Missing requestId in event"); return; } double timestamp; if (!ObjectGetDouble(context, params, "timestamp").To(×tamp)) { + ThrowEventError(isolate, "Missing timestamp in event"); return; } int data_length; if (!ObjectGetInt(context, params, "dataLength").To(&data_length)) { + ThrowEventError(isolate, "Missing dataLength in event"); return; } int encoded_data_length; if (!ObjectGetInt(context, params, "encodedDataLength") .To(&encoded_data_length)) { + ThrowEventError(isolate, "Missing encodedDataLength in event"); return; } Local data_obj; if (!ObjectGetObject(context, params, "data").ToLocal(&data_obj)) { + ThrowEventError(isolate, "Missing data in event"); return; } if (!data_obj->IsUint8Array()) { + ThrowEventError(isolate, "Expected data to be Uint8Array in event"); return; } Local data = data_obj.As(); diff --git a/src/inspector/network_agent.h b/src/inspector/network_agent.h index 7a5d545cb8d499..2136a45baf45f6 100644 --- a/src/inspector/network_agent.h +++ b/src/inspector/network_agent.h @@ -79,6 +79,15 @@ class NetworkAgent : public protocol::Network::Backend { v8::Local params); private: + std::unique_ptr createHeadersFromObject( + v8::Local context, v8::Local headers_obj); + std::unique_ptr createRequestFromObject( + v8::Local context, v8::Local request); + std::unique_ptr createResponseFromObject( + v8::Local context, v8::Local response); + std::unique_ptr createWebSocketResponse( + v8::Local context, v8::Local response); + NetworkInspector* inspector_; v8_inspector::V8Inspector* v8_inspector_; std::shared_ptr frontend_; diff --git a/src/inspector/node_inspector.gypi b/src/inspector/node_inspector.gypi index 5b96bc77733059..a493f59465d207 100644 --- a/src/inspector/node_inspector.gypi +++ b/src/inspector/node_inspector.gypi @@ -42,6 +42,14 @@ 'src/inspector/io_agent.h', 'src/inspector/network_resource_manager.cc', 'src/inspector/network_resource_manager.h', + 'src/inspector/dom_storage_agent.cc', + 'src/inspector/dom_storage_agent.h', + 'src/inspector/inspector_object_utils.cc', + 'src/inspector/inspector_object_utils.h', + 'src/inspector/storage_agent.h', + 'src/inspector/storage_agent.cc', + 'src/inspector/notification_emitter.h', + 'src/inspector/notification_emitter.cc', ], 'node_inspector_generated_sources': [ '<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/Forward.h', @@ -59,6 +67,10 @@ '<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/Target.h', '<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/IO.h', '<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/IO.cpp', + '<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/DOMStorage.h', + '<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/DOMStorage.cpp', + '<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/Storage.cpp', + '<(SHARED_INTERMEDIATE_DIR)/src/node/inspector/protocol/Storage.h', ], 'node_protocol_files': [ '<(protocol_tool_path)/lib/Forward_h.template', @@ -82,6 +94,8 @@ 'domain_node_tracing.pdl', 'domain_node_worker.pdl', 'domain_target.pdl', + 'domain_dom_storage.pdl', + 'domain_storage.pdl', ], }, 'defines': [ diff --git a/src/inspector/node_protocol.pdl b/src/inspector/node_protocol.pdl index 87b3d93d39c245..8cd8f5efd93d7f 100644 --- a/src/inspector/node_protocol.pdl +++ b/src/inspector/node_protocol.pdl @@ -9,3 +9,5 @@ include domain_node_runtime.pdl include domain_node_tracing.pdl include domain_node_worker.pdl include domain_target.pdl +include domain_dom_storage.pdl +include domain_storage.pdl diff --git a/src/inspector/notification_emitter.cc b/src/inspector/notification_emitter.cc new file mode 100644 index 00000000000000..c47226c7c5d226 --- /dev/null +++ b/src/inspector/notification_emitter.cc @@ -0,0 +1,24 @@ +#include "notification_emitter.h" +#include "v8-inspector.h" +#include "v8.h" + +namespace node { +namespace inspector { + +NotificationEmitter::NotificationEmitter() {} + +void NotificationEmitter::emitNotification(v8::Local context, + const EventKey& event, + v8::Local params) { + auto it = event_notifier_map_.find(event); + if (it != event_notifier_map_.end() && it->second) { + it->second(context, params); + } +} + +void NotificationEmitter::addEventNotifier(const EventKey& event, + EventNotifier notifier) { + event_notifier_map_[event] = notifier; +} +} // namespace inspector +} // namespace node diff --git a/src/inspector/notification_emitter.h b/src/inspector/notification_emitter.h new file mode 100644 index 00000000000000..65f493234c36e5 --- /dev/null +++ b/src/inspector/notification_emitter.h @@ -0,0 +1,38 @@ +#ifndef SRC_INSPECTOR_NOTIFICATION_EMITTER_H_ +#define SRC_INSPECTOR_NOTIFICATION_EMITTER_H_ + +#include +#include +#include "v8.h" + +namespace node { +namespace inspector { + +class NotificationEmitter { + public: + using EventKey = std::string; + using EventNotifier = + std::function, v8::Local)>; + + NotificationEmitter(); + virtual ~NotificationEmitter() = default; + + void emitNotification(v8::Local context, + const EventKey& event, + v8::Local params); + virtual bool canEmit(const std::string& domain) = 0; + + NotificationEmitter(const NotificationEmitter&) = delete; + NotificationEmitter& operator=(const NotificationEmitter&) = delete; + + protected: + void addEventNotifier(const EventKey& event, EventNotifier notifier); + + private: + std::unordered_map event_notifier_map_ = {}; +}; + +} // namespace inspector +} // namespace node + +#endif // SRC_INSPECTOR_NOTIFICATION_EMITTER_H_ diff --git a/src/inspector/storage_agent.cc b/src/inspector/storage_agent.cc new file mode 100644 index 00000000000000..62fc2d9e858d0f --- /dev/null +++ b/src/inspector/storage_agent.cc @@ -0,0 +1,32 @@ +#include "inspector/storage_agent.h" +#include +#include "env-inl.h" +#include "node_url.h" + +namespace node { +namespace inspector { +namespace protocol { +StorageAgent::StorageAgent(Environment* env) : env_(env) {} +StorageAgent::~StorageAgent() {} + +void StorageAgent::Wire(protocol::UberDispatcher* dispatcher) { + frontend_ = + std::make_unique(dispatcher->channel()); + protocol::Storage::Dispatcher::wire(dispatcher, this); +} +DispatchResponse StorageAgent::getStorageKey( + std::optional frameId, protocol::String* storageKey) { + auto local_storage_file = env_->options()->localstorage_file; + *storageKey = node::url::FromFilePath(to_absolute_path(local_storage_file)); + return protocol::DispatchResponse::Success(); +} + +std::string StorageAgent::to_absolute_path(const std::filesystem::path& input) { + std::filesystem::path abs = + std::filesystem::weakly_canonical(std::filesystem::absolute(input)); + return abs.generic_string(); +} + +} // namespace protocol +} // namespace inspector +} // namespace node diff --git a/src/inspector/storage_agent.h b/src/inspector/storage_agent.h new file mode 100644 index 00000000000000..46680182672f64 --- /dev/null +++ b/src/inspector/storage_agent.h @@ -0,0 +1,33 @@ +#ifndef SRC_INSPECTOR_STORAGE_AGENT_H_ +#define SRC_INSPECTOR_STORAGE_AGENT_H_ + +#include "env.h" +#include "node/inspector/protocol/Storage.h" + +namespace node { +namespace inspector { +namespace protocol { + +class StorageAgent : public protocol::Storage::Backend { + public: + explicit StorageAgent(Environment* env); + ~StorageAgent() override; + + void Wire(protocol::UberDispatcher* dispatcher); + + DispatchResponse getStorageKey(std::optional frameId, + protocol::String* storageKey) override; + + StorageAgent(const StorageAgent&) = delete; + StorageAgent& operator=(const StorageAgent&) = delete; + + private: + std::string to_absolute_path(const std::filesystem::path& input); + std::unique_ptr frontend_; + Environment* env_; +}; +} // namespace protocol +} // namespace inspector +} // namespace node + +#endif // SRC_INSPECTOR_STORAGE_AGENT_H_ diff --git a/src/inspector_agent.cc b/src/inspector_agent.cc index 5c49b00a2bd9b4..5e1d9149dc755a 100644 --- a/src/inspector_agent.cc +++ b/src/inspector_agent.cc @@ -1,13 +1,18 @@ #include "inspector_agent.h" +#include +#include #include "crdtp/json.h" #include "env-inl.h" +#include "inspector/dom_storage_agent.h" +#include "inspector/io_agent.h" #include "inspector/main_thread_interface.h" #include "inspector/network_inspector.h" #include "inspector/node_json.h" #include "inspector/node_string.h" #include "inspector/protocol_helper.h" #include "inspector/runtime_agent.h" +#include "inspector/storage_agent.h" #include "inspector/target_agent.h" #include "inspector/tracing_agent.h" #include "inspector/worker_agent.h" @@ -225,6 +230,7 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel, bool prevent_shutdown) : delegate_(std::move(delegate)), main_thread_(main_thread), + env_(env), prevent_shutdown_(prevent_shutdown), retaining_context_(false) { session_ = inspector->connect(CONTEXT_GROUP_ID, @@ -259,6 +265,12 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel, target_agent_->Wire(node_dispatcher_.get()); target_agent_->listenWorker(worker_manager); } + if (env->options()->experimental_storage_inspection) { + dom_storage_agent_ = std::make_unique(env); + dom_storage_agent_->Wire(node_dispatcher_.get()); + storage_agent_ = std::make_unique(env_); + storage_agent_->Wire(node_dispatcher_.get()); + } } ~ChannelImpl() override { @@ -275,6 +287,14 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel, if (target_agent_) { target_agent_->reset(); } + if (storage_agent_) { + storage_agent_->disable(); + storage_agent_.reset(); + } + if (dom_storage_agent_) { + dom_storage_agent_->disable(); + dom_storage_agent_.reset(); + } } void emitNotificationFromBackend(v8::Local context, @@ -283,11 +303,13 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel, 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)) { + if (network_inspector_->canEmit(domain_name) && + env_->options()->experimental_network_inspection) { network_inspector_->emitNotification( context, domain_name, event_name, params); - } else { - UNREACHABLE("Unknown domain for emitNotificationFromBackend"); + } else if (dom_storage_agent_ && dom_storage_agent_->canEmit(domain_name) && + env_->options()->experimental_storage_inspection) { + dom_storage_agent_->emitNotification(context, event_name, params); } } @@ -418,11 +440,14 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel, std::unique_ptr worker_agent_; std::shared_ptr target_agent_; std::unique_ptr network_inspector_; + std::unique_ptr dom_storage_agent_; + std::unique_ptr storage_agent_; std::shared_ptr io_agent_; std::unique_ptr delegate_; std::unique_ptr session_; std::unique_ptr node_dispatcher_; std::shared_ptr main_thread_; + Environment* env_; bool prevent_shutdown_; bool retaining_context_; }; @@ -942,7 +967,6 @@ std::unique_ptr Agent::ConnectToMainThread( void Agent::EmitProtocolEvent(v8::Local context, const StringView& event, Local params) { - if (!env()->options()->experimental_network_inspection) return; client_->emitNotification(context, event, params); } diff --git a/src/node_builtins.cc b/src/node_builtins.cc index 6a45e847ada079..70c628294c98d7 100644 --- a/src/node_builtins.cc +++ b/src/node_builtins.cc @@ -122,6 +122,7 @@ BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const { "internal/inspector/network", "internal/inspector/network_http", "internal/inspector/network_http2", "internal/inspector/network_undici", "internal/inspector_async_hook", "internal/inspector_network_tracking", + "internal/inspector/webstorage", #endif // !HAVE_INSPECTOR #if !NODE_USE_V8_PLATFORM || !defined(NODE_HAVE_I18N_SUPPORT) @@ -145,6 +146,7 @@ BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const { "wasi", // Experimental. #if !HAVE_SQLITE "internal/webstorage", // Experimental. + "internal/inspector/webstorage", #endif "internal/test/binding", "internal/v8_prof_polyfill", }; diff --git a/src/node_options.cc b/src/node_options.cc index 0cd3f6bd82d45b..ece1707e084c21 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -800,6 +800,9 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { AddOption("--experimental-network-inspection", "experimental network inspection support", &EnvironmentOptions::experimental_network_inspection); + AddOption("--experimental-storage-inspection", + "experimental storage inspection support", + &EnvironmentOptions::experimental_storage_inspection); AddOption("--experimental-worker-inspection", "experimental worker inspection support", &EnvironmentOptions::experimental_worker_inspection); diff --git a/src/node_options.h b/src/node_options.h index 65baf63cafe5ad..aceb54e382bf72 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -174,6 +174,7 @@ class EnvironmentOptions : public Options { bool cpu_prof = false; bool experimental_network_inspection = false; bool experimental_worker_inspection = false; + bool experimental_storage_inspection = false; bool experimental_inspector_network_resource = false; std::string heap_prof_dir; std::string heap_prof_name; diff --git a/src/node_webstorage.cc b/src/node_webstorage.cc index 013322e8fb6cb7..d8078241e81310 100644 --- a/src/node_webstorage.cc +++ b/src/node_webstorage.cc @@ -1,4 +1,6 @@ #include "node_webstorage.h" +#include +#include #include "base_object-inl.h" #include "debug_utils-inl.h" #include "env-inl.h" @@ -7,6 +9,7 @@ #include "node_errors.h" #include "node_mem-inl.h" #include "path.h" +#include "simdutf.h" #include "sqlite3.h" #include "util-inl.h" @@ -278,6 +281,35 @@ MaybeLocal Storage::Enumerate() { return Array::New(env()->isolate(), values.data(), values.size()); } +std::unordered_map Storage::GetAll() { + if (!Open().IsJust()) { + return {}; + } + + static constexpr std::string_view sql = + "SELECT key, value FROM nodejs_webstorage"; + sqlite3_stmt* s = nullptr; + int r = sqlite3_prepare_v2(db_.get(), sql.data(), sql.size(), &s, nullptr); + auto stmt = stmt_unique_ptr(s); + std::unordered_map result; + while ((r = sqlite3_step(stmt.get())) == SQLITE_ROW) { + CHECK(sqlite3_column_type(stmt.get(), 0) == SQLITE_BLOB); + CHECK(sqlite3_column_type(stmt.get(), 1) == SQLITE_BLOB); + auto key_size = sqlite3_column_bytes(stmt.get(), 0) / sizeof(uint16_t); + auto value_size = sqlite3_column_bytes(stmt.get(), 1) / sizeof(uint16_t); + auto key_uint16( + reinterpret_cast(sqlite3_column_blob(stmt.get(), 0))); + auto value_uint16( + reinterpret_cast(sqlite3_column_blob(stmt.get(), 1))); + + std::u16string key(key_uint16, key_size); + std::u16string value(value_uint16, value_size); + + result.emplace(std::move(key), std::move(value)); + } + return result; +} + MaybeLocal Storage::Length() { if (!Open().IsJust()) { return {}; diff --git a/src/node_webstorage.h b/src/node_webstorage.h index c2548d32e993fd..938a2333194b76 100644 --- a/src/node_webstorage.h +++ b/src/node_webstorage.h @@ -3,6 +3,7 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#include #include "base_object.h" #include "node_mem.h" #include "sqlite3.h" @@ -40,6 +41,7 @@ class Storage : public BaseObject { v8::MaybeLocal LoadKey(const int index); v8::Maybe Remove(v8::Local key); v8::Maybe Store(v8::Local key, v8::Local value); + std::unordered_map GetAll(); SET_MEMORY_INFO_NAME(Storage) SET_SELF_SIZE(Storage) diff --git a/test/fixtures/test-inspector-dom-storage.mjs b/test/fixtures/test-inspector-dom-storage.mjs new file mode 100644 index 00000000000000..fa85528e3cc1c6 --- /dev/null +++ b/test/fixtures/test-inspector-dom-storage.mjs @@ -0,0 +1,151 @@ +import * as common from '../common/index.mjs'; +import assert from 'assert'; +import { DOMStorage, Session } from 'node:inspector/promises'; +import { pathToFileURL } from 'node:url'; +import path from 'node:path'; +import process from 'node:process'; + +{ + const session = new Session(); + await session.connect(); + + await session.post('DOMStorage.enable'); + + const localStorageFileUrl = + pathToFileURL(path.join(process.cwd(), 'localstorage.db')).href; + + const { storageKey } = await session.post('Storage.getStorageKey'); + assert.strictEqual( + storageKey.toLowerCase(), + localStorageFileUrl.toLowerCase(), + ); + + for (const isLocalStorage of [true, false]) { + DOMStorage.registerStorage({ + isLocalStorage, + storageMap: { + key1: 'value1', + key2: 'value2', + [1]: 2, + [true]: 'booleanKey', + ['ключ']: 'значение', + }, + }); + const result = await session.post('DOMStorage.getDOMStorageItems', { + storageId: { + isLocalStorage, + securityOrigin: 'node-inspector://default-dom-storage', + }, + }); + const sortedEntries = result.entries.sort((a, b) => + a[0].localeCompare(b[0]), + ); + assert.deepStrictEqual(sortedEntries, [ + ['1', '2'], + ['key1', 'value1'], + ['key2', 'value2'], + ['true', 'booleanKey'], + ['ключ', 'значение'], + ]); + } + session.disconnect(); +} + +{ + for (const isLocalStorage of [true, false]) { + const webStorage = isLocalStorage ? localStorage : sessionStorage; + const session = new Session(); + webStorage.clear(); + await session.connect(); + + const storageKey = await session.post('Storage.getStorageKey'); + await session.post('DOMStorage.enable'); + + webStorage.setItem('key1', 'value'); + webStorage.setItem('key2', 1); + webStorage.setItem('key3', JSON.stringify({ a: 1 })); + webStorage.setItem('ключ', 'значение'); + + const result = await session.post('DOMStorage.getDOMStorageItems', { + storageId: { + isLocalStorage, + securityOrigin: '', + storageKey: storageKey.storageKey, + }, + }); + assert.strictEqual(result.entries.length, 4); + const entries = Object.fromEntries(result.entries); + assert.strictEqual(entries.key1, 'value'); + assert.strictEqual(entries.key2, '1'); + assert.strictEqual(entries.key3, JSON.stringify({ a: 1 })); + assert.strictEqual(entries['ключ'], 'значение'); + session.disconnect(); + } +} + +{ + for (const isLocalStorage of [true, false]) { + const webStorage = isLocalStorage ? localStorage : sessionStorage; + webStorage.clear(); + const session = new Session(); + await session.connect(); + await session.post('DOMStorage.enable'); + const storageKey = await session.post('Storage.getStorageKey'); + session.on( + 'DOMStorage.domStorageItemAdded', + common.mustCall(({ params }) => { + assert.strictEqual(params.key, 'key'); + assert.strictEqual(params.newValue, 'value'); + assert.deepStrictEqual(params.storageId, { + securityOrigin: '', + isLocalStorage, + storageKey: storageKey.storageKey, + }); + }), + ); + webStorage.setItem('key', 'value'); + + session.on( + 'DOMStorage.domStorageItemUpdated', + common.mustCall(({ params }) => { + assert.strictEqual(params.key, 'key'); + assert.strictEqual(params.oldValue, 'value'); + assert.strictEqual(params.newValue, 'newValue'); + assert.deepStrictEqual(params.storageId, { + securityOrigin: '', + isLocalStorage, + storageKey: storageKey.storageKey, + }); + }), + ); + + webStorage.setItem('key', 'newValue'); + + session.on( + 'DOMStorage.domStorageItemRemoved', + common.mustCall(({ params }) => { + assert.strictEqual(params.key, 'key'); + assert.deepStrictEqual(params.storageId, { + securityOrigin: '', + isLocalStorage, + storageKey: storageKey.storageKey, + }); + }), + ); + + webStorage.removeItem('key'); + + session.on( + 'DOMStorage.domStorageItemsCleared', + common.mustCall(({ params }) => { + assert.deepStrictEqual(params.storageId, { + securityOrigin: '', + isLocalStorage, + storageKey: storageKey.storageKey, + }); + }), + ); + webStorage.clear(); + session.disconnect(); + } +} diff --git a/test/parallel/test-inspector-dom-storage.js b/test/parallel/test-inspector-dom-storage.js new file mode 100644 index 00000000000000..134addea102658 --- /dev/null +++ b/test/parallel/test-inspector-dom-storage.js @@ -0,0 +1,17 @@ +'use strict'; + +const common = require('../common'); +common.skipIfSQLiteMissing(); +common.skipIfInspectorDisabled(); +const { spawnSyncAndExitWithoutError } = require('../common/child_process'); +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +spawnSyncAndExitWithoutError(process.execPath, [ + '--inspect=0', + '--experimental-webstorage', + '--experimental-storage-inspection', + '--localstorage-file=./localstorage.db', + fixtures.path('test-inspector-dom-storage.mjs'), +], { cwd: tmpdir.path }); diff --git a/test/parallel/test-inspector-emit-protocol-event-errors.js b/test/parallel/test-inspector-emit-protocol-event-errors.js new file mode 100644 index 00000000000000..1a76a491c2195c --- /dev/null +++ b/test/parallel/test-inspector-emit-protocol-event-errors.js @@ -0,0 +1,503 @@ +// Flags: --inspect=0 --experimental-network-inspection --experimental-storage-inspection +'use strict'; +const common = require('../common'); + +common.skipIfInspectorDisabled(); + +const inspector = require('node:inspector/promises'); +const assert = require('node:assert'); + +function omit(object, ...keys) { + const copy = { ...object }; + for (const key of keys) { + delete copy[key]; + } + return copy; +} + +function networkRequest(overrides = {}) { + return { + requestId: 'request-id', + timestamp: 1000, + wallTime: 1000, + request: { + url: 'https://nodejs.org/en', + method: 'GET', + headers: {}, + }, + ...overrides, + }; +} + +function networkResponse(overrides = {}) { + return { + requestId: 'response-id', + timestamp: 1000, + type: 'Other', + response: { + url: 'https://nodejs.org/en', + status: 200, + statusText: 'OK', + headers: {}, + }, + ...overrides, + }; +} + +function loadingFailed(overrides = {}) { + return { + requestId: 'loading-failed-id', + timestamp: 1000, + type: 'Document', + errorText: 'Failed to load resource', + ...overrides, + }; +} + +function loadingFinished(overrides = {}) { + return { + requestId: 'loading-finished-id', + timestamp: 1000, + ...overrides, + }; +} + +function webSocketCreated(overrides = {}) { + return { + requestId: 'websocket-created-id', + url: 'ws://example.com:8080', + ...overrides, + }; +} + +function webSocketClosed(overrides = {}) { + return { + requestId: 'websocket-closed-id', + timestamp: 1000, + ...overrides, + }; +} + +function webSocketResponse(overrides = {}) { + return { + requestId: 'websocket-response-id', + timestamp: 1000, + response: { + status: 101, + statusText: 'Switching Protocols', + headers: {}, + }, + ...overrides, + }; +} + +function storageId(overrides = {}) { + return { + securityOrigin: '', + isLocalStorage: true, + storageKey: 'node-inspector://default-dom-storage', + ...overrides, + }; +} + +function domStorageItemAdded(overrides = {}) { + return { + storageId: storageId(), + key: 'testKey', + newValue: 'testValue', + ...overrides, + }; +} + +function domStorageItemRemoved(overrides = {}) { + return { + storageId: storageId(), + key: 'testKey', + ...overrides, + }; +} + +function domStorageItemUpdated(overrides = {}) { + return { + storageId: storageId(), + key: 'testKey', + oldValue: 'oldValue', + newValue: 'newValue', + ...overrides, + }; +} + +function registerStorage(overrides = {}) { + return { + isLocalStorage: true, + storageMap: {}, + ...overrides, + }; +} + +const NETWORK_ERROR_CASES = [ + [ + 'requestWillBeSent', + omit(networkRequest(), 'requestId'), + 'Missing requestId in event', + ], + [ + 'requestWillBeSent', + omit(networkRequest(), 'timestamp'), + 'Missing timestamp in event', + ], + [ + 'requestWillBeSent', + omit(networkRequest(), 'wallTime'), + 'Missing wallTime in event', + ], + [ + 'requestWillBeSent', + omit(networkRequest(), 'request'), + 'Missing request in event', + ], + [ + 'requestWillBeSent', + networkRequest({ request: omit(networkRequest().request, 'url') }), + 'Missing request.url in event', + ], + [ + 'requestWillBeSent', + networkRequest({ request: omit(networkRequest().request, 'method') }), + 'Missing request.method in event', + ], + [ + 'requestWillBeSent', + networkRequest({ request: omit(networkRequest().request, 'headers') }), + 'Missing request.headers in event', + ], + + [ + 'responseReceived', + omit(networkResponse(), 'requestId'), + 'Missing requestId in event', + ], + [ + 'responseReceived', + omit(networkResponse(), 'timestamp'), + 'Missing timestamp in event', + ], + [ + 'responseReceived', + omit(networkResponse(), 'type'), + 'Missing type in event', + ], + [ + 'responseReceived', + omit(networkResponse(), 'response'), + 'Missing response in event', + ], + [ + 'responseReceived', + networkResponse({ response: omit(networkResponse().response, 'url') }), + 'Missing response.url in event', + ], + [ + 'responseReceived', + networkResponse({ response: omit(networkResponse().response, 'status') }), + 'Missing response.status in event', + ], + [ + 'responseReceived', + networkResponse({ + response: omit(networkResponse().response, 'statusText'), + }), + 'Missing response.statusText in event', + ], + [ + 'responseReceived', + networkResponse({ response: omit(networkResponse().response, 'headers') }), + 'Missing response.headers in event', + ], + [ + 'responseReceived', + networkResponse({ + response: { ...networkResponse().response, headers: { host: 1 } }, + }), + 'Invalid header value in event', + ], + + [ + 'loadingFailed', + omit(loadingFailed(), 'requestId'), + 'Missing requestId in event', + ], + [ + 'loadingFailed', + omit(loadingFailed(), 'timestamp'), + 'Missing timestamp in event', + ], + ['loadingFailed', omit(loadingFailed(), 'type'), 'Missing type in event'], + [ + 'loadingFailed', + omit(loadingFailed(), 'errorText'), + 'Missing errorText in event', + ], + + [ + 'loadingFinished', + omit(loadingFinished(), 'requestId'), + 'Missing requestId in event', + ], + [ + 'loadingFinished', + omit(loadingFinished(), 'timestamp'), + 'Missing timestamp in event', + ], + + [ + 'webSocketCreated', + omit(webSocketCreated(), 'requestId'), + 'Missing requestId in event', + ], + ['webSocketCreated', omit(webSocketCreated(), 'url'), 'Missing url in event'], + + [ + 'webSocketClosed', + omit(webSocketClosed(), 'requestId'), + 'Missing requestId in event', + ], + [ + 'webSocketClosed', + omit(webSocketClosed(), 'timestamp'), + 'Missing timestamp in event', + ], + + [ + 'webSocketHandshakeResponseReceived', + omit(webSocketResponse(), 'requestId'), + 'Missing requestId in event', + ], + [ + 'webSocketHandshakeResponseReceived', + omit(webSocketResponse(), 'timestamp'), + 'Missing timestamp in event', + ], + [ + 'webSocketHandshakeResponseReceived', + omit(webSocketResponse(), 'response'), + 'Missing response in event', + ], + [ + 'webSocketHandshakeResponseReceived', + webSocketResponse({ + response: omit(webSocketResponse().response, 'status'), + }), + 'Missing response.status in event', + ], + [ + 'webSocketHandshakeResponseReceived', + webSocketResponse({ + response: omit(webSocketResponse().response, 'statusText'), + }), + 'Missing response.statusText in event', + ], + [ + 'webSocketHandshakeResponseReceived', + webSocketResponse({ + response: omit(webSocketResponse().response, 'headers'), + }), + 'Missing response.headers in event', + ], +]; + +const DOM_STORAGE_ERROR_CASES = [ + [ + 'domStorageItemAdded', + omit(domStorageItemAdded(), 'storageId'), + 'Missing storageId in event', + ], + [ + 'domStorageItemAdded', + domStorageItemAdded({ storageId: omit(storageId(), 'securityOrigin') }), + 'Missing securityOrigin in storageId', + ], + [ + 'domStorageItemAdded', + domStorageItemAdded({ storageId: omit(storageId(), 'storageKey') }), + 'Missing storageKey in storageId', + ], + [ + 'domStorageItemAdded', + omit(domStorageItemAdded(), 'key'), + 'Missing key in event', + ], + [ + 'domStorageItemAdded', + omit(domStorageItemAdded(), 'newValue'), + 'Missing newValue in event', + ], + + [ + 'domStorageItemRemoved', + omit(domStorageItemRemoved(), 'storageId'), + 'Missing storageId in event', + ], + [ + 'domStorageItemRemoved', + omit(domStorageItemRemoved(), 'key'), + 'Missing key in event', + ], + + [ + 'domStorageItemUpdated', + omit(domStorageItemUpdated(), 'storageId'), + 'Missing storageId in event', + ], + [ + 'domStorageItemUpdated', + omit(domStorageItemUpdated(), 'key'), + 'Missing key in event', + ], + [ + 'domStorageItemUpdated', + omit(domStorageItemUpdated(), 'oldValue'), + 'Missing oldValue in event', + ], + [ + 'domStorageItemUpdated', + omit(domStorageItemUpdated(), 'newValue'), + 'Missing newValue in event', + ], + + ['domStorageItemsCleared', {}, 'Missing storageId in event'], + + [ + 'registerStorage', + omit(registerStorage(), 'isLocalStorage'), + 'Missing isLocalStorage in event', + ], + [ + 'registerStorage', + omit(registerStorage(), 'storageMap'), + 'Missing storageMap in event', + ], + [ + 'registerStorage', + registerStorage({ + storageMap: new Proxy( + {}, + { + ownKeys() { + throw new Error('boom'); + }, + }, + ), + }), + 'Failed to get property names from storageMap', + ], + [ + 'registerStorage', + registerStorage({ + storageMap: new Proxy( + { testKey: 'testValue' }, + { + get(target, property, receiver) { + if (property === 'testKey') throw new Error('boom'); + return Reflect.get(target, property, receiver); + }, + }, + ), + }), + 'Failed to get value from storageMap', + ], +]; + +const DATA_SENT_REQUEST_ID = 'data-sent-id'; +const DATA_SENT_ERROR_CASES = [ + [{ finished: false }, 'Missing requestId in event'], + [{ requestId: DATA_SENT_REQUEST_ID }, 'Missing timestamp in event'], + [ + { requestId: DATA_SENT_REQUEST_ID, timestamp: 1000 }, + 'Missing dataLength in event', + ], + [ + { requestId: DATA_SENT_REQUEST_ID, timestamp: 1000, dataLength: 1 }, + 'Missing data in event', + ], + [ + { + requestId: DATA_SENT_REQUEST_ID, + timestamp: 1000, + dataLength: 1, + data: {}, + }, + 'Expected data to be Uint8Array in event', + ], +]; + +const DATA_RECEIVED_ERROR_CASES = [ + [{}, 'Missing requestId in event'], + [{ requestId: 'data-received-id' }, 'Missing timestamp in event'], + [ + { requestId: 'data-received-id', timestamp: 1000 }, + 'Missing dataLength in event', + ], + [ + { requestId: 'data-received-id', timestamp: 1000, dataLength: 1 }, + 'Missing encodedDataLength in event', + ], + [ + { + requestId: 'data-received-id', + timestamp: 1000, + dataLength: 1, + encodedDataLength: 1, + }, + 'Missing data in event', + ], + [ + { + requestId: 'data-received-id', + timestamp: 1000, + dataLength: 1, + encodedDataLength: 1, + data: {}, + }, + 'Expected data to be Uint8Array in event', + ], +]; + +function assertEventErrors(domain, name, params, message) { + assert.throws( + () => inspector[domain][name](params), + { + message, + }, + `Expected ${domain}.${name} to throw`, + ); +} + +function startRequest(requestId) { + inspector.Network.requestWillBeSent(networkRequest({ requestId })); +} + +(async () => { + const session = new inspector.Session(); + session.connect(); + + await session.post('Network.enable'); + await session.post('DOMStorage.enable'); + + for (const [name, params, message] of NETWORK_ERROR_CASES) { + assertEventErrors('Network', name, params, message); + } + + startRequest(DATA_SENT_REQUEST_ID); + for (const [params, message] of DATA_SENT_ERROR_CASES) { + assertEventErrors('Network', 'dataSent', params, message); + } + + for (const [params, message] of DATA_RECEIVED_ERROR_CASES) { + assertEventErrors('Network', 'dataReceived', params, message); + } + + for (const [name, params, message] of DOM_STORAGE_ERROR_CASES) { + assertEventErrors('DOMStorage', name, params, message); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-inspector-emit-protocol-event.js b/test/parallel/test-inspector-emit-protocol-event.js index bffea8c1cd867e..567c92e3eeba6a 100644 --- a/test/parallel/test-inspector-emit-protocol-event.js +++ b/test/parallel/test-inspector-emit-protocol-event.js @@ -1,4 +1,4 @@ -// Flags: --inspect=0 --experimental-network-inspection +// Flags: --inspect=0 --experimental-network-inspection --experimental-storage-inspection 'use strict'; const common = require('../common'); @@ -116,6 +116,36 @@ const EXPECTED_EVENTS = { } }, + ], + DOMStorage: [ + { + name: 'domStorageItemAdded', + params: { + storageId: { + securityOrigin: '', + isLocalStorage: true, + storageKey: 'node-inspector://default-dom-storage', + }, + key: 'testKey', + newValue: 'testValue', + } + }, + { + name: 'domStorageItemRemoved', + skip: true + }, + { + name: 'domStorageItemUpdated', + skip: true + }, + { + name: 'domStorageItemsCleared', + skip: true + }, + { + name: 'registerStorage', + skip: true + }, ] }; @@ -146,6 +176,7 @@ for (const [domain, events] of Object.entries(EXPECTED_EVENTS)) { // Check that all events emit the expected parameters. await session.post('Network.enable'); + await session.post('DOMStorage.enable'); for (const [domain, events] of Object.entries(EXPECTED_EVENTS)) { for (const event of events) { if (event.skip) {