| // Copyright 2015 Google Inc. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #include "cobalt/debug/debug_web_server.h" |
| |
| #include <string> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/file_util.h" |
| #include "base/logging.h" |
| #include "base/optional.h" |
| #include "base/path_service.h" |
| #include "base/string_util.h" |
| #include "build/build_config.h" |
| #include "cobalt/base/cobalt_paths.h" |
| #include "cobalt/debug/json_object.h" |
| #include "net/base/ip_endpoint.h" |
| #include "net/base/net_errors.h" |
| #include "net/base/tcp_listen_socket.h" |
| #include "net/server/http_server_request_info.h" |
| #include "starboard/socket.h" |
| |
| namespace cobalt { |
| namespace debug { |
| |
| namespace { |
| |
| std::string GetMimeType(const FilePath& path) { |
| if (path.MatchesExtension(".html")) { |
| return "text/html"; |
| } else if (path.MatchesExtension(".css")) { |
| return "text/css"; |
| } else if (path.MatchesExtension(".js")) { |
| return "application/javascript"; |
| } else if (path.MatchesExtension(".png")) { |
| return "image/png"; |
| } else if (path.MatchesExtension(".gif")) { |
| return "image/gif"; |
| } else if (path.MatchesExtension(".json")) { |
| return "application/json"; |
| } else if (path.MatchesExtension(".svg")) { |
| return "image/svg+xml"; |
| } else if (path.MatchesExtension(".ico")) { |
| return "image/x-icon"; |
| } |
| DLOG(ERROR) << "GetMimeType doesn't know mime type for: " << path.value() |
| << " text/plain will be returned"; |
| return "text/plain"; |
| } |
| |
| base::optional<FilePath> AppendIndexFile(const FilePath& directory) { |
| DCHECK(file_util::DirectoryExists(directory)); |
| FilePath result; |
| result = directory.AppendASCII("index.html"); |
| if (file_util::PathExists(result)) { |
| return result; |
| } |
| result = directory.AppendASCII("index.json"); |
| if (file_util::PathExists(result)) { |
| return result; |
| } |
| DLOG(ERROR) << "No index file found at: " << directory.value(); |
| return base::nullopt; |
| } |
| |
| base::optional<std::string> GetLocalIpAddress() { |
| net::IPEndPoint ip_addr; |
| SbSocketAddress local_ip; |
| SbMemorySet(&local_ip, 0, sizeof(local_ip)); |
| bool result = false; |
| |
| // Prefer IPv4 addresses, as they're easier to type for debugging. |
| SbSocketAddressType address_types[] = {kSbSocketAddressTypeIpv4, |
| kSbSocketAddressTypeIpv6}; |
| |
| for (std::size_t i = 0; i != SB_ARRAY_SIZE(address_types); ++i) { |
| SbSocketAddress destination; |
| SbMemorySet(&(destination.address), 0, sizeof(destination.address)); |
| destination.type = address_types[i]; |
| if (!SbSocketGetInterfaceAddress(&destination, &local_ip, NULL)) { |
| continue; |
| } |
| |
| if (ip_addr.FromSbSocketAddress(&local_ip)) { |
| result = true; |
| break; |
| } |
| } |
| |
| if (!result) { |
| DLOG(WARNING) << "Unable to get a local interface address."; |
| return base::nullopt; |
| } |
| |
| return ip_addr.ToStringWithoutPort(); |
| } |
| |
| const char kContentDir[] = "cobalt/debug"; |
| const char kDetached[] = "Inspector.detached"; |
| const char kDetachReasonField[] = "params.reason"; |
| const char kErrorField[] = "error.message"; |
| const char kIdField[] = "id"; |
| const char kMethodField[] = "method"; |
| const char kParamsField[] = "params"; |
| } // namespace |
| |
| DebugWebServer::DebugWebServer( |
| int port, const GetDebugServerCallback& get_debug_server_callback) |
| : http_server_thread_("DebugWebServer"), |
| get_debug_server_callback_(get_debug_server_callback), |
| websocket_id_(-1), |
| // Local address will be set when the web server is successfully started. |
| local_address_("Cobalt.Server.DevTools", "<NOT RUNNING>", |
| "Address to connect to for remote debugging.") { |
| // Construct the content root directory to serve files from. |
| PathService::Get(paths::DIR_COBALT_WEB_ROOT, &content_root_dir_); |
| content_root_dir_ = content_root_dir_.AppendASCII(kContentDir); |
| |
| // Start the Http server thread and create the server on that thread. |
| // Thread checker will be attached to that thread in |StartServer|. |
| thread_checker_.DetachFromThread(); |
| const size_t stack_size = 0; |
| http_server_thread_.StartWithOptions( |
| base::Thread::Options(MessageLoop::TYPE_IO, stack_size)); |
| http_server_thread_.message_loop()->PostTask( |
| FROM_HERE, |
| base::Bind(&DebugWebServer::StartServer, base::Unretained(this), port)); |
| } |
| |
| DebugWebServer::~DebugWebServer() { |
| // Destroy the server on its own thread then stop the thread. |
| http_server_thread_.message_loop()->PostTask( |
| FROM_HERE, |
| base::Bind(&DebugWebServer::StopServer, base::Unretained(this))); |
| http_server_thread_.Stop(); |
| } |
| |
| void DebugWebServer::OnHttpRequest(int connection_id, |
| const net::HttpServerRequestInfo& info) { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| DLOG(INFO) << "Got HTTP request: " << connection_id << ": " << info.path; |
| |
| // TODO: Requests for / or /json (listing of discoverable pages) |
| // currently send static index pages. When the debugger has support to get |
| // the current URL (and any other dynamic content), then the index pages |
| // should be created dynamically from templates. |
| |
| // Get the relative URL path with no query parameters. |
| std::string url_path = info.path; |
| while (url_path[0] == '/') { |
| url_path = url_path.substr(1); |
| } |
| size_t query_position = url_path.find("?"); |
| if (query_position != std::string::npos) { |
| url_path.resize(query_position); |
| } |
| |
| // Construct the local disk path corresponding to the request path. |
| FilePath file_path(content_root_dir_); |
| if (!IsStringASCII(url_path)) { |
| LOG(WARNING) << "Got HTTP request with non-ASCII URL path."; |
| server_->Send404(connection_id); |
| return; |
| } |
| file_path = file_path.AppendASCII(url_path); |
| |
| // If the disk path is a directory, look for an index file. |
| if (file_util::DirectoryExists(file_path)) { |
| base::optional<FilePath> index_file_path = AppendIndexFile(file_path); |
| if (index_file_path) { |
| file_path = *index_file_path; |
| } else { |
| DLOG(WARNING) << "No index file in directory: " << file_path.value(); |
| server_->Send404(connection_id); |
| return; |
| } |
| } |
| |
| // If we can read the local file, send its contents, otherwise send a 404. |
| std::string data; |
| if (file_util::PathExists(file_path) && |
| file_util::ReadFileToString(file_path, &data)) { |
| DLOG(INFO) << "Sending data from: " << file_path.value(); |
| std::string mime_type = GetMimeType(file_path); |
| server_->Send200(connection_id, data, mime_type); |
| } else { |
| DLOG(WARNING) << "Cannot read file: " << file_path.value(); |
| server_->Send404(connection_id); |
| } |
| } |
| |
| void DebugWebServer::OnWebSocketRequest( |
| int connection_id, const net::HttpServerRequestInfo& info) { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| std::string path = info.path; |
| DLOG(INFO) << "Got web socket request [" << connection_id << "]: " << path; |
| |
| // Ignore the path and bind any web socket request to the debugger. |
| websocket_id_ = connection_id; |
| server_->AcceptWebSocket(connection_id, info); |
| |
| DebugServer* debug_server = get_debug_server_callback_.Run(); |
| debug_client_.reset(new DebugClient(debug_server, this)); |
| } |
| |
| void DebugWebServer::OnWebSocketMessage(int connection_id, |
| const std::string& json) { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| DCHECK_EQ(connection_id, websocket_id_); |
| |
| // Parse the json string to get id, method and params. |
| JSONObject json_object = JSONParse(json); |
| if (!json_object) { |
| return SendErrorResponseOverWebSocket(websocket_id_, "Error parsing JSON"); |
| } |
| int id = 0; |
| if (!json_object->GetInteger(kIdField, &id)) { |
| return SendErrorResponseOverWebSocket(id, "Missing request id"); |
| } |
| std::string method; |
| if (!json_object->GetString(kMethodField, &method)) { |
| return SendErrorResponseOverWebSocket(id, "Missing method"); |
| } |
| // Parameters are optional. |
| base::Value* params_value = NULL; |
| std::string json_params; |
| if (json_object->Remove(kParamsField, ¶ms_value)) { |
| base::DictionaryValue* params_dictionary = NULL; |
| params_value->GetAsDictionary(¶ms_dictionary); |
| JSONObject params(params_dictionary); |
| DCHECK(params); |
| json_params = JSONStringify(params); |
| } |
| |
| if (!debug_client_ || !debug_client_->IsAttached()) { |
| return SendErrorResponseOverWebSocket( |
| id, "Debugger is not connected - call attach first."); |
| } |
| |
| debug_client_->SendCommand(method, json_params, |
| base::Bind(&DebugWebServer::OnDebuggerResponse, |
| base::Unretained(this), id)); |
| } |
| |
| void DebugWebServer::SendErrorResponseOverWebSocket( |
| int id, const std::string& message) { |
| DCHECK_GE(websocket_id_, 0); |
| JSONObject response(new base::DictionaryValue()); |
| response->SetInteger(kIdField, id); |
| response->SetString(kErrorField, message); |
| server_->SendOverWebSocket(websocket_id_, JSONStringify(response)); |
| } |
| |
| void DebugWebServer::OnDebuggerResponse( |
| int id, const base::optional<std::string>& response) { |
| JSONObject response_object = JSONParse(response.value()); |
| DCHECK(response_object); |
| response_object->SetInteger(kIdField, id); |
| server_->SendOverWebSocket(websocket_id_, JSONStringify(response_object)); |
| } |
| |
| void DebugWebServer::OnDebugClientEvent( |
| const std::string& method, const base::optional<std::string>& json_params) { |
| // Debugger events occur on the thread of the web module the debugger is |
| // attached to, so we must post to the server thread here. |
| if (MessageLoop::current() != http_server_thread_.message_loop()) { |
| http_server_thread_.message_loop()->PostTask( |
| FROM_HERE, base::Bind(&DebugWebServer::OnDebugClientEvent, |
| base::Unretained(this), method, json_params)); |
| return; |
| } |
| |
| JSONObject event(new base::DictionaryValue()); |
| event->SetString(kMethodField, method); |
| JSONObject params = JSONParse(json_params.value()); |
| // |params| may be NULL if event does not use them. |
| if (params) { |
| event->Set(kParamsField, params.release()); |
| } |
| server_->SendOverWebSocket(websocket_id_, JSONStringify(event)); |
| } |
| |
| void DebugWebServer::OnDebugClientDetach(const std::string& reason) { |
| // Debugger events occur on the thread of the web module the debugger is |
| // attached to, so we must post to the server thread here. |
| if (MessageLoop::current() != http_server_thread_.message_loop()) { |
| http_server_thread_.message_loop()->PostTask( |
| FROM_HERE, base::Bind(&DebugWebServer::OnDebugClientDetach, |
| base::Unretained(this), reason)); |
| return; |
| } |
| |
| DLOG(INFO) << "Got detach event: " << reason; |
| JSONObject event(new base::DictionaryValue()); |
| event->SetString(kMethodField, kDetached); |
| event->SetString(kDetachReasonField, reason); |
| server_->SendOverWebSocket(websocket_id_, JSONStringify(event)); |
| } |
| |
| int DebugWebServer::GetLocalAddress(std::string* out) const { |
| net::IPEndPoint ip_addr; |
| int result = server_->GetLocalAddress(&ip_addr); |
| if (result == net::OK) { |
| *out = std::string("http://") + ip_addr.ToString(); |
| } |
| return result; |
| } |
| |
| void DebugWebServer::StartServer(int port) { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| |
| // Create http server |
| const base::optional<std::string> ip_addr = GetLocalIpAddress(); |
| if (!ip_addr) { |
| DLOG(WARNING) |
| << "Could not get a local IP address for the debug web server."; |
| return; |
| } |
| factory_.reset(new net::TCPListenSocketFactory(*ip_addr, port)); |
| server_ = new net::HttpServer(*factory_, this); |
| |
| std::string address; |
| int result = GetLocalAddress(&address); |
| if (result == net::OK) { |
| DLOG(INFO) << "Debug web server running at: " << address; |
| local_address_ = address; |
| } else { |
| DLOG(WARNING) << "Could not start debug web server"; |
| } |
| } |
| |
| void DebugWebServer::StopServer() { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| server_ = NULL; |
| } |
| |
| } // namespace debug |
| } // namespace cobalt |