| // Copyright 2015 The Cobalt Authors. 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/remote/debug_web_server.h" |
| |
| #include <string> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/files/file_util.h" |
| #include "base/logging.h" |
| #include "base/optional.h" |
| #include "base/path_service.h" |
| #include "base/strings/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/server/http_server_request_info.h" |
| #include "net/socket/tcp_server_socket.h" |
| #include "starboard/common/socket.h" |
| |
| namespace cobalt { |
| namespace debug { |
| namespace remote { |
| |
| namespace { |
| |
| constexpr char kLogBrowserEntryAdded[] = "Log.browserEntryAdded"; |
| |
| std::string GetMimeType(const base::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<base::FilePath> AppendIndexFile( |
| const base::FilePath& directory) { |
| DCHECK(base::DirectoryExists(directory)); |
| base::FilePath result; |
| result = directory.AppendASCII("index.html"); |
| if (base::PathExists(result)) { |
| return result; |
| } |
| result = directory.AppendASCII("index.json"); |
| if (base::PathExists(result)) { |
| return result; |
| } |
| DLOG(ERROR) << "No index file found at: " << directory.value(); |
| return base::nullopt; |
| } |
| |
| const char kContentDir[] = "debug_remote"; |
| 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"; |
| constexpr net::NetworkTrafficAnnotationTag kNetworkTrafficAnnotation = |
| net::DefineNetworkTrafficAnnotation("cobalt_debug_web_server", |
| "cobalt_debug_web_server"); |
| |
| constexpr int kUnattachedWebSocketId = -1; |
| } // namespace |
| |
| DebugWebServer::DebugWebServer( |
| int port, const std::string& listen_ip, |
| const CreateDebugClientCallback& create_debug_client_callback) |
| : http_server_thread_("DebugWebServer"), |
| create_debug_client_callback_(create_debug_client_callback), |
| websocket_id_(kUnattachedWebSocketId), |
| // 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. |
| base::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|. |
| DETACH_FROM_THREAD(thread_checker_); |
| const size_t stack_size = 0; |
| http_server_thread_.StartWithOptions( |
| base::Thread::Options(base::MessageLoop::TYPE_IO, stack_size)); |
| http_server_thread_.message_loop()->task_runner()->PostTask( |
| FROM_HERE, base::Bind(&DebugWebServer::StartServer, |
| base::Unretained(this), port, listen_ip)); |
| } |
| |
| DebugWebServer::~DebugWebServer() { |
| // Destroy the server on its own thread then stop the thread. |
| http_server_thread_.message_loop()->task_runner()->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_CALLED_ON_VALID_THREAD(thread_checker_); |
| 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. |
| base::FilePath file_path(content_root_dir_); |
| if (!base::IsStringASCII(url_path)) { |
| LOG(WARNING) << "Got HTTP request with non-ASCII URL path."; |
| server_->Send404(connection_id, kNetworkTrafficAnnotation); |
| return; |
| } |
| file_path = file_path.AppendASCII(url_path); |
| |
| // If the disk path is a directory, look for an index file. |
| if (base::DirectoryExists(file_path)) { |
| base::Optional<base::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, kNetworkTrafficAnnotation); |
| return; |
| } |
| } |
| |
| // If we can read the local file, send its contents, otherwise send a 404. |
| std::string data; |
| if (base::PathExists(file_path) && base::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, kNetworkTrafficAnnotation); |
| } else { |
| DLOG(WARNING) << "Cannot read file: " << file_path.value(); |
| server_->Send404(connection_id, kNetworkTrafficAnnotation); |
| } |
| } |
| |
| void DebugWebServer::OnWebSocketRequest( |
| int connection_id, const net::HttpServerRequestInfo& info) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| std::string path = info.path; |
| DLOG(INFO) << "Got web socket request [" << connection_id << "]: " << path; |
| |
| // Disconnect any other DevTools client that's already attached. |
| if (websocket_id_ != kUnattachedWebSocketId) { |
| server_->Close(websocket_id_); |
| } |
| |
| // Ignore the path and bind any web socket request to the debugger. |
| websocket_id_ = connection_id; |
| server_->AcceptWebSocket(connection_id, info, kNetworkTrafficAnnotation); |
| |
| debug_client_ = create_debug_client_callback_.Run(this); |
| } |
| |
| void DebugWebServer::OnClose(int connection_id) { |
| if (connection_id == websocket_id_) { |
| websocket_id_ = kUnattachedWebSocketId; |
| } |
| } |
| |
| void DebugWebServer::OnWebSocketMessage(int connection_id, |
| const std::string& json) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| DCHECK_EQ(connection_id, websocket_id_) << "Mismatched 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. |
| std::unique_ptr<base::Value> params_value; |
| std::string json_params; |
| if (json_object->Remove(kParamsField, ¶ms_value)) { |
| base::DictionaryValue* params_dictionary = NULL; |
| params_value->GetAsDictionary(¶ms_dictionary); |
| params_value.release(); |
| JSONObject params(params_dictionary); |
| DCHECK(params); |
| json_params = JSONStringify(params); |
| } |
| |
| if (!debug_client_ || !debug_client_->IsAttached()) { |
| return SendErrorResponseOverWebSocket(id, "Debugger is not connected."); |
| } |
| |
| 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), |
| kNetworkTrafficAnnotation); |
| } |
| |
| 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), |
| kNetworkTrafficAnnotation); |
| } |
| |
| void DebugWebServer::OnDebugClientEvent(const std::string& method, |
| const std::string& json_params) { |
| // Squelch the Cobalt-specific log message meant only for the overlay console. |
| if (method == kLogBrowserEntryAdded) { |
| return; |
| } |
| |
| // 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 (base::MessageLoop::current() != http_server_thread_.message_loop()) { |
| http_server_thread_.message_loop()->task_runner()->PostTask( |
| FROM_HERE, base::Bind(&DebugWebServer::OnDebugClientEvent, |
| base::Unretained(this), method, json_params)); |
| return; |
| } |
| |
| JSONObject event(new base::DictionaryValue()); |
| event->SetString(kMethodField, method); |
| event->Set(kParamsField, JSONParse(json_params)); |
| server_->SendOverWebSocket(websocket_id_, JSONStringify(event), |
| kNetworkTrafficAnnotation); |
| } |
| |
| 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 (base::MessageLoop::current() != http_server_thread_.message_loop()) { |
| http_server_thread_.message_loop()->task_runner()->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), |
| kNetworkTrafficAnnotation); |
| } |
| |
| void DebugWebServer::StartServer(int port, const std::string& listen_ip) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| // Create http server |
| auto* server_socket = |
| new net::TCPServerSocket(NULL /*net_log*/, net::NetLogSource()); |
| server_socket->ListenWithAddressAndPort( |
| listen_ip, static_cast<uint16_t>(port), 1 /*backlog*/); |
| server_.reset(new net::HttpServer( |
| std::unique_ptr<net::ServerSocket>(server_socket), this)); |
| |
| net::IPEndPoint ip_addr; |
| int result = server_->GetLocalInterfaceAddress(&ip_addr); |
| if (result == net::OK) { |
| std::string address = "http://" + ip_addr.ToString(); |
| // clang-format off |
| LOG(INFO) << "\n---------------------------------" |
| << "\n Connect to the web debugger at:" |
| << "\n " << address |
| << "\n---------------------------------"; |
| // clang-format on |
| local_address_ = address; |
| } else { |
| LOG(WARNING) << "Could not start debug web server"; |
| } |
| } |
| |
| void DebugWebServer::StopServer() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| server_ = NULL; |
| } |
| |
| } // namespace remote |
| } // namespace debug |
| } // namespace cobalt |