current commit
260926.2 · src: throw on a malformed localStorage file
#include "dom_storage_agent.h"
#include <optional>
#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<protocol::DOMStorage::StorageId> createStorageIdFromObject(
    Local<Context> context, Local<Object> 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<protocol::DOMStorage::Frontend>(dispatcher->channel());
  protocol::DOMStorage::Dispatcher::wire(dispatcher, this);
  addEventNotifier("domStorageItemAdded",
                   [this](v8::Local<v8::Context> ctx, v8::Local<v8::Object> p) {
                     this->domStorageItemAdded(ctx, p);
                   });
  addEventNotifier("domStorageItemRemoved",
                   [this](v8::Local<v8::Context> ctx, v8::Local<v8::Object> p) {
                     this->domStorageItemRemoved(ctx, p);
                   });
  addEventNotifier("domStorageItemUpdated",
                   [this](v8::Local<v8::Context> ctx, v8::Local<v8::Object> p) {
                     this->domStorageItemUpdated(ctx, p);
                   });
  addEventNotifier("domStorageItemsCleared",
                   [this](v8::Local<v8::Context> ctx, v8::Local<v8::Object> p) {
                     this->domStorageItemsCleared(ctx, p);
                   });
  addEventNotifier("registerStorage",
                   [this](v8::Local<v8::Context> ctx, v8::Local<v8::Object> 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<protocol::DOMStorage::StorageId> storageId,
    std::unique_ptr<protocol::Array<protocol::Array<protocol::String>>>*
        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<StorageMap> storage_map_fallback;
  if (storage_map->empty()) {
    auto web_storage_obj = getWebStorage(is_local_storage);
    // Each way of failing below says something different about the store, so
    // each reports a different reason. A frontend that cannot read a store
    // otherwise has no way to tell a missing one from a corrupt one.
    if (!web_storage_obj) {
      return protocol::DispatchResponse::ServerError(
          "Could not read DOM storage items: storage is unavailable");
    }
    // A message from a remote frontend is dispatched without a HandleScope
    // on the stack, and opening the backing file can throw, so give the
    // exception a scope to be allocated in and somewhere to land.
    v8::HandleScope handle_scope(env_->isolate());
    v8::TryCatch try_catch(env_->isolate());
    storage_map_fallback = web_storage_obj.value()->GetAll();
    if (try_catch.HasCaught()) {
      // Pass the reason along; "the file was written by a newer Node.js" and
      // "the file is locked" are not the same problem to the user. Read it
      // off the Message, which was built when the exception was thrown.
      // Converting the exception itself would call a user-patchable
      // Error.prototype.toString, and there is no JavaScript frame here to
      // run it from.
      Local<v8::Message> message = try_catch.Message();
      if (!message.IsEmpty()) {
        Utf8Value reason(env_->isolate(), message->Get());
        return protocol::DispatchResponse::ServerError(
            std::string("Could not read DOM storage items: ") + reason.out());
      }
      // V8 builds that Message on a best-effort basis, so the throw is all we
      // can report when it is missing.
      return protocol::DispatchResponse::ServerError(
          "Could not read DOM storage items: the backing store could not be "
          "opened");
    }
    if (!storage_map_fallback.has_value()) {
      return protocol::DispatchResponse::ServerError(
          "Could not read DOM storage items: the backing file is malformed");
    }
    storage_map = &storage_map_fallback.value();
  }

  auto result =
      std::make_unique<protocol::Array<protocol::Array<protocol::String>>>();
  for (const auto& pair : *storage_map) {
    auto item = std::make_unique<protocol::Array<protocol::String>>();
    item->push_back(protocol::StringUtil::fromUTF16(
        reinterpret_cast<const uint16_t*>(pair.first.data()),
        pair.first.size()));
    item->push_back(protocol::StringUtil::fromUTF16(
        reinterpret_cast<const uint16_t*>(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<protocol::DOMStorage::StorageId> storageId,
    const std::string& key,
    const std::string& value) {
  return protocol::DispatchResponse::ServerError("Not implemented");
}

protocol::DispatchResponse DOMStorageAgent::removeDOMStorageItem(
    std::unique_ptr<protocol::DOMStorage::StorageId> storageId,
    const std::string& key) {
  return protocol::DispatchResponse::ServerError("Not implemented");
}

protocol::DispatchResponse DOMStorageAgent::clear(
    std::unique_ptr<protocol::DOMStorage::StorageId> storageId) {
  return protocol::DispatchResponse::ServerError("Not implemented");
}

void DOMStorageAgent::domStorageItemAdded(Local<Context> context,
                                          Local<Object> params) {
  Isolate* isolate = env_->isolate();
  Local<Object> storage_id_obj;
  if (!ObjectGetObject(context, params, "storageId").ToLocal(&storage_id_obj)) {
    ThrowEventError(isolate, "Missing storageId in event");
    return;
  }

  std::unique_ptr<protocol::DOMStorage::StorageId> 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> context,
                                            Local<Object> params) {
  Isolate* isolate = env_->isolate();
  Local<Object> storage_id_obj;
  if (!ObjectGetObject(context, params, "storageId").ToLocal(&storage_id_obj)) {
    ThrowEventError(isolate, "Missing storageId in event");
    return;
  }
  std::unique_ptr<protocol::DOMStorage::StorageId> 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> context,
                                            Local<Object> params) {
  Isolate* isolate = env_->isolate();
  Local<Object> storage_id_obj;
  if (!ObjectGetObject(context, params, "storageId").ToLocal(&storage_id_obj)) {
    ThrowEventError(isolate, "Missing storageId in event");
    return;
  }

  std::unique_ptr<protocol::DOMStorage::StorageId> 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> context,
                                             Local<Object> params) {
  Isolate* isolate = env_->isolate();
  Local<Object> storage_id_obj;
  if (!ObjectGetObject(context, params, "storageId").ToLocal(&storage_id_obj)) {
    ThrowEventError(isolate, "Missing storageId in event");
    return;
  }
  std::unique_ptr<protocol::DOMStorage::StorageId> storage_id =
      createStorageIdFromObject(context, storage_id_obj);

  if (!storage_id) {
    return;
  }
  frontend_->domStorageItemsCleared(std::move(storage_id));
}

void DOMStorageAgent::registerStorage(Local<Context> context,
                                      Local<Object> 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<Object> 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<Array> 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<Value> key_value;
    if (!property_names->Get(context, i).ToLocal(&key_value)) {
      ThrowEventError(isolate, "Failed to get key from storageMap");
      return;
    }
    Local<Value> 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<node::webstorage::Storage*> DOMStorageAgent::getWebStorage(
    bool is_local_storage) {
  v8::Isolate* isolate = env_->isolate();
  v8::HandleScope handle_scope(isolate);
  v8::Local<v8::Object> global = env_->context()->Global();
  v8::Local<v8::Value> 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<v8::Object>(), std::nullopt);
    return storage;
  }
}

bool DOMStorageAgent::canEmit(const std::string& domain) {
  return domain == "DOMStorage";
}
}  // namespace inspector
}  // namespace node