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 std::string
GetHeaderValue(const mojo::HttpRequest
& request
,
51 const std::string
& name
) {
52 for (size_t i
= 0; i
< request
.headers
.size(); ++i
) {
53 if (name
== request
.headers
[i
]->name
)
54 return request
.headers
[i
]->value
;
60 bool ParseJsonPath(const std::string
& path
,
62 std::string
* target_id
) {
63 // Fall back to list in case of empty query.
65 *command
= kListCommand
;
69 if (path
.find("/") != 0) {
73 *command
= path
.substr(1);
75 size_t separator_pos
= command
->find("/");
76 if (separator_pos
!= std::string::npos
) {
77 *target_id
= command
->substr(separator_pos
+ 1);
78 *command
= command
->substr(0, separator_pos
);
83 mojo::HttpResponsePtr
MakeResponse(uint32_t status_code
,
84 const std::string
& content_type
,
85 const std::string
& body
) {
86 mojo::HttpResponsePtr
response(mojo::HttpResponse::New());
87 response
->headers
.resize(2);
88 response
->headers
[0] = mojo::HttpHeader::New();
89 response
->headers
[0]->name
= "Content-Length";
90 response
->headers
[0]->value
=
91 base::StringPrintf("%lu", static_cast<unsigned long>(body
.size()));
92 response
->headers
[1] = mojo::HttpHeader::New();
93 response
->headers
[1]->name
= "Content-Type";
94 response
->headers
[1]->value
= content_type
;
97 uint32_t num_bytes
= static_cast<uint32_t>(body
.size());
98 MojoCreateDataPipeOptions options
;
99 options
.struct_size
= sizeof(MojoCreateDataPipeOptions
);
100 options
.flags
= MOJO_CREATE_DATA_PIPE_OPTIONS_FLAG_NONE
;
101 options
.element_num_bytes
= 1;
102 options
.capacity_num_bytes
= num_bytes
;
103 mojo::DataPipe
data_pipe(options
);
104 response
->body
= data_pipe
.consumer_handle
.Pass();
106 WriteDataRaw(data_pipe
.producer_handle
.get(), body
.data(), &num_bytes
,
107 MOJO_WRITE_DATA_FLAG_ALL_OR_NONE
);
108 CHECK_EQ(MOJO_RESULT_OK
, result
);
110 return response
.Pass();
113 mojo::HttpResponsePtr
MakeJsonResponse(uint32_t status_code
,
115 const std::string
& message
) {
116 // Serialize value and message.
117 std::string json_value
;
119 base::JSONWriter::WriteWithOptions(
120 *value
, base::JSONWriter::OPTIONS_PRETTY_PRINT
, &json_value
);
123 return MakeResponse(status_code
, "application/json; charset=UTF-8",
124 json_value
+ message
);
127 class WebSocketRelayer
: public DevToolsAgentHost::Delegate
,
128 public mojo::WebSocketClient
{
130 // Creates a WebSocketRelayer instance and sets it as the delegate of
133 // The object destroys itself when either of the following happens:
134 // - |agent_host| is dead and the object finishes all pending sends (if any)
135 // to the Web socket; or
136 // - the underlying pipe of |web_socket| is closed and the object finishes all
137 // pending receives (if any) from the Web socket.
138 static mojo::WebSocketClientPtr
SetUp(
139 DevToolsAgentHost
* agent_host
,
140 mojo::WebSocketPtr web_socket
,
141 mojo::ScopedDataPipeProducerHandle send_stream
) {
144 DCHECK(send_stream
.is_valid());
146 mojo::WebSocketClientPtr web_socket_client
;
147 new WebSocketRelayer(agent_host
, web_socket
.Pass(), send_stream
.Pass(),
149 return web_socket_client
.Pass();
153 WebSocketRelayer(DevToolsAgentHost
* agent_host
,
154 mojo::WebSocketPtr web_socket
,
155 mojo::ScopedDataPipeProducerHandle send_stream
,
156 mojo::WebSocketClientPtr
* web_socket_client
)
157 : agent_host_(agent_host
),
158 binding_(this, web_socket_client
),
159 web_socket_(web_socket
.Pass()),
160 send_stream_(send_stream
.Pass()),
161 write_send_stream_(new mojo::WebSocketWriteQueue(send_stream_
.get())),
162 pending_send_count_(0),
163 pending_receive_count_(0) {
164 web_socket_
.set_connection_error_handler([this]() { OnConnectionError(); });
165 agent_host
->SetDelegate(this);
168 ~WebSocketRelayer() override
{
170 agent_host_
->SetDelegate(nullptr);
173 // DevToolsAgentHost::Delegate implementation.
174 void DispatchProtocolMessage(DevToolsAgentHost
* agent_host
,
175 const std::string
& message
) override
{
179 // TODO(yzshen): It shouldn't be an issue to pass an empty message. However,
180 // WebSocket{Read,Write}Queue doesn't handle that correctly.
184 pending_send_count_
++;
185 uint32_t size
= static_cast<uint32_t>(message
.size());
186 write_send_stream_
->Write(
188 base::Bind(&WebSocketRelayer::OnFinishedWritingSendStream
,
189 base::Unretained(this), size
));
192 void OnAgentHostClosed(DevToolsAgentHost
* agent_host
) override
{
193 DispatchProtocolMessage(agent_host_
,
194 "{ \"method\": \"Inspector.detached\", "
195 "\"params\": { \"reason\": \"target_closed\" } }");
197 // No need to call SetDelegate(nullptr) on |agent_host_| because it is going
199 agent_host_
= nullptr;
201 if (ShouldSelfDestruct())
205 // WebSocketClient implementation.
206 void DidConnect(const mojo::String
& selected_subprotocol
,
207 const mojo::String
& extensions
,
208 mojo::ScopedDataPipeConsumerHandle receive_stream
) override
{
209 receive_stream_
= receive_stream
.Pass();
210 read_receive_stream_
.reset(
211 new mojo::WebSocketReadQueue(receive_stream_
.get()));
214 void DidReceiveData(bool fin
,
215 mojo::WebSocket::MessageType type
,
216 uint32_t num_bytes
) override
{
220 // TODO(yzshen): It shouldn't be an issue to pass an empty message. However,
221 // WebSocket{Read,Write}Queue doesn't handle that correctly.
225 pending_receive_count_
++;
226 read_receive_stream_
->Read(
227 num_bytes
, base::Bind(&WebSocketRelayer::OnFinishedReadingReceiveStream
,
228 base::Unretained(this), num_bytes
));
231 void DidReceiveFlowControl(int64_t quota
) override
{}
233 void DidFail(const mojo::String
& message
) override
{}
235 void DidClose(bool was_clean
,
237 const mojo::String
& reason
) override
{}
239 void OnConnectionError() {
240 web_socket_
= nullptr;
243 if (ShouldSelfDestruct())
247 void OnFinishedWritingSendStream(uint32_t num_bytes
, const char* buffer
) {
248 DCHECK_GT(pending_send_count_
, 0u);
249 pending_send_count_
--;
251 if (web_socket_
&& buffer
)
252 web_socket_
->Send(true, mojo::WebSocket::MESSAGE_TYPE_TEXT
, num_bytes
);
254 if (ShouldSelfDestruct())
258 void OnFinishedReadingReceiveStream(uint32_t num_bytes
, const char* data
) {
259 DCHECK_GT(pending_receive_count_
, 0u);
260 pending_receive_count_
--;
262 if (agent_host_
&& data
)
263 agent_host_
->SendProtocolMessageToAgent(std::string(data
, num_bytes
));
265 if (ShouldSelfDestruct())
269 bool ShouldSelfDestruct() const {
270 return (!agent_host_
&& pending_send_count_
== 0) ||
271 (!web_socket_
&& pending_receive_count_
== 0);
274 DevToolsAgentHost
* agent_host_
;
275 mojo::Binding
<WebSocketClient
> binding_
;
276 mojo::WebSocketPtr web_socket_
;
278 mojo::ScopedDataPipeProducerHandle send_stream_
;
279 scoped_ptr
<mojo::WebSocketWriteQueue
> write_send_stream_
;
280 size_t pending_send_count_
;
282 mojo::ScopedDataPipeConsumerHandle receive_stream_
;
283 scoped_ptr
<mojo::WebSocketReadQueue
> read_receive_stream_
;
284 size_t pending_receive_count_
;
286 DISALLOW_COPY_AND_ASSIGN(WebSocketRelayer
);
291 class DevToolsHttpServer::HttpConnectionDelegateImpl
292 : public mojo::HttpConnectionDelegate
{
294 HttpConnectionDelegateImpl(
295 DevToolsHttpServer
* owner
,
296 mojo::HttpConnectionPtr connection
,
297 mojo::InterfaceRequest
<HttpConnectionDelegate
> delegate_request
)
299 connection_(connection
.Pass()),
300 binding_(this, delegate_request
.Pass()) {
303 DCHECK(binding_
.is_bound());
305 auto error_handler
= [this]() { owner_
->OnConnectionClosed(this); };
306 connection_
.set_connection_error_handler(error_handler
);
307 binding_
.set_connection_error_handler(error_handler
);
310 mojo::HttpConnection
* connection() { return connection_
.get(); }
313 // mojo::HttpConnectionDelegate implementation:
314 void OnReceivedRequest(mojo::HttpRequestPtr request
,
315 const OnReceivedRequestCallback
& callback
) override
{
316 owner_
->OnReceivedRequest(this, request
.Pass(), callback
);
319 void OnReceivedWebSocketRequest(
320 mojo::HttpRequestPtr request
,
321 const OnReceivedWebSocketRequestCallback
& callback
) override
{
322 owner_
->OnReceivedWebSocketRequest(this, request
.Pass(), callback
);
325 DevToolsHttpServer
* const owner_
;
326 mojo::HttpConnectionPtr connection_
;
327 mojo::Binding
<HttpConnectionDelegate
> binding_
;
329 DISALLOW_COPY_AND_ASSIGN(HttpConnectionDelegateImpl
);
332 DevToolsHttpServer::DevToolsHttpServer(DevToolsService
* service
,
333 uint16_t remote_debugging_port
)
334 : service_(service
), remote_debugging_port_(remote_debugging_port
) {
335 VLOG(1) << "Remote debugging HTTP server is started on port "
336 << remote_debugging_port
<< ".";
337 mojo::NetworkServicePtr network_service
;
338 mojo::URLRequestPtr
request(mojo::URLRequest::New());
339 request
->url
= "mojo:network_service";
340 service_
->application()->ConnectToService(request
.Pass(), &network_service
);
342 mojo::NetAddressPtr
local_address(mojo::NetAddress::New());
343 local_address
->family
= mojo::NET_ADDRESS_FAMILY_IPV4
;
344 local_address
->ipv4
= mojo::NetAddressIPv4::New();
345 local_address
->ipv4
->port
= remote_debugging_port
;
346 local_address
->ipv4
->addr
.resize(4);
347 local_address
->ipv4
->addr
[0] = 127;
348 local_address
->ipv4
->addr
[1] = 0;
349 local_address
->ipv4
->addr
[2] = 0;
350 local_address
->ipv4
->addr
[3] = 1;
352 mojo::HttpServerDelegatePtr http_server_delegate
;
353 http_server_delegate_binding_
.reset(
354 new mojo::Binding
<mojo::HttpServerDelegate
>(this, &http_server_delegate
));
355 network_service
->CreateHttpServer(
356 local_address
.Pass(), http_server_delegate
.Pass(),
357 mojo::NetworkService::CreateHttpServerCallback());
360 DevToolsHttpServer::~DevToolsHttpServer() {
361 STLDeleteElements(&connections_
);
364 void DevToolsHttpServer::OnConnected(
365 mojo::HttpConnectionPtr connection
,
366 mojo::InterfaceRequest
<mojo::HttpConnectionDelegate
> delegate
) {
368 new HttpConnectionDelegateImpl(this, connection
.Pass(), delegate
.Pass()));
371 void DevToolsHttpServer::OnReceivedRequest(
372 HttpConnectionDelegateImpl
* connection
,
373 mojo::HttpRequestPtr request
,
374 const OnReceivedRequestCallback
& callback
) {
375 DCHECK(connections_
.find(connection
) != connections_
.end());
377 if (request
->url
.get().find(kJsonRequestUrlPrefix
) == 0) {
378 mojo::HttpResponsePtr response
= ProcessJsonRequest(request
.Pass());
380 callback
.Run(response
.Pass());
382 OnConnectionClosed(connection
);
384 // TODO(yzshen): Implement it.
386 callback
.Run(MakeResponse(404, "text/html", "Not implemented yet!"));
390 void DevToolsHttpServer::OnReceivedWebSocketRequest(
391 HttpConnectionDelegateImpl
* connection
,
392 mojo::HttpRequestPtr request
,
393 const OnReceivedWebSocketRequestCallback
& callback
) {
394 DCHECK(connections_
.find(connection
) != connections_
.end());
396 std::string path
= request
->url
;
397 size_t browser_pos
= path
.find(kBrowserUrlPrefix
);
398 if (browser_pos
== 0) {
399 // TODO(yzshen): Implement it.
401 callback
.Run(nullptr, mojo::ScopedDataPipeConsumerHandle(), nullptr);
405 size_t pos
= path
.find(kPageUrlPrefix
);
407 callback
.Run(nullptr, mojo::ScopedDataPipeConsumerHandle(), nullptr);
411 std::string target_id
= path
.substr(strlen(kPageUrlPrefix
));
412 DevToolsAgentHost
* agent
= service_
->registry()->GetAgentById(target_id
);
413 if (!agent
|| agent
->IsAttached()) {
414 callback
.Run(nullptr, mojo::ScopedDataPipeConsumerHandle(), nullptr);
418 mojo::WebSocketPtr web_socket
;
419 mojo::InterfaceRequest
<mojo::WebSocket
> web_socket_request
=
420 mojo::GetProxy(&web_socket
);
421 mojo::DataPipe data_pipe
;
422 mojo::WebSocketClientPtr web_socket_client
= WebSocketRelayer::SetUp(
423 agent
, web_socket
.Pass(), data_pipe
.producer_handle
.Pass());
424 callback
.Run(web_socket_request
.Pass(), data_pipe
.consumer_handle
.Pass(),
425 web_socket_client
.Pass());
428 void DevToolsHttpServer::OnConnectionClosed(
429 HttpConnectionDelegateImpl
* connection
) {
430 DCHECK(connections_
.find(connection
) != connections_
.end());
433 connections_
.erase(connection
);
436 mojo::HttpResponsePtr
DevToolsHttpServer::ProcessJsonRequest(
437 mojo::HttpRequestPtr request
) {
439 std::string path
= request
->url
.get().substr(strlen(kJsonRequestUrlPrefix
));
442 size_t query_pos
= path
.find("?");
443 if (query_pos
!= std::string::npos
)
444 path
= path
.substr(0, query_pos
);
447 size_t fragment_pos
= path
.find("#");
448 if (fragment_pos
!= std::string::npos
)
449 path
= path
.substr(0, fragment_pos
);
452 std::string target_id
;
453 if (!ParseJsonPath(path
, &command
, &target_id
))
454 return MakeJsonResponse(404, nullptr,
455 "Malformed query: " + request
->url
.get());
457 if (command
== kVersionCommand
|| command
== kNewCommand
||
458 command
== kActivateCommand
|| command
== kCloseCommand
) {
460 return MakeJsonResponse(404, nullptr,
461 "Not implemented yet: " + request
->url
.get());
464 if (command
== kListCommand
) {
465 DevToolsRegistryImpl::Iterator
iter(service_
->registry());
466 if (iter
.IsAtEnd()) {
467 // If no agent is available, return a nullptr to indicate that the
468 // connection should be closed.
472 std::string host
= GetHeaderValue(*request
, "host");
474 host
= base::StringPrintf("127.0.0.1:%u",
475 static_cast<unsigned>(remote_debugging_port_
));
478 base::ListValue list_value
;
479 for (; !iter
.IsAtEnd(); iter
.Advance()) {
480 scoped_ptr
<base::DictionaryValue
> dict_value(new base::DictionaryValue());
482 // TODO(yzshen): Add more information.
483 dict_value
->SetString(kTargetDescriptionField
, std::string());
484 dict_value
->SetString(kTargetDevtoolsFrontendUrlField
, std::string());
485 dict_value
->SetString(kTargetIdField
, iter
.value()->id());
486 dict_value
->SetString(kTargetTitleField
, std::string());
487 dict_value
->SetString(kTargetTypeField
, "page");
488 dict_value
->SetString(kTargetUrlField
, std::string());
489 dict_value
->SetString(
490 kTargetWebSocketDebuggerUrlField
,
491 base::StringPrintf("ws://%s%s%s", host
.c_str(), kPageUrlPrefix
,
492 iter
.value()->id().c_str()));
493 list_value
.Append(dict_value
.Pass());
495 return MakeJsonResponse(200, &list_value
, std::string());
498 return MakeJsonResponse(404, nullptr, "Unknown command: " + command
);
501 } // namespace devtools_service