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
,
119 public mojo::ErrorHandler
{
121 // Creates a WebSocketRelayer instance and sets it as the delegate of
124 // The object destroys itself when either of the following happens:
125 // - |agent_host| is dead and the object finishes all pending sends (if any)
126 // to the Web socket; or
127 // - the underlying pipe of |web_socket| is closed and the object finishes all
128 // pending receives (if any) from the Web socket.
129 static mojo::WebSocketClientPtr
SetUp(
130 DevToolsAgentHost
* agent_host
,
131 mojo::WebSocketPtr web_socket
,
132 mojo::ScopedDataPipeProducerHandle send_stream
) {
135 DCHECK(send_stream
.is_valid());
137 mojo::WebSocketClientPtr web_socket_client
;
138 new WebSocketRelayer(agent_host
, web_socket
.Pass(), send_stream
.Pass(),
140 return web_socket_client
.Pass();
144 WebSocketRelayer(DevToolsAgentHost
* agent_host
,
145 mojo::WebSocketPtr web_socket
,
146 mojo::ScopedDataPipeProducerHandle send_stream
,
147 mojo::WebSocketClientPtr
* web_socket_client
)
148 : agent_host_(agent_host
),
149 binding_(this, web_socket_client
),
150 web_socket_(web_socket
.Pass()),
151 send_stream_(send_stream
.Pass()),
152 write_send_stream_(new mojo::WebSocketWriteQueue(send_stream_
.get())),
153 pending_send_count_(0),
154 pending_receive_count_(0) {
155 web_socket_
.set_error_handler(this);
156 agent_host
->SetDelegate(this);
159 ~WebSocketRelayer() override
{
161 agent_host_
->SetDelegate(nullptr);
164 // DevToolsAgentHost::Delegate implementation.
165 void DispatchProtocolMessage(DevToolsAgentHost
* agent_host
,
166 const std::string
& message
) override
{
170 // TODO(yzshen): It shouldn't be an issue to pass an empty message. However,
171 // WebSocket{Read,Write}Queue doesn't handle that correctly.
175 pending_send_count_
++;
176 uint32_t size
= static_cast<uint32_t>(message
.size());
177 write_send_stream_
->Write(
179 base::Bind(&WebSocketRelayer::OnFinishedWritingSendStream
,
180 base::Unretained(this), size
));
183 void OnAgentHostClosed(DevToolsAgentHost
* agent_host
) override
{
184 DispatchProtocolMessage(agent_host_
,
185 "{ \"method\": \"Inspector.detached\", "
186 "\"params\": { \"reason\": \"target_closed\" } }");
188 // No need to call SetDelegate(nullptr) on |agent_host_| because it is going
190 agent_host_
= nullptr;
192 if (ShouldSelfDestruct())
196 // WebSocketClient implementation.
197 void DidConnect(const mojo::String
& selected_subprotocol
,
198 const mojo::String
& extensions
,
199 mojo::ScopedDataPipeConsumerHandle receive_stream
) override
{
200 receive_stream_
= receive_stream
.Pass();
201 read_receive_stream_
.reset(
202 new mojo::WebSocketReadQueue(receive_stream_
.get()));
205 void DidReceiveData(bool fin
,
206 mojo::WebSocket::MessageType type
,
207 uint32_t num_bytes
) override
{
211 // TODO(yzshen): It shouldn't be an issue to pass an empty message. However,
212 // WebSocket{Read,Write}Queue doesn't handle that correctly.
216 pending_receive_count_
++;
217 read_receive_stream_
->Read(
218 num_bytes
, base::Bind(&WebSocketRelayer::OnFinishedReadingReceiveStream
,
219 base::Unretained(this), num_bytes
));
222 void DidReceiveFlowControl(int64_t quota
) override
{}
224 void DidFail(const mojo::String
& message
) override
{}
226 void DidClose(bool was_clean
,
228 const mojo::String
& reason
) override
{}
230 // mojo::ErrorHandler implementation.
231 void OnConnectionError() override
{
232 web_socket_
= nullptr;
235 if (ShouldSelfDestruct())
239 void OnFinishedWritingSendStream(uint32_t num_bytes
, const char* buffer
) {
240 DCHECK_GT(pending_send_count_
, 0u);
241 pending_send_count_
--;
243 if (web_socket_
&& buffer
)
244 web_socket_
->Send(true, mojo::WebSocket::MESSAGE_TYPE_TEXT
, num_bytes
);
246 if (ShouldSelfDestruct())
250 void OnFinishedReadingReceiveStream(uint32_t num_bytes
, const char* data
) {
251 DCHECK_GT(pending_receive_count_
, 0u);
252 pending_receive_count_
--;
254 if (agent_host_
&& data
)
255 agent_host_
->SendProtocolMessageToAgent(std::string(data
, num_bytes
));
257 if (ShouldSelfDestruct())
261 bool ShouldSelfDestruct() const {
262 return (!agent_host_
&& pending_send_count_
== 0) ||
263 (!web_socket_
&& pending_receive_count_
== 0);
266 DevToolsAgentHost
* agent_host_
;
267 mojo::Binding
<WebSocketClient
> binding_
;
268 mojo::WebSocketPtr web_socket_
;
270 mojo::ScopedDataPipeProducerHandle send_stream_
;
271 scoped_ptr
<mojo::WebSocketWriteQueue
> write_send_stream_
;
272 size_t pending_send_count_
;
274 mojo::ScopedDataPipeConsumerHandle receive_stream_
;
275 scoped_ptr
<mojo::WebSocketReadQueue
> read_receive_stream_
;
276 size_t pending_receive_count_
;
278 DISALLOW_COPY_AND_ASSIGN(WebSocketRelayer
);
283 class DevToolsHttpServer::HttpConnectionDelegateImpl
284 : public mojo::HttpConnectionDelegate
,
285 public mojo::ErrorHandler
{
287 HttpConnectionDelegateImpl(
288 DevToolsHttpServer
* owner
,
289 mojo::HttpConnectionPtr connection
,
290 mojo::InterfaceRequest
<HttpConnectionDelegate
> delegate_request
)
292 connection_(connection
.Pass()),
293 binding_(this, delegate_request
.Pass()) {
296 DCHECK(binding_
.is_bound());
298 connection_
.set_error_handler(this);
299 binding_
.set_error_handler(this);
302 mojo::HttpConnection
* connection() { return connection_
.get(); }
305 // mojo::HttpConnectionDelegate implementation:
306 void OnReceivedRequest(mojo::HttpRequestPtr request
,
307 const OnReceivedRequestCallback
& callback
) override
{
308 owner_
->OnReceivedRequest(this, request
.Pass(), callback
);
311 void OnReceivedWebSocketRequest(
312 mojo::HttpRequestPtr request
,
313 const OnReceivedWebSocketRequestCallback
& callback
) override
{
314 owner_
->OnReceivedWebSocketRequest(this, request
.Pass(), callback
);
317 // mojo::ErrorHandler implementation.
318 void OnConnectionError() override
{ owner_
->OnConnectionClosed(this); }
320 DevToolsHttpServer
* const owner_
;
321 mojo::HttpConnectionPtr connection_
;
322 mojo::Binding
<HttpConnectionDelegate
> binding_
;
324 DISALLOW_COPY_AND_ASSIGN(HttpConnectionDelegateImpl
);
327 DevToolsHttpServer::DevToolsHttpServer(DevToolsService
* service
,
328 uint16_t remote_debugging_port
)
329 : service_(service
), remote_debugging_port_(remote_debugging_port
) {
330 VLOG(1) << "Remote debugging HTTP server is started on port "
331 << remote_debugging_port
<< ".";
332 mojo::NetworkServicePtr network_service
;
333 mojo::URLRequestPtr
request(mojo::URLRequest::New());
334 request
->url
= "mojo:network_service";
335 service_
->application()->ConnectToService(request
.Pass(), &network_service
);
337 mojo::NetAddressPtr
local_address(mojo::NetAddress::New());
338 local_address
->family
= mojo::NET_ADDRESS_FAMILY_IPV4
;
339 local_address
->ipv4
= mojo::NetAddressIPv4::New();
340 local_address
->ipv4
->port
= remote_debugging_port
;
341 local_address
->ipv4
->addr
.resize(4);
342 local_address
->ipv4
->addr
[0] = 127;
343 local_address
->ipv4
->addr
[1] = 0;
344 local_address
->ipv4
->addr
[2] = 0;
345 local_address
->ipv4
->addr
[3] = 1;
347 mojo::HttpServerDelegatePtr http_server_delegate
;
348 http_server_delegate_binding_
.reset(
349 new mojo::Binding
<mojo::HttpServerDelegate
>(this, &http_server_delegate
));
350 network_service
->CreateHttpServer(
351 local_address
.Pass(), http_server_delegate
.Pass(),
352 mojo::NetworkService::CreateHttpServerCallback());
355 DevToolsHttpServer::~DevToolsHttpServer() {
356 STLDeleteElements(&connections_
);
359 void DevToolsHttpServer::OnConnected(
360 mojo::HttpConnectionPtr connection
,
361 mojo::InterfaceRequest
<mojo::HttpConnectionDelegate
> delegate
) {
363 new HttpConnectionDelegateImpl(this, connection
.Pass(), delegate
.Pass()));
366 void DevToolsHttpServer::OnReceivedRequest(
367 HttpConnectionDelegateImpl
* connection
,
368 mojo::HttpRequestPtr request
,
369 const OnReceivedRequestCallback
& callback
) {
370 DCHECK(connections_
.find(connection
) != connections_
.end());
372 if (request
->url
.get().find(kJsonRequestUrlPrefix
) == 0) {
373 callback
.Run(ProcessJsonRequest(request
.Pass()));
375 // TODO(yzshen): Implement it.
377 callback
.Run(MakeResponse(404, "text/html", "Not implemented yet!"));
381 void DevToolsHttpServer::OnReceivedWebSocketRequest(
382 HttpConnectionDelegateImpl
* connection
,
383 mojo::HttpRequestPtr request
,
384 const OnReceivedWebSocketRequestCallback
& callback
) {
385 DCHECK(connections_
.find(connection
) != connections_
.end());
387 std::string path
= request
->url
;
388 size_t browser_pos
= path
.find(kBrowserUrlPrefix
);
389 if (browser_pos
== 0) {
390 // TODO(yzshen): Implement it.
392 callback
.Run(nullptr, mojo::ScopedDataPipeConsumerHandle(), nullptr);
396 size_t pos
= path
.find(kPageUrlPrefix
);
398 callback
.Run(nullptr, mojo::ScopedDataPipeConsumerHandle(), nullptr);
402 std::string target_id
= path
.substr(strlen(kPageUrlPrefix
));
403 DevToolsAgentHost
* agent
= service_
->registry()->GetAgentById(target_id
);
404 if (!agent
|| agent
->IsAttached()) {
405 callback
.Run(nullptr, mojo::ScopedDataPipeConsumerHandle(), nullptr);
409 mojo::WebSocketPtr web_socket
;
410 mojo::InterfaceRequest
<mojo::WebSocket
> web_socket_request
=
411 mojo::GetProxy(&web_socket
);
412 mojo::DataPipe data_pipe
;
413 mojo::WebSocketClientPtr web_socket_client
= WebSocketRelayer::SetUp(
414 agent
, web_socket
.Pass(), data_pipe
.producer_handle
.Pass());
415 callback
.Run(web_socket_request
.Pass(), data_pipe
.consumer_handle
.Pass(),
416 web_socket_client
.Pass());
419 void DevToolsHttpServer::OnConnectionClosed(
420 HttpConnectionDelegateImpl
* connection
) {
421 DCHECK(connections_
.find(connection
) != connections_
.end());
424 connections_
.erase(connection
);
427 mojo::HttpResponsePtr
DevToolsHttpServer::ProcessJsonRequest(
428 mojo::HttpRequestPtr request
) {
430 std::string path
= request
->url
.get().substr(strlen(kJsonRequestUrlPrefix
));
433 size_t query_pos
= path
.find("?");
434 if (query_pos
!= std::string::npos
)
435 path
= path
.substr(0, query_pos
);
438 size_t fragment_pos
= path
.find("#");
439 if (fragment_pos
!= std::string::npos
)
440 path
= path
.substr(0, fragment_pos
);
443 std::string target_id
;
444 if (!ParseJsonPath(path
, &command
, &target_id
))
445 return MakeJsonResponse(404, nullptr,
446 "Malformed query: " + request
->url
.get());
448 if (command
== kVersionCommand
|| command
== kNewCommand
||
449 command
== kActivateCommand
|| command
== kCloseCommand
) {
451 return MakeJsonResponse(404, nullptr,
452 "Not implemented yet: " + request
->url
.get());
455 if (command
== kListCommand
) {
456 base::ListValue list_value
;
457 for (DevToolsRegistryImpl::Iterator
iter(service_
->registry());
458 !iter
.IsAtEnd(); iter
.Advance()) {
459 scoped_ptr
<base::DictionaryValue
> dict_value(new base::DictionaryValue());
461 // TODO(yzshen): Add more information.
462 dict_value
->SetString(kTargetDescriptionField
, std::string());
463 dict_value
->SetString(kTargetDevtoolsFrontendUrlField
, std::string());
464 dict_value
->SetString(kTargetIdField
, iter
.value()->id());
465 dict_value
->SetString(kTargetTitleField
, std::string());
466 dict_value
->SetString(kTargetTypeField
, "page");
467 dict_value
->SetString(kTargetUrlField
, std::string());
468 dict_value
->SetString(
469 kTargetWebSocketDebuggerUrlField
,
470 base::StringPrintf("ws://127.0.0.1:%u%s%s",
471 static_cast<unsigned>(remote_debugging_port_
),
472 kPageUrlPrefix
, iter
.value()->id().c_str()));
473 list_value
.Append(dict_value
.Pass());
475 return MakeJsonResponse(200, &list_value
, std::string());
478 return MakeJsonResponse(404, nullptr, "Unknown command: " + command
);
481 } // namespace devtools_service