// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Implementation of the ExtensionPortsRemoteService.
// Inspired significantly from debugger_remote_service
// and ../automation/extension_port_container.
#include "chrome/browser/debugger/extension_ports_remote_service.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/message_loop.h"
#include "base/string_number_conversions.h"
#include "base/values.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/debugger/devtools_manager.h"
#include "chrome/browser/debugger/devtools_protocol_handler.h"
#include "chrome/browser/debugger/devtools_remote_message.h"
#include "chrome/browser/debugger/inspectable_tab_proxy.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/common/devtools_messages.h"
#include "chrome/common/extensions/extension_messages.h"
#include "content/browser/tab_contents/tab_contents.h"
namespace {
// Protocol is as follows:
//
// From external client:
// {"command": "connect",
// "data": {
// "extensionId": "<extension_id string>",
// "channelName": "<port name string>", (optional)
// "tabId": <numerical tab ID> (optional)
// }
// }
// To connect to a background page or tool strip, the tabId should be omitted.
// Tab IDs can be enumerated with the list_tabs DevToolsService command.
//
// Response:
// {"command": "connect",
// "result": 0, (assuming success)
// "data": {
// "portId": <numerical port ID>
// }
// }
//
// Posting a message from external client:
// Put the target message port ID in the devtools destination field.
// {"command": "postMessage",
// "data": <message body - arbitrary JSON>
// }
// Response:
// {"command": "postMessage",
// "result": 0 (Assuming success)
// }
// Note this is a confirmation from the devtools protocol layer, not
// a response from the extension.
//
// Message from an extension to the external client:
// The message port ID is in the devtools destination field.
// {"command": "onMessage",
// "result": 0, (Always 0)
// "data": <message body - arbitrary JSON>
// }
//
// The "disconnect" command from the external client, and
// "onDisconnect" notification from the ExtensionMessageService, are
// similar: with the message port ID in the destination field, but no
// "data" field in this case.
// Commands:
const char kConnect[] = "connect";
const char kDisconnect[] = "disconnect";
const char kPostMessage[] = "postMessage";
// Events:
const char kOnMessage[] = "onMessage";
const char kOnDisconnect[] = "onDisconnect";
// Constants for the JSON message fields.
// The type is wstring because the constant is used to get a
// DictionaryValue field (which requires a wide string).
// Mandatory.
const char kCommandKey[] = "command";
// Always present in messages sent to the external client.
const char kResultKey[] = "result";
// Field for command-specific parameters. Not strictly necessary, but
// makes it more similar to the remote debugger protocol, which should
// allow easier reuse of client code.
const char kDataKey[] = "data";
// Fields within the "data" dictionary:
// Required for "connect":
const char kExtensionIdKey[] = "extensionId";
// Optional in "connect":
const char kChannelNameKey[] = "channelName";
const char kTabIdKey[] = "tabId";
// Present under "data" in replies to a successful "connect" .
const char kPortIdKey[] = "portId";
} // namespace
const std::string ExtensionPortsRemoteService::kToolName = "ExtensionPorts";
ExtensionPortsRemoteService::ExtensionPortsRemoteService(
DevToolsProtocolHandler* delegate)
: delegate_(delegate), service_(NULL) {
// We need an ExtensionMessageService instance. It hangs off of
// |profile|. But we do not have a particular tab or RenderViewHost
// as context. I'll just use the first active profile not in
// incognito mode. But this is probably not the right way.
ProfileManager* profile_manager = g_browser_process->profile_manager();
if (!profile_manager) {
LOG(WARNING) << "No profile manager for ExtensionPortsRemoteService";
return;
}
std::vector<Profile*> profiles(profile_manager->GetLoadedProfiles());
for (size_t i = 0; i < profiles.size(); ++i) {
if (!profiles[i]->IsOffTheRecord()) {
service_ = profiles[i]->GetExtensionMessageService();
break;
}
}
if (!service_)
LOG(WARNING) << "No usable profile for ExtensionPortsRemoteService";
}
ExtensionPortsRemoteService::~ExtensionPortsRemoteService() {
}
void ExtensionPortsRemoteService::HandleMessage(
const DevToolsRemoteMessage& message) {
DCHECK_EQ(MessageLoop::current()->type(), MessageLoop::TYPE_UI);
const std::string destinationString = message.destination();
scoped_ptr<Value> request(base::JSONReader::Read(message.content(), true));
if (request.get() == NULL) {
// Bad JSON
NOTREACHED();
return;
}
DictionaryValue* content;
if (!request->IsType(Value::TYPE_DICTIONARY)) {
NOTREACHED(); // Broken protocol :(
return;
}
content = static_cast<DictionaryValue*>(request.get());
if (!content->HasKey(kCommandKey)) {
NOTREACHED(); // Broken protocol :(
return;
}
std::string command;
DictionaryValue response;
content->GetString(kCommandKey, &command);
response.SetString(kCommandKey, command);
if (!service_) {
// This happens if we failed to obtain an ExtensionMessageService
// during initialization.
NOTREACHED();
response.SetInteger(kResultKey, RESULT_NO_SERVICE);
SendResponse(response, message.tool(), message.destination());
return;
}
int destination = -1;
if (!destinationString.empty())
base::StringToInt(destinationString, &destination);
if (command == kConnect) {
if (destination != -1) // destination should be empty for this command.
response.SetInteger(kResultKey, RESULT_UNKNOWN_COMMAND);
else
ConnectCommand(content, &response);
} else if (command == kDisconnect) {
if (destination == -1) // Destination required for this command.
response.SetInteger(kResultKey, RESULT_UNKNOWN_COMMAND);
else
DisconnectCommand(destination, &response);
} else if (command == kPostMessage) {
if (destination == -1) // Destination required for this command.
response.SetInteger(kResultKey, RESULT_UNKNOWN_COMMAND);
else
PostMessageCommand(destination, content, &response);
} else {
// Unknown command
NOTREACHED();
response.SetInteger(kResultKey, RESULT_UNKNOWN_COMMAND);
}
SendResponse(response, message.tool(), message.destination());
}
void ExtensionPortsRemoteService::OnConnectionLost() {
VLOG(1) << "OnConnectionLost";
DCHECK_EQ(MessageLoop::current()->type(), MessageLoop::TYPE_UI);
DCHECK(service_);
for (PortIdSet::iterator it = openPortIds_.begin();
it != openPortIds_.end();
++it)
service_->CloseChannel(*it);
openPortIds_.clear();
}
void ExtensionPortsRemoteService::SendResponse(
const Value& response, const std::string& tool,
const std::string& destination) {
std::string response_content;
base::JSONWriter::Write(&response, false, &response_content);
scoped_ptr<DevToolsRemoteMessage> response_message(
DevToolsRemoteMessageBuilder::instance().Create(
tool, destination, response_content));
delegate_->Send(*response_message.get());
}
bool ExtensionPortsRemoteService::Send(IPC::Message *message) {
DCHECK_EQ(MessageLoop::current()->type(), MessageLoop::TYPE_UI);
IPC_BEGIN_MESSAGE_MAP(ExtensionPortsRemoteService, *message)
IPC_MESSAGE_HANDLER(ExtensionMsg_MessageInvoke, OnExtensionMessageInvoke)
IPC_MESSAGE_UNHANDLED_ERROR()
IPC_END_MESSAGE_MAP()
delete message;
return true;
}
void ExtensionPortsRemoteService::OnExtensionMessageInvoke(
const std::string& extension_id,
const std::string& function_name,
const ListValue& args,
const GURL& event_url) {
if (function_name == ExtensionMessageService::kDispatchOnMessage) {
DCHECK_EQ(args.GetSize(), 2u);
std::string message;
int port_id;
if (args.GetString(0, &message) && args.GetInteger(1, &port_id))
OnExtensionMessage(message, port_id);
} else if (function_name == ExtensionMessageService::kDispatchOnDisconnect) {
DCHECK_EQ(args.GetSize(), 1u);
int port_id;
if (args.GetInteger(0, &port_id))
OnExtensionPortDisconnected(port_id);
} else if (function_name == ExtensionMessageService::kDispatchOnConnect) {
// There is no way for this service to be addressed and receive
// connections.
NOTREACHED() << function_name << " shouldn't be called.";
} else {
NOTREACHED() << function_name << " shouldn't be called.";
}
}
void ExtensionPortsRemoteService::OnExtensionMessage(
const std::string& message, int port_id) {
VLOG(1) << "Message event: from port " << port_id << ", < " << message << ">";
// Transpose the information into a JSON message for the external client.
DictionaryValue content;
content.SetString(kCommandKey, kOnMessage);
content.SetInteger(kResultKey, RESULT_OK);
// Turn the stringified message body back into JSON.
Value* data = base::JSONReader::Read(message, false);
if (!data) {
NOTREACHED();
return;
}
content.Set(kDataKey, data);
SendResponse(content, kToolName, base::IntToString(port_id));
}
void ExtensionPortsRemoteService::OnExtensionPortDisconnected(int port_id) {
VLOG(1) << "Disconnect event for port " << port_id;
openPortIds_.erase(port_id);
DictionaryValue content;
content.SetString(kCommandKey, kOnDisconnect);
content.SetInteger(kResultKey, RESULT_OK);
SendResponse(content, kToolName, base::IntToString(port_id));
}
void ExtensionPortsRemoteService::ConnectCommand(
DictionaryValue* content, DictionaryValue* response) {
// Parse out the parameters.
DictionaryValue* data;
if (!content->GetDictionary(kDataKey, &data)) {
response->SetInteger(kResultKey, RESULT_PARAMETER_ERROR);
return;
}
std::string extension_id;
if (!data->GetString(kExtensionIdKey, &extension_id)) {
response->SetInteger(kResultKey, RESULT_PARAMETER_ERROR);
return;
}
std::string channel_name = "";
data->GetString(kChannelNameKey, &channel_name); // optional.
int tab_id = -1;
data->GetInteger(kTabIdKey, &tab_id); // optional.
int port_id;
if (tab_id != -1) { // Resolve the tab ID.
const InspectableTabProxy::ControllersMap& navcon_map =
delegate_->inspectable_tab_proxy()->controllers_map();
InspectableTabProxy::ControllersMap::const_iterator it =
navcon_map.find(tab_id);
TabContents* tab_contents = NULL;
if (it != navcon_map.end())
tab_contents = it->second->tab_contents();
if (!tab_contents) {
VLOG(1) << "tab not found: " << tab_id;
response->SetInteger(kResultKey, RESULT_TAB_NOT_FOUND);
return;
}
// Ask the ExtensionMessageService to open the channel.
VLOG(1) << "Connect: extension_id <" << extension_id
<< ">, channel_name <" << channel_name
<< ">, tab " << tab_id;
DCHECK(service_);
port_id = service_->OpenSpecialChannelToTab(
extension_id, channel_name, tab_contents, this);
} else { // no tab: channel to an extension' background page / toolstrip.
// Ask the ExtensionMessageService to open the channel.
VLOG(1) << "Connect: extension_id <" << extension_id
<< ">, channel_name <" << channel_name << ">";
DCHECK(service_);
port_id = service_->OpenSpecialChannelToExtension(
extension_id, channel_name, "null", this);
}
if (port_id == -1) {
// Failure: probably the extension ID doesn't exist.
VLOG(1) << "Connect failed";
response->SetInteger(kResultKey, RESULT_CONNECT_FAILED);
return;
}
VLOG(1) << "Connected: port " << port_id;
openPortIds_.insert(port_id);
// Reply to external client with the port ID assigned to the new channel.
DictionaryValue* reply_data = new DictionaryValue();
reply_data->SetInteger(kPortIdKey, port_id);
response->Set(kDataKey, reply_data);
response->SetInteger(kResultKey, RESULT_OK);
}
void ExtensionPortsRemoteService::DisconnectCommand(
int port_id, DictionaryValue* response) {
VLOG(1) << "Disconnect port " << port_id;
PortIdSet::iterator portEntry = openPortIds_.find(port_id);
if (portEntry == openPortIds_.end()) { // unknown port ID.
VLOG(1) << "unknown port: " << port_id;
response->SetInteger(kResultKey, RESULT_UNKNOWN_PORT);
return;
}
DCHECK(service_);
service_->CloseChannel(port_id);
openPortIds_.erase(portEntry);
response->SetInteger(kResultKey, RESULT_OK);
}
void ExtensionPortsRemoteService::PostMessageCommand(
int port_id, DictionaryValue* content, DictionaryValue* response) {
Value* data;
if (!content->Get(kDataKey, &data)) {
response->SetInteger(kResultKey, RESULT_PARAMETER_ERROR);
return;
}
std::string message;
// Stringified the JSON message body.
base::JSONWriter::Write(data, false, &message);
VLOG(1) << "postMessage: port " << port_id
<< ", message: <" << message << ">";
PortIdSet::iterator portEntry = openPortIds_.find(port_id);
if (portEntry == openPortIds_.end()) { // Unknown port ID.
VLOG(1) << "unknown port: " << port_id;
response->SetInteger(kResultKey, RESULT_UNKNOWN_PORT);
return;
}
// Post the message through the ExtensionMessageService.
DCHECK(service_);
service_->PostMessageFromRenderer(port_id, message);
// Confirm to the external client that we sent its message.
response->SetInteger(kResultKey, RESULT_OK);
}