blob: 0c7dcbbc0751688bda4b79bebca0425e8966a1ab [file] [log] [blame]
// Copyright 2015 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.
#include "components/devtools_service/devtools_http_server.h"
#include <string.h>
#include <string>
#include "base/bind.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/stl_util.h"
#include "base/strings/stringprintf.h"
#include "base/values.h"
#include "components/devtools_service/devtools_agent_host.h"
#include "components/devtools_service/devtools_registry_impl.h"
#include "components/devtools_service/devtools_service.h"
#include "mojo/application/public/cpp/application_impl.h"
#include "mojo/services/network/public/cpp/web_socket_read_queue.h"
#include "mojo/services/network/public/cpp/web_socket_write_queue.h"
#include "mojo/services/network/public/interfaces/net_address.mojom.h"
#include "mojo/services/network/public/interfaces/network_service.mojom.h"
#include "mojo/services/network/public/interfaces/web_socket.mojom.h"
#include "third_party/mojo/src/mojo/public/cpp/system/data_pipe.h"
namespace devtools_service {
namespace {
const char kPageUrlPrefix[] = "/devtools/page/";
const char kBrowserUrlPrefix[] = "/devtools/browser";
const char kJsonRequestUrlPrefix[] = "/json";
const char kActivateCommand[] = "activate";
const char kCloseCommand[] = "close";
const char kListCommand[] = "list";
const char kNewCommand[] = "new";
const char kVersionCommand[] = "version";
const char kTargetIdField[] = "id";
const char kTargetTypeField[] = "type";
const char kTargetTitleField[] = "title";
const char kTargetDescriptionField[] = "description";
const char kTargetUrlField[] = "url";
const char kTargetWebSocketDebuggerUrlField[] = "webSocketDebuggerUrl";
const char kTargetDevtoolsFrontendUrlField[] = "devtoolsFrontendUrl";
std::string GetHeaderValue(const mojo::HttpRequest& request,
const std::string& name) {
for (size_t i = 0; i < request.headers.size(); ++i) {
if (name == request.headers[i]->name)
return request.headers[i]->value;
}
return std::string();
}
bool ParseJsonPath(const std::string& path,
std::string* command,
std::string* target_id) {
// Fall back to list in case of empty query.
if (path.empty()) {
*command = kListCommand;
return true;
}
if (path.find("/") != 0) {
// Malformed command.
return false;
}
*command = path.substr(1);
size_t separator_pos = command->find("/");
if (separator_pos != std::string::npos) {
*target_id = command->substr(separator_pos + 1);
*command = command->substr(0, separator_pos);
}
return true;
}
mojo::HttpResponsePtr MakeResponse(uint32_t status_code,
const std::string& content_type,
const std::string& body) {
mojo::HttpResponsePtr response(mojo::HttpResponse::New());
response->headers.resize(2);
response->headers[0] = mojo::HttpHeader::New();
response->headers[0]->name = "Content-Length";
response->headers[0]->value =
base::StringPrintf("%lu", static_cast<unsigned long>(body.size()));
response->headers[1] = mojo::HttpHeader::New();
response->headers[1]->name = "Content-Type";
response->headers[1]->value = content_type;
if (!body.empty()) {
uint32_t num_bytes = static_cast<uint32_t>(body.size());
MojoCreateDataPipeOptions options;
options.struct_size = sizeof(MojoCreateDataPipeOptions);
options.flags = MOJO_CREATE_DATA_PIPE_OPTIONS_FLAG_NONE;
options.element_num_bytes = 1;
options.capacity_num_bytes = num_bytes;
mojo::DataPipe data_pipe(options);
response->body = data_pipe.consumer_handle.Pass();
MojoResult result =
WriteDataRaw(data_pipe.producer_handle.get(), body.data(), &num_bytes,
MOJO_WRITE_DATA_FLAG_ALL_OR_NONE);
CHECK_EQ(MOJO_RESULT_OK, result);
}
return response.Pass();
}
mojo::HttpResponsePtr MakeJsonResponse(uint32_t status_code,
base::Value* value,
const std::string& message) {
// Serialize value and message.
std::string json_value;
if (value) {
base::JSONWriter::WriteWithOptions(
*value, base::JSONWriter::OPTIONS_PRETTY_PRINT, &json_value);
}
return MakeResponse(status_code, "application/json; charset=UTF-8",
json_value + message);
}
class WebSocketRelayer : public DevToolsAgentHost::Delegate,
public mojo::WebSocketClient {
public:
// Creates a WebSocketRelayer instance and sets it as the delegate of
// |agent_host|.
//
// The object destroys itself when either of the following happens:
// - |agent_host| is dead and the object finishes all pending sends (if any)
// to the Web socket; or
// - the underlying pipe of |web_socket| is closed and the object finishes all
// pending receives (if any) from the Web socket.
static mojo::WebSocketClientPtr SetUp(
DevToolsAgentHost* agent_host,
mojo::WebSocketPtr web_socket,
mojo::ScopedDataPipeProducerHandle send_stream) {
DCHECK(agent_host);
DCHECK(web_socket);
DCHECK(send_stream.is_valid());
mojo::WebSocketClientPtr web_socket_client;
new WebSocketRelayer(agent_host, web_socket.Pass(), send_stream.Pass(),
&web_socket_client);
return web_socket_client.Pass();
}
private:
WebSocketRelayer(DevToolsAgentHost* agent_host,
mojo::WebSocketPtr web_socket,
mojo::ScopedDataPipeProducerHandle send_stream,
mojo::WebSocketClientPtr* web_socket_client)
: agent_host_(agent_host),
binding_(this, web_socket_client),
web_socket_(web_socket.Pass()),
send_stream_(send_stream.Pass()),
write_send_stream_(new mojo::WebSocketWriteQueue(send_stream_.get())),
pending_send_count_(0),
pending_receive_count_(0) {
web_socket_.set_connection_error_handler([this]() { OnConnectionError(); });
agent_host->SetDelegate(this);
}
~WebSocketRelayer() override {
if (agent_host_)
agent_host_->SetDelegate(nullptr);
}
// DevToolsAgentHost::Delegate implementation.
void DispatchProtocolMessage(DevToolsAgentHost* agent_host,
const std::string& message) override {
if (!web_socket_)
return;
// TODO(yzshen): It shouldn't be an issue to pass an empty message. However,
// WebSocket{Read,Write}Queue doesn't handle that correctly.
if (message.empty())
return;
pending_send_count_++;
uint32_t size = static_cast<uint32_t>(message.size());
write_send_stream_->Write(
&message[0], size,
base::Bind(&WebSocketRelayer::OnFinishedWritingSendStream,
base::Unretained(this), size));
}
void OnAgentHostClosed(DevToolsAgentHost* agent_host) override {
DispatchProtocolMessage(agent_host_,
"{ \"method\": \"Inspector.detached\", "
"\"params\": { \"reason\": \"target_closed\" } }");
// No need to call SetDelegate(nullptr) on |agent_host_| because it is going
// away.
agent_host_ = nullptr;
if (ShouldSelfDestruct())
delete this;
}
// WebSocketClient implementation.
void DidConnect(const mojo::String& selected_subprotocol,
const mojo::String& extensions,
mojo::ScopedDataPipeConsumerHandle receive_stream) override {
receive_stream_ = receive_stream.Pass();
read_receive_stream_.reset(
new mojo::WebSocketReadQueue(receive_stream_.get()));
}
void DidReceiveData(bool fin,
mojo::WebSocket::MessageType type,
uint32_t num_bytes) override {
if (!agent_host_)
return;
// TODO(yzshen): It shouldn't be an issue to pass an empty message. However,
// WebSocket{Read,Write}Queue doesn't handle that correctly.
if (num_bytes == 0)
return;
pending_receive_count_++;
read_receive_stream_->Read(
num_bytes, base::Bind(&WebSocketRelayer::OnFinishedReadingReceiveStream,
base::Unretained(this), num_bytes));
}
void DidReceiveFlowControl(int64_t quota) override {}
void DidFail(const mojo::String& message) override {}
void DidClose(bool was_clean,
uint16_t code,
const mojo::String& reason) override {}
void OnConnectionError() {
web_socket_ = nullptr;
binding_.Close();
if (ShouldSelfDestruct())
delete this;
}
void OnFinishedWritingSendStream(uint32_t num_bytes, const char* buffer) {
DCHECK_GT(pending_send_count_, 0u);
pending_send_count_--;
if (web_socket_ && buffer)
web_socket_->Send(true, mojo::WebSocket::MESSAGE_TYPE_TEXT, num_bytes);
if (ShouldSelfDestruct())
delete this;
}
void OnFinishedReadingReceiveStream(uint32_t num_bytes, const char* data) {
DCHECK_GT(pending_receive_count_, 0u);
pending_receive_count_--;
if (agent_host_ && data)
agent_host_->SendProtocolMessageToAgent(std::string(data, num_bytes));
if (ShouldSelfDestruct())
delete this;
}
bool ShouldSelfDestruct() const {
return (!agent_host_ && pending_send_count_ == 0) ||
(!web_socket_ && pending_receive_count_ == 0);
}
DevToolsAgentHost* agent_host_;
mojo::Binding<WebSocketClient> binding_;
mojo::WebSocketPtr web_socket_;
mojo::ScopedDataPipeProducerHandle send_stream_;
scoped_ptr<mojo::WebSocketWriteQueue> write_send_stream_;
size_t pending_send_count_;
mojo::ScopedDataPipeConsumerHandle receive_stream_;
scoped_ptr<mojo::WebSocketReadQueue> read_receive_stream_;
size_t pending_receive_count_;
DISALLOW_COPY_AND_ASSIGN(WebSocketRelayer);
};
} // namespace
class DevToolsHttpServer::HttpConnectionDelegateImpl
: public mojo::HttpConnectionDelegate {
public:
HttpConnectionDelegateImpl(
DevToolsHttpServer* owner,
mojo::HttpConnectionPtr connection,
mojo::InterfaceRequest<HttpConnectionDelegate> delegate_request)
: owner_(owner),
connection_(connection.Pass()),
binding_(this, delegate_request.Pass()) {
DCHECK(owner_);
DCHECK(connection_);
DCHECK(binding_.is_bound());
auto error_handler = [this]() { owner_->OnConnectionClosed(this); };
connection_.set_connection_error_handler(error_handler);
binding_.set_connection_error_handler(error_handler);
}
mojo::HttpConnection* connection() { return connection_.get(); }
private:
// mojo::HttpConnectionDelegate implementation:
void OnReceivedRequest(mojo::HttpRequestPtr request,
const OnReceivedRequestCallback& callback) override {
owner_->OnReceivedRequest(this, request.Pass(), callback);
}
void OnReceivedWebSocketRequest(
mojo::HttpRequestPtr request,
const OnReceivedWebSocketRequestCallback& callback) override {
owner_->OnReceivedWebSocketRequest(this, request.Pass(), callback);
}
DevToolsHttpServer* const owner_;
mojo::HttpConnectionPtr connection_;
mojo::Binding<HttpConnectionDelegate> binding_;
DISALLOW_COPY_AND_ASSIGN(HttpConnectionDelegateImpl);
};
DevToolsHttpServer::DevToolsHttpServer(DevToolsService* service,
uint16_t remote_debugging_port)
: service_(service), remote_debugging_port_(remote_debugging_port) {
VLOG(1) << "Remote debugging HTTP server is started on port "
<< remote_debugging_port << ".";
mojo::NetworkServicePtr network_service;
mojo::URLRequestPtr request(mojo::URLRequest::New());
request->url = "mojo:network_service";
service_->application()->ConnectToService(request.Pass(), &network_service);
mojo::NetAddressPtr local_address(mojo::NetAddress::New());
local_address->family = mojo::NET_ADDRESS_FAMILY_IPV4;
local_address->ipv4 = mojo::NetAddressIPv4::New();
local_address->ipv4->port = remote_debugging_port;
local_address->ipv4->addr.resize(4);
local_address->ipv4->addr[0] = 127;
local_address->ipv4->addr[1] = 0;
local_address->ipv4->addr[2] = 0;
local_address->ipv4->addr[3] = 1;
mojo::HttpServerDelegatePtr http_server_delegate;
http_server_delegate_binding_.reset(
new mojo::Binding<mojo::HttpServerDelegate>(this, &http_server_delegate));
network_service->CreateHttpServer(
local_address.Pass(), http_server_delegate.Pass(),
mojo::NetworkService::CreateHttpServerCallback());
}
DevToolsHttpServer::~DevToolsHttpServer() {
STLDeleteElements(&connections_);
}
void DevToolsHttpServer::OnConnected(
mojo::HttpConnectionPtr connection,
mojo::InterfaceRequest<mojo::HttpConnectionDelegate> delegate) {
connections_.insert(
new HttpConnectionDelegateImpl(this, connection.Pass(), delegate.Pass()));
}
void DevToolsHttpServer::OnReceivedRequest(
HttpConnectionDelegateImpl* connection,
mojo::HttpRequestPtr request,
const OnReceivedRequestCallback& callback) {
DCHECK(connections_.find(connection) != connections_.end());
if (request->url.get().find(kJsonRequestUrlPrefix) == 0) {
mojo::HttpResponsePtr response = ProcessJsonRequest(request.Pass());
if (response)
callback.Run(response.Pass());
else
OnConnectionClosed(connection);
} else {
// TODO(yzshen): Implement it.
NOTIMPLEMENTED();
callback.Run(MakeResponse(404, "text/html", "Not implemented yet!"));
}
}
void DevToolsHttpServer::OnReceivedWebSocketRequest(
HttpConnectionDelegateImpl* connection,
mojo::HttpRequestPtr request,
const OnReceivedWebSocketRequestCallback& callback) {
DCHECK(connections_.find(connection) != connections_.end());
std::string path = request->url;
size_t browser_pos = path.find(kBrowserUrlPrefix);
if (browser_pos == 0) {
// TODO(yzshen): Implement it.
NOTIMPLEMENTED();
callback.Run(nullptr, mojo::ScopedDataPipeConsumerHandle(), nullptr);
return;
}
size_t pos = path.find(kPageUrlPrefix);
if (pos != 0) {
callback.Run(nullptr, mojo::ScopedDataPipeConsumerHandle(), nullptr);
return;
}
std::string target_id = path.substr(strlen(kPageUrlPrefix));
DevToolsAgentHost* agent = service_->registry()->GetAgentById(target_id);
if (!agent || agent->IsAttached()) {
callback.Run(nullptr, mojo::ScopedDataPipeConsumerHandle(), nullptr);
return;
}
mojo::WebSocketPtr web_socket;
mojo::InterfaceRequest<mojo::WebSocket> web_socket_request =
mojo::GetProxy(&web_socket);
mojo::DataPipe data_pipe;
mojo::WebSocketClientPtr web_socket_client = WebSocketRelayer::SetUp(
agent, web_socket.Pass(), data_pipe.producer_handle.Pass());
callback.Run(web_socket_request.Pass(), data_pipe.consumer_handle.Pass(),
web_socket_client.Pass());
}
void DevToolsHttpServer::OnConnectionClosed(
HttpConnectionDelegateImpl* connection) {
DCHECK(connections_.find(connection) != connections_.end());
delete connection;
connections_.erase(connection);
}
mojo::HttpResponsePtr DevToolsHttpServer::ProcessJsonRequest(
mojo::HttpRequestPtr request) {
// Trim "/json".
std::string path = request->url.get().substr(strlen(kJsonRequestUrlPrefix));
// Trim query.
size_t query_pos = path.find("?");
if (query_pos != std::string::npos)
path = path.substr(0, query_pos);
// Trim fragment.
size_t fragment_pos = path.find("#");
if (fragment_pos != std::string::npos)
path = path.substr(0, fragment_pos);
std::string command;
std::string target_id;
if (!ParseJsonPath(path, &command, &target_id))
return MakeJsonResponse(404, nullptr,
"Malformed query: " + request->url.get());
if (command == kVersionCommand || command == kNewCommand ||
command == kActivateCommand || command == kCloseCommand) {
NOTIMPLEMENTED();
return MakeJsonResponse(404, nullptr,
"Not implemented yet: " + request->url.get());
}
if (command == kListCommand) {
DevToolsRegistryImpl::Iterator iter(service_->registry());
if (iter.IsAtEnd()) {
// If no agent is available, return a nullptr to indicate that the
// connection should be closed.
return nullptr;
}
std::string host = GetHeaderValue(*request, "host");
if (host.empty()) {
host = base::StringPrintf("127.0.0.1:%u",
static_cast<unsigned>(remote_debugging_port_));
}
base::ListValue list_value;
for (; !iter.IsAtEnd(); iter.Advance()) {
scoped_ptr<base::DictionaryValue> dict_value(new base::DictionaryValue());
// TODO(yzshen): Add more information.
dict_value->SetString(kTargetDescriptionField, std::string());
dict_value->SetString(kTargetDevtoolsFrontendUrlField, std::string());
dict_value->SetString(kTargetIdField, iter.value()->id());
dict_value->SetString(kTargetTitleField, std::string());
dict_value->SetString(kTargetTypeField, "page");
dict_value->SetString(kTargetUrlField, std::string());
dict_value->SetString(
kTargetWebSocketDebuggerUrlField,
base::StringPrintf("ws://%s%s%s", host.c_str(), kPageUrlPrefix,
iter.value()->id().c_str()));
list_value.Append(dict_value.Pass());
}
return MakeJsonResponse(200, &list_value, std::string());
}
return MakeJsonResponse(404, nullptr, "Unknown command: " + command);
}
} // namespace devtools_service