1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "components/devtools_service/devtools_http_server.h"
11 #include "base/bind.h"
12 #include "base/json/json_writer.h"
13 #include "base/logging.h"
14 #include "base/stl_util.h"
15 #include "base/strings/stringprintf.h"
16 #include "base/values.h"
17 #include "components/devtools_service/devtools_agent_host.h"
18 #include "components/devtools_service/devtools_registry_impl.h"
19 #include "components/devtools_service/devtools_service.h"
20 #include "mojo/application/public/cpp/application_impl.h"
21 #include "mojo/services/network/public/cpp/web_socket_read_queue.h"
22 #include "mojo/services/network/public/cpp/web_socket_write_queue.h"
23 #include "mojo/services/network/public/interfaces/net_address.mojom.h"
24 #include "mojo/services/network/public/interfaces/network_service.mojom.h"
25 #include "mojo/services/network/public/interfaces/web_socket.mojom.h"
26 #include "third_party/mojo/src/mojo/public/cpp/system/data_pipe.h"
28 namespace devtools_service
{
32 const char kPageUrlPrefix
[] = "/devtools/page/";
33 const char kBrowserUrlPrefix
[] = "/devtools/browser";
34 const char kJsonRequestUrlPrefix
[] = "/json";
36 const char kActivateCommand
[] = "activate";
37 const char kCloseCommand
[] = "close";
38 const char kListCommand
[] = "list";
39 const char kNewCommand
[] = "new";
40 const char kVersionCommand
[] = "version";
42 const char kTargetIdField
[] = "id";
43 const char kTargetTypeField
[] = "type";
44 const char kTargetTitleField
[] = "title";
45 const char kTargetDescriptionField
[] = "description";
46 const char kTargetUrlField
[] = "url";
47 const char kTargetWebSocketDebuggerUrlField
[] = "webSocketDebuggerUrl";
48 const char kTargetDevtoolsFrontendUrlField
[] = "devtoolsFrontendUrl";
50 bool ParseJsonPath(const std::string
& path
,
52 std::string
* target_id
) {
53 // Fall back to list in case of empty query.
55 *command
= kListCommand
;
59 if (path
.find("/") != 0) {
63 *command
= path
.substr(1);
65 size_t separator_pos
= command
->find("/");
66 if (separator_pos
!= std::string::npos
) {
67 *target_id
= command
->substr(separator_pos
+ 1);
68 *command
= command
->substr(0, separator_pos
);
73 mojo::HttpResponsePtr
MakeResponse(uint32_t status_code
,
74 const std::string
& content_type
,
75 const std::string
& body
) {
76 mojo::HttpResponsePtr
response(mojo::HttpResponse::New());
77 response
->headers
.resize(2);
78 response
->headers
[0] = mojo::HttpHeader::New();
79 response
->headers
[0]->name
= "Content-Length";
80 response
->headers
[0]->value
=
81 base::StringPrintf("%lu", static_cast<unsigned long>(body
.size()));
82 response
->headers
[1] = mojo::HttpHeader::New();
83 response
->headers
[1]->name
= "Content-Type";
84 response
->headers
[1]->value
= content_type
;
87 uint32_t num_bytes
= static_cast<uint32_t>(body
.size());
88 MojoCreateDataPipeOptions options
;
89 options
.struct_size
= sizeof(MojoCreateDataPipeOptions
);
90 options
.flags
= MOJO_CREATE_DATA_PIPE_OPTIONS_FLAG_NONE
;
91 options
.element_num_bytes
= 1;
92 options
.capacity_num_bytes
= num_bytes
;
93 mojo::DataPipe
data_pipe(options
);
94 response
->body
= data_pipe
.consumer_handle
.Pass();
96 WriteDataRaw(data_pipe
.producer_handle
.get(), body
.data(), &num_bytes
,
97 MOJO_WRITE_DATA_FLAG_ALL_OR_NONE
);
98 CHECK_EQ(MOJO_RESULT_OK
, result
);
100 return response
.Pass();
103 mojo::HttpResponsePtr
MakeJsonResponse(uint32_t status_code
,
105 const std::string
& message
) {
106 // Serialize value and message.
107 std::string json_value
;
109 base::JSONWriter::WriteWithOptions(
110 *value
, base::JSONWriter::OPTIONS_PRETTY_PRINT
, &json_value
);
113 return MakeResponse(status_code
, "application/json; charset=UTF-8",
114 json_value
+ message
);
117 class WebSocketRelayer
: public DevToolsAgentHost::Delegate
,
118 public mojo::WebSocketClient
{
120 // Creates a WebSocketRelayer instance and sets it as the delegate of
123 // The object destroys itself when either of the following happens:
124 // - |agent_host| is dead and the object finishes all pending sends (if any)
125 // to the Web socket; or
126 // - the underlying pipe of |web_socket| is closed and the object finishes all
127 // pending receives (if any) from the Web socket.
128 static mojo::WebSocketClientPtr
SetUp(
129 DevToolsAgentHost
* agent_host
,
130 mojo::WebSocketPtr web_socket
,
131 mojo::ScopedDataPipeProducerHandle send_stream
) {
134 DCHECK(send_stream
.is_valid());
136 mojo::WebSocketClientPtr web_socket_client
;
137 new WebSocketRelayer(agent_host
, web_socket
.Pass(), send_stream
.Pass(),
139 return web_socket_client
.Pass();
143 WebSocketRelayer(DevToolsAgentHost
* agent_host
,
144 mojo::WebSocketPtr web_socket
,
145 mojo::ScopedDataPipeProducerHandle send_stream
,
146 mojo::WebSocketClientPtr
* web_socket_client
)
147 : agent_host_(agent_host
),
148 binding_(this, web_socket_client
),
149 web_socket_(web_socket
.Pass()),
150 send_stream_(send_stream
.Pass()),
151 write_send_stream_(new mojo::WebSocketWriteQueue(send_stream_
.get())),
152 pending_send_count_(0),
153 pending_receive_count_(0) {
154 web_socket_
.set_connection_error_handler([this]() { OnConnectionError(); });
155 agent_host
->SetDelegate(this);
158 ~WebSocketRelayer() override
{
160 agent_host_
->SetDelegate(nullptr);
163 // DevToolsAgentHost::Delegate implementation.
164 void DispatchProtocolMessage(DevToolsAgentHost
* agent_host
,
165 const std::string
& message
) override
{
169 // TODO(yzshen): It shouldn't be an issue to pass an empty message. However,
170 // WebSocket{Read,Write}Queue doesn't handle that correctly.
174 pending_send_count_
++;
175 uint32_t size
= static_cast<uint32_t>(message
.size());
176 write_send_stream_
->Write(
178 base::Bind(&WebSocketRelayer::OnFinishedWritingSendStream
,
179 base::Unretained(this), size
));
182 void OnAgentHostClosed(DevToolsAgentHost
* agent_host
) override
{
183 DispatchProtocolMessage(agent_host_
,
184 "{ \"method\": \"Inspector.detached\", "
185 "\"params\": { \"reason\": \"target_closed\" } }");
187 // No need to call SetDelegate(nullptr) on |agent_host_| because it is going
189 agent_host_
= nullptr;
191 if (ShouldSelfDestruct())
195 // WebSocketClient implementation.
196 void DidConnect(const mojo::String
& selected_subprotocol
,
197 const mojo::String
& extensions
,
198 mojo::ScopedDataPipeConsumerHandle receive_stream
) override
{
199 receive_stream_
= receive_stream
.Pass();
200 read_receive_stream_
.reset(
201 new mojo::WebSocketReadQueue(receive_stream_
.get()));
204 void DidReceiveData(bool fin
,
205 mojo::WebSocket::MessageType type
,
206 uint32_t num_bytes
) override
{
210 // TODO(yzshen): It shouldn't be an issue to pass an empty message. However,
211 // WebSocket{Read,Write}Queue doesn't handle that correctly.
215 pending_receive_count_
++;
216 read_receive_stream_
->Read(
217 num_bytes
, base::Bind(&WebSocketRelayer::OnFinishedReadingReceiveStream
,
218 base::Unretained(this), num_bytes
));
221 void DidReceiveFlowControl(int64_t quota
) override
{}
223 void DidFail(const mojo::String
& message
) override
{}
225 void DidClose(bool was_clean
,
227 const mojo::String
& reason
) override
{}
229 void OnConnectionError() {
230 web_socket_
= nullptr;
233 if (ShouldSelfDestruct())
237 void OnFinishedWritingSendStream(uint32_t num_bytes
, const char* buffer
) {
238 DCHECK_GT(pending_send_count_
, 0u);
239 pending_send_count_
--;
241 if (web_socket_
&& buffer
)
242 web_socket_
->Send(true, mojo::WebSocket::MESSAGE_TYPE_TEXT
, num_bytes
);
244 if (ShouldSelfDestruct())
248 void OnFinishedReadingReceiveStream(uint32_t num_bytes
, const char* data
) {
249 DCHECK_GT(pending_receive_count_
, 0u);
250 pending_receive_count_
--;
252 if (agent_host_
&& data
)
253 agent_host_
->SendProtocolMessageToAgent(std::string(data
, num_bytes
));
255 if (ShouldSelfDestruct())
259 bool ShouldSelfDestruct() const {
260 return (!agent_host_
&& pending_send_count_
== 0) ||
261 (!web_socket_
&& pending_receive_count_
== 0);
264 DevToolsAgentHost
* agent_host_
;
265 mojo::Binding
<WebSocketClient
> binding_
;
266 mojo::WebSocketPtr web_socket_
;
268 mojo::ScopedDataPipeProducerHandle send_stream_
;
269 scoped_ptr
<mojo::WebSocketWriteQueue
> write_send_stream_
;
270 size_t pending_send_count_
;
272 mojo::ScopedDataPipeConsumerHandle receive_stream_
;
273 scoped_ptr
<mojo::WebSocketReadQueue
> read_receive_stream_
;
274 size_t pending_receive_count_
;
276 DISALLOW_COPY_AND_ASSIGN(WebSocketRelayer
);
281 class DevToolsHttpServer::HttpConnectionDelegateImpl
282 : public mojo::HttpConnectionDelegate
{
284 HttpConnectionDelegateImpl(
285 DevToolsHttpServer
* owner
,
286 mojo::HttpConnectionPtr connection
,
287 mojo::InterfaceRequest
<HttpConnectionDelegate
> delegate_request
)
289 connection_(connection
.Pass()),
290 binding_(this, delegate_request
.Pass()) {
293 DCHECK(binding_
.is_bound());
295 auto error_handler
= [this]() { owner_
->OnConnectionClosed(this); };
296 connection_
.set_connection_error_handler(error_handler
);
297 binding_
.set_connection_error_handler(error_handler
);
300 mojo::HttpConnection
* connection() { return connection_
.get(); }
303 // mojo::HttpConnectionDelegate implementation:
304 void OnReceivedRequest(mojo::HttpRequestPtr request
,
305 const OnReceivedRequestCallback
& callback
) override
{
306 owner_
->OnReceivedRequest(this, request
.Pass(), callback
);
309 void OnReceivedWebSocketRequest(
310 mojo::HttpRequestPtr request
,
311 const OnReceivedWebSocketRequestCallback
& callback
) override
{
312 owner_
->OnReceivedWebSocketRequest(this, request
.Pass(), callback
);
315 DevToolsHttpServer
* const owner_
;
316 mojo::HttpConnectionPtr connection_
;
317 mojo::Binding
<HttpConnectionDelegate
> binding_
;
319 DISALLOW_COPY_AND_ASSIGN(HttpConnectionDelegateImpl
);
322 DevToolsHttpServer::DevToolsHttpServer(DevToolsService
* service
,
323 uint16_t remote_debugging_port
)
324 : service_(service
), remote_debugging_port_(remote_debugging_port
) {
325 VLOG(1) << "Remote debugging HTTP server is started on port "
326 << remote_debugging_port
<< ".";
327 mojo::NetworkServicePtr network_service
;
328 mojo::URLRequestPtr
request(mojo::URLRequest::New());
329 request
->url
= "mojo:network_service";
330 service_
->application()->ConnectToService(request
.Pass(), &network_service
);
332 mojo::NetAddressPtr
local_address(mojo::NetAddress::New());
333 local_address
->family
= mojo::NET_ADDRESS_FAMILY_IPV4
;
334 local_address
->ipv4
= mojo::NetAddressIPv4::New();
335 local_address
->ipv4
->port
= remote_debugging_port
;
336 local_address
->ipv4
->addr
.resize(4);
337 local_address
->ipv4
->addr
[0] = 127;
338 local_address
->ipv4
->addr
[1] = 0;
339 local_address
->ipv4
->addr
[2] = 0;
340 local_address
->ipv4
->addr
[3] = 1;
342 mojo::HttpServerDelegatePtr http_server_delegate
;
343 http_server_delegate_binding_
.reset(
344 new mojo::Binding
<mojo::HttpServerDelegate
>(this, &http_server_delegate
));
345 network_service
->CreateHttpServer(
346 local_address
.Pass(), http_server_delegate
.Pass(),
347 mojo::NetworkService::CreateHttpServerCallback());
350 DevToolsHttpServer::~DevToolsHttpServer() {
351 STLDeleteElements(&connections_
);
354 void DevToolsHttpServer::OnConnected(
355 mojo::HttpConnectionPtr connection
,
356 mojo::InterfaceRequest
<mojo::HttpConnectionDelegate
> delegate
) {
358 new HttpConnectionDelegateImpl(this, connection
.Pass(), delegate
.Pass()));
361 void DevToolsHttpServer::OnReceivedRequest(
362 HttpConnectionDelegateImpl
* connection
,
363 mojo::HttpRequestPtr request
,
364 const OnReceivedRequestCallback
& callback
) {
365 DCHECK(connections_
.find(connection
) != connections_
.end());
367 if (request
->url
.get().find(kJsonRequestUrlPrefix
) == 0) {
368 mojo::HttpResponsePtr response
= ProcessJsonRequest(request
.Pass());
370 callback
.Run(response
.Pass());
372 OnConnectionClosed(connection
);
374 // TODO(yzshen): Implement it.
376 callback
.Run(MakeResponse(404, "text/html", "Not implemented yet!"));
380 void DevToolsHttpServer::OnReceivedWebSocketRequest(
381 HttpConnectionDelegateImpl
* connection
,
382 mojo::HttpRequestPtr request
,
383 const OnReceivedWebSocketRequestCallback
& callback
) {
384 DCHECK(connections_
.find(connection
) != connections_
.end());
386 std::string path
= request
->url
;
387 size_t browser_pos
= path
.find(kBrowserUrlPrefix
);
388 if (browser_pos
== 0) {
389 // TODO(yzshen): Implement it.
391 callback
.Run(nullptr, mojo::ScopedDataPipeConsumerHandle(), nullptr);
395 size_t pos
= path
.find(kPageUrlPrefix
);
397 callback
.Run(nullptr, mojo::ScopedDataPipeConsumerHandle(), nullptr);
401 std::string target_id
= path
.substr(strlen(kPageUrlPrefix
));
402 DevToolsAgentHost
* agent
= service_
->registry()->GetAgentById(target_id
);
403 if (!agent
|| agent
->IsAttached()) {
404 callback
.Run(nullptr, mojo::ScopedDataPipeConsumerHandle(), nullptr);
408 mojo::WebSocketPtr web_socket
;
409 mojo::InterfaceRequest
<mojo::WebSocket
> web_socket_request
=
410 mojo::GetProxy(&web_socket
);
411 mojo::DataPipe data_pipe
;
412 mojo::WebSocketClientPtr web_socket_client
= WebSocketRelayer::SetUp(
413 agent
, web_socket
.Pass(), data_pipe
.producer_handle
.Pass());
414 callback
.Run(web_socket_request
.Pass(), data_pipe
.consumer_handle
.Pass(),
415 web_socket_client
.Pass());
418 void DevToolsHttpServer::OnConnectionClosed(
419 HttpConnectionDelegateImpl
* connection
) {
420 DCHECK(connections_
.find(connection
) != connections_
.end());
423 connections_
.erase(connection
);
426 mojo::HttpResponsePtr
DevToolsHttpServer::ProcessJsonRequest(
427 mojo::HttpRequestPtr request
) {
429 std::string path
= request
->url
.get().substr(strlen(kJsonRequestUrlPrefix
));
432 size_t query_pos
= path
.find("?");
433 if (query_pos
!= std::string::npos
)
434 path
= path
.substr(0, query_pos
);
437 size_t fragment_pos
= path
.find("#");
438 if (fragment_pos
!= std::string::npos
)
439 path
= path
.substr(0, fragment_pos
);
442 std::string target_id
;
443 if (!ParseJsonPath(path
, &command
, &target_id
))
444 return MakeJsonResponse(404, nullptr,
445 "Malformed query: " + request
->url
.get());
447 if (command
== kVersionCommand
|| command
== kNewCommand
||
448 command
== kActivateCommand
|| command
== kCloseCommand
) {
450 return MakeJsonResponse(404, nullptr,
451 "Not implemented yet: " + request
->url
.get());
454 if (command
== kListCommand
) {
455 DevToolsRegistryImpl::Iterator
iter(service_
->registry());
456 if (iter
.IsAtEnd()) {
457 // If no agent is available, return a nullptr to indicate that the
458 // connection should be closed.
462 base::ListValue list_value
;
463 for (; !iter
.IsAtEnd(); iter
.Advance()) {
464 scoped_ptr
<base::DictionaryValue
> dict_value(new base::DictionaryValue());
466 // TODO(yzshen): Add more information.
467 dict_value
->SetString(kTargetDescriptionField
, std::string());
468 dict_value
->SetString(kTargetDevtoolsFrontendUrlField
, std::string());
469 dict_value
->SetString(kTargetIdField
, iter
.value()->id());
470 dict_value
->SetString(kTargetTitleField
, std::string());
471 dict_value
->SetString(kTargetTypeField
, "page");
472 dict_value
->SetString(kTargetUrlField
, std::string());
473 dict_value
->SetString(
474 kTargetWebSocketDebuggerUrlField
,
475 base::StringPrintf("ws://127.0.0.1:%u%s%s",
476 static_cast<unsigned>(remote_debugging_port_
),
477 kPageUrlPrefix
, iter
.value()->id().c_str()));
478 list_value
.Append(dict_value
.Pass());
480 return MakeJsonResponse(200, &list_value
, std::string());
483 return MakeJsonResponse(404, nullptr, "Unknown command: " + command
);
486 } // namespace devtools_service