From 84ad31fff369f0ec9a972c2a11cdefee316dab9d Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Sun, 7 Feb 2016 08:47:14 -0800 Subject: [PATCH] src,lib: v8-inspector support This change introduces experimental v8-inspector support. This brings the DevTools debug protocol allowing Node.js to be debugged with Chrome DevTools native, or through other debuggers supporting that protocol. Partial WebSocket support, to the extent required by DevTools, is included. This is derived from the implementation in Blink. v8-inspector support can be disabled by the --without-inspector configure flag. PR-URL: https://github.com/nodejs/node/pull/6792 Reviewed-By: jasnell - James M Snell Reviewed-By: addaleax - Anna Henningsen Reviewed-By: bnoordhuis - Ben Noordhuis --- configure | 6 + doc/api/debugger.md | 14 + lib/internal/bootstrap_node.js | 4 + node.gyp | 39 ++ src/env-inl.h | 3 + src/env.h | 12 + src/inspector_agent.cc | 506 ++++++++++++++++ src/inspector_agent.h | 97 +++ src/inspector_socket.cc | 679 +++++++++++++++++++++ src/inspector_socket.h | 57 ++ src/node.cc | 67 ++- src/node_internals.h | 6 +- src/signal_wrap.cc | 9 + test/cctest/test_inspector_socket.cc | 864 +++++++++++++++++++++++++++ 14 files changed, 2358 insertions(+), 5 deletions(-) create mode 100644 src/inspector_agent.cc create mode 100644 src/inspector_agent.h create mode 100644 src/inspector_socket.cc create mode 100644 src/inspector_socket.h create mode 100644 test/cctest/test_inspector_socket.cc diff --git a/configure b/configure index 10fd1b5fffa..f7a3f41ae02 100755 --- a/configure +++ b/configure @@ -415,6 +415,11 @@ parser.add_option('--no-browser-globals', help='do not export browser globals like setTimeout, console, etc. ' + '(This mode is not officially supported for regular applications)') +parser.add_option('--without-inspector', + action='store_true', + dest='without_inspector', + help='disable experimental V8 inspector support') + (options, args) = parser.parse_args() # Expand ~ in the install prefix now, it gets written to multiple files. @@ -810,6 +815,7 @@ def configure_node(o): o['variables']['library_files'] = options.linked_module o['variables']['asan'] = int(options.enable_asan or 0) + o['variables']['v8_inspector'] = b(not options.without_inspector) if options.use_xcode and options.use_ninja: raise Exception('--xcode and --ninja cannot be used together.') diff --git a/doc/api/debugger.md b/doc/api/debugger.md index a966ee2b011..6a31212d9c2 100644 --- a/doc/api/debugger.md +++ b/doc/api/debugger.md @@ -179,4 +179,18 @@ process or via URI reference to the listening debugger: * `node debug ` - Connects to the process via the URI such as localhost:5858 +## V8 Inspector Integration for Node.js + +__NOTE: This is an experimental feature.__ + +V8 Inspector integration allows attaching Chrome DevTools to Node.js +instances for debugging and profiling. + +V8 Inspector can be enabled by passing the `--inspect` flag when starting a +Node.js application. It is also possible to supply a custom port with that flag, +e.g. `--inspect=9222` will accept DevTools connections on port 9222. + +To break on the first line of the application code, provide the `--debug-brk` +flag in addition to `--inspect`. + [TCP-based protocol]: https://github.com/v8/v8/wiki/Debugging-Protocol diff --git a/lib/internal/bootstrap_node.js b/lib/internal/bootstrap_node.js index 79359bed551..27f05a4fcf1 100644 --- a/lib/internal/bootstrap_node.js +++ b/lib/internal/bootstrap_node.js @@ -69,6 +69,10 @@ // Start the debugger agent NativeModule.require('_debugger').start(); + } else if (process.argv[1] == '--remote_debugging_server') { + // Start the debugging server + NativeModule.require('internal/inspector/remote_debugging_server'); + } else if (process.argv[1] == '--debug-agent') { // Start the debugger agent NativeModule.require('_debug_agent').start(); diff --git a/node.gyp b/node.gyp index 0d32905b6ce..05a5530a2b1 100644 --- a/node.gyp +++ b/node.gyp @@ -250,6 +250,28 @@ 'deps/v8/src/third_party/vtune/v8vtune.gyp:v8_vtune' ], }], + [ 'v8_inspector=="true"', { + 'defines': [ + 'HAVE_INSPECTOR=1', + 'V8_INSPECTOR_USE_STL=1', + ], + 'sources': [ + 'src/inspector_agent.cc', + 'src/inspector_socket.cc', + 'src/inspector_socket.h', + 'src/inspector-agent.h', + ], + 'dependencies': [ + 'deps/v8_inspector/v8_inspector.gyp:v8_inspector', + ], + 'include_dirs': [ + 'deps/v8_inspector', + 'deps/v8_inspector/deps/wtf', # temporary + '<(SHARED_INTERMEDIATE_DIR)/blink', # for inspector + ], + }, { + 'defines': [ 'HAVE_INSPECTOR=0' ] + }], [ 'node_use_openssl=="true"', { 'defines': [ 'HAVE_OPENSSL=1' ], 'sources': [ @@ -690,7 +712,10 @@ 'target_name': 'cctest', 'type': 'executable', 'dependencies': [ + 'deps/openssl/openssl.gyp:openssl', + 'deps/http_parser/http_parser.gyp:http_parser', 'deps/gtest/gtest.gyp:gtest', + 'deps/uv/uv.gyp:libuv', 'deps/v8/tools/gyp/v8.gyp:v8', 'deps/v8/tools/gyp/v8.gyp:v8_libplatform' ], @@ -711,6 +736,20 @@ 'sources': [ 'test/cctest/util.cc', ], + + 'conditions': [ + ['v8_inspector=="true"', { + 'dependencies': [ + 'deps/openssl/openssl.gyp:openssl', + 'deps/http_parser/http_parser.gyp:http_parser', + 'deps/uv/uv.gyp:libuv' + ], + 'sources': [ + 'src/inspector_socket.cc', + 'test/cctest/test_inspector_socket.cc' + ] + }] + ] } ], # end targets diff --git a/src/env-inl.h b/src/env-inl.h index 34f9bf7d72d..97e1ba8f764 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -225,6 +225,9 @@ inline Environment::Environment(v8::Local context, makecallback_cntr_(0), async_wrap_uid_(0), debugger_agent_(this), +#if HAVE_INSPECTOR + inspector_agent_(this), +#endif http_parser_buffer_(nullptr), context_(context->GetIsolate(), context) { // We'll be creating new objects so make sure we've entered the context. diff --git a/src/env.h b/src/env.h index 0c95abd56cb..4c310c8831f 100644 --- a/src/env.h +++ b/src/env.h @@ -5,6 +5,9 @@ #include "ares.h" #include "debug-agent.h" +#if HAVE_INSPECTOR +#include "inspector_agent.h" +#endif #include "handle_wrap.h" #include "req-wrap.h" #include "tree.h" @@ -549,6 +552,12 @@ class Environment { return &debugger_agent_; } +#if HAVE_INSPECTOR + inline inspector::Agent* inspector_agent() { + return &inspector_agent_; + } +#endif + typedef ListHead HandleWrapQueue; typedef ListHead, &ReqWrap::req_wrap_queue_> ReqWrapQueue; @@ -586,6 +595,9 @@ class Environment { size_t makecallback_cntr_; int64_t async_wrap_uid_; debugger::Agent debugger_agent_; +#if HAVE_INSPECTOR + inspector::Agent inspector_agent_; +#endif HandleWrapQueue handle_wrap_queue_; ReqWrapQueue req_wrap_queue_; diff --git a/src/inspector_agent.cc b/src/inspector_agent.cc new file mode 100644 index 00000000000..cd2ae83b19b --- /dev/null +++ b/src/inspector_agent.cc @@ -0,0 +1,506 @@ +#include "inspector_agent.h" + +#include "node.h" +#include "env.h" +#include "env-inl.h" +#include "node_version.h" +#include "v8-platform.h" +#include "util.h" + +#include "platform/v8_inspector/public/V8Inspector.h" +#include "platform/inspector_protocol/FrontendChannel.h" +#include "platform/inspector_protocol/String16.h" +#include "platform/inspector_protocol/Values.h" + +#include "libplatform/libplatform.h" + +#include + +// We need pid to use as ID with Chrome +#if defined(_MSC_VER) +#include +#include +#define getpid GetCurrentProcessId +#else +#include // setuid, getuid +#endif + +namespace node { +namespace { + +const char DEVTOOLS_PATH[] = "/node"; + +void PrintDebuggerReadyMessage(int port) { + fprintf(stderr, "Debugger listening on port %d. " + "To start debugging, open the following URL in Chrome:\n" + " chrome-devtools://devtools/remote/serve_file/" + "@521e5b7e2b7cc66b4006a8a54cb9c4e57494a5ef/inspector.html?" + "experiments=true&v8only=true&ws=localhost:%d/node\n", port, port); +} + +bool AcceptsConnection(inspector_socket_t* socket, const char* path) { + return strncmp(DEVTOOLS_PATH, path, sizeof(DEVTOOLS_PATH)) == 0; +} + +void DisposeInspector(inspector_socket_t* socket, int status) { + free(socket); +} + +void DisconnectAndDisposeIO(inspector_socket_t* socket) { + if (socket) { + inspector_close(socket, DisposeInspector); + } +} + +void OnBufferAlloc(uv_handle_t* handle, size_t len, uv_buf_t* buf) { + if (len > 0) { + buf->base = static_cast(malloc(len)); + CHECK_NE(buf->base, nullptr); + } + buf->len = len; +} + +void SendHttpResponse(inspector_socket_t* socket, const char* response, + size_t len) { + const char HEADERS[] = "HTTP/1.0 200 OK\r\n" + "Content-Type: application/json; charset=UTF-8\r\n" + "Cache-Control: no-cache\r\n" + "Content-Length: %ld\r\n" + "\r\n"; + char header[sizeof(HEADERS) + 20]; + int header_len = snprintf(header, sizeof(header), HEADERS, len); + inspector_write(socket, header, header_len); + inspector_write(socket, response, len); +} + +void SendVersionResponse(inspector_socket_t* socket) { + const char VERSION_RESPONSE_TEMPLATE[] = + "[ {" + " \"Browser\": \"node.js/%s\"," + " \"Protocol-Version\": \"1.1\"," + " \"User-Agent\": \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" + "(KHTML, like Gecko) Chrome/45.0.2446.0 Safari/537.36\"," + " \"WebKit-Version\": \"537.36 (@198122)\"" + "} ]"; + char buffer[sizeof(VERSION_RESPONSE_TEMPLATE) + 128]; + size_t len = snprintf(buffer, sizeof(buffer), VERSION_RESPONSE_TEMPLATE, + NODE_VERSION); + ASSERT_LT(len, sizeof(buffer)); + SendHttpResponse(socket, buffer, len); +} + +void SendTargentsListResponse(inspector_socket_t* socket) { + const char LIST_RESPONSE_TEMPLATE[] = + "[ {" + " \"description\": \"node.js instance\"," + " \"devtoolsFrontendUrl\": " + "\"https://chrome-devtools-frontend.appspot.com/serve_file/" + "@4604d24a75168768584760ba56d175507941852f/inspector.html\"," + " \"faviconUrl\": \"https://nodejs.org/static/favicon.ico\"," + " \"id\": \"%d\"," + " \"title\": \"%s\"," + " \"type\": \"node\"," + " \"webSocketDebuggerUrl\": \"ws://%s\"" + "} ]"; + char buffer[sizeof(LIST_RESPONSE_TEMPLATE) + 4096]; + char title[2048]; // uv_get_process_title trims the title if too long + int err = uv_get_process_title(title, sizeof(title)); + ASSERT_EQ(0, err); + char* c = title; + while (!c) { + if (*c < ' ' || *c == '\"') { + *c = '_'; + } + c++; + } + size_t len = snprintf(buffer, sizeof(buffer), LIST_RESPONSE_TEMPLATE, + getpid(), title, DEVTOOLS_PATH); + ASSERT_LT(len, sizeof(buffer)); + SendHttpResponse(socket, buffer, len); +} + +bool RespondToGet(inspector_socket_t* socket, const char* path) { + const char PATH[] = "/json"; + const char PATH_LIST[] = "/json/list"; + const char PATH_VERSION[] = "/json/version"; + const char PATH_ACTIVATE[] = "/json/activate/"; + if (!strncmp(PATH_VERSION, path, sizeof(PATH_VERSION))) { + SendVersionResponse(socket); + } else if (!strncmp(PATH_LIST, path, sizeof(PATH_LIST)) || + !strncmp(PATH, path, sizeof(PATH))) { + SendTargentsListResponse(socket); + } else if (!strncmp(path, PATH_ACTIVATE, sizeof(PATH_ACTIVATE) - 1) && + atoi(path + (sizeof(PATH_ACTIVATE) - 1)) == getpid()) { + const char TARGET_ACTIVATED[] = "Target activated"; + SendHttpResponse(socket, TARGET_ACTIVATED, sizeof(TARGET_ACTIVATED) - 1); + } else { + return false; + } + return true; +} + +} // namespace + +namespace inspector { + +using blink::protocol::DictionaryValue; +using blink::protocol::String16; + +void InterruptCallback(v8::Isolate*, void* agent) { + static_cast(agent)->PostMessages(); +} + +class DispatchOnInspectorBackendTask : public v8::Task { + public: + explicit DispatchOnInspectorBackendTask(Agent* agent) : agent_(agent) {} + + void Run() override { + agent_->PostMessages(); + } + + private: + Agent* agent_; +}; + +class ChannelImpl final : public blink::protocol::FrontendChannel { + public: + explicit ChannelImpl(Agent* agent): agent_(agent) {} + virtual ~ChannelImpl() {} + private: + virtual void sendProtocolResponse(int sessionId, int callId, + std::unique_ptr message) + override { + sendMessageToFrontend(std::move(message)); + } + + virtual void sendProtocolNotification( + std::unique_ptr message) override { + sendMessageToFrontend(std::move(message)); + } + + virtual void flush() override { } + + void sendMessageToFrontend(std::unique_ptr message) { + agent_->Write(message->toJSONString().utf8()); + } + + Agent* const agent_; +}; + +class SetConnectedTask : public v8::Task { + public: + SetConnectedTask(Agent* agent, bool connected) + : agent_(agent), + connected_(connected) {} + + void Run() override { + agent_->SetConnected(connected_); + } + + private: + Agent* agent_; + bool connected_; +}; + +class V8NodeInspector : public blink::V8Inspector { + public: + V8NodeInspector(Agent* agent, node::Environment* env, v8::Platform* platform) + : blink::V8Inspector(env->isolate(), env->context()), + agent_(agent), + isolate_(env->isolate()), + platform_(platform), + terminated_(false), + running_nested_loop_(false) {} + + void runMessageLoopOnPause(int context_group_id) override { + if (running_nested_loop_) + return; + terminated_ = false; + running_nested_loop_ = true; + do { + uv_mutex_lock(&agent_->pause_lock_); + uv_cond_wait(&agent_->pause_cond_, &agent_->pause_lock_); + uv_mutex_unlock(&agent_->pause_lock_); + while (v8::platform::PumpMessageLoop(platform_, isolate_)) + {} + } while (!terminated_); + terminated_ = false; + running_nested_loop_ = false; + } + + void quitMessageLoopOnPause() override { + terminated_ = true; + } + + private: + Agent* agent_; + v8::Isolate* isolate_; + v8::Platform* platform_; + bool terminated_; + bool running_nested_loop_; +}; + +Agent::Agent(Environment* env) : port_(9229), + wait_(false), + connected_(false), + shutting_down_(false), + parent_env_(env), + client_socket_(nullptr), + inspector_(nullptr), + platform_(nullptr), + dispatching_messages_(false) { + int err; + err = uv_sem_init(&start_sem_, 0); + CHECK_EQ(err, 0); +} + +Agent::~Agent() { + if (!inspector_) + return; + uv_mutex_destroy(&queue_lock_); + uv_mutex_destroy(&pause_lock_); + uv_cond_destroy(&pause_cond_); + uv_close(reinterpret_cast(&data_written_), nullptr); +} + +void Agent::Start(v8::Platform* platform, int port, bool wait) { + auto env = parent_env_; + inspector_ = new V8NodeInspector(this, env, platform); + + int err; + + platform_ = platform; + + err = uv_loop_init(&child_loop_); + CHECK_EQ(err, 0); + err = uv_async_init(env->event_loop(), &data_written_, nullptr); + CHECK_EQ(err, 0); + err = uv_mutex_init(&queue_lock_); + CHECK_EQ(err, 0); + err = uv_mutex_init(&pause_lock_); + CHECK_EQ(err, 0); + err = uv_cond_init(&pause_cond_); + CHECK_EQ(err, 0); + + uv_unref(reinterpret_cast(&data_written_)); + + port_ = port; + wait_ = wait; + + err = uv_thread_create(&thread_, Agent::ThreadCbIO, this); + CHECK_EQ(err, 0); + uv_sem_wait(&start_sem_); + + if (wait) { + // Flush messages in case of wait to connect, see OnRemoteDataIO on how it + // should be fixed. + SetConnected(true); + PostMessages(); + } +} + +void Agent::Stop() { + // TODO(repenaxa): hop on the right thread. + DisconnectAndDisposeIO(client_socket_); + int err = uv_thread_join(&thread_); + CHECK_EQ(err, 0); + + uv_run(&child_loop_, UV_RUN_NOWAIT); + + err = uv_loop_close(&child_loop_); + CHECK_EQ(err, 0); + delete inspector_; +} + +bool Agent::IsStarted() { + return !!platform_; +} + +void Agent::WaitForDisconnect() { + shutting_down_ = true; + fprintf(stderr, "Waiting for the debugger to disconnect...\n"); + inspector_->runMessageLoopOnPause(0); +} + +// static +void Agent::ThreadCbIO(void* agent) { + static_cast(agent)->WorkerRunIO(); +} + +// static +void Agent::OnSocketConnectionIO(uv_stream_t* server, int status) { + if (status == 0) { + inspector_socket_t* socket = + static_cast(malloc(sizeof(*socket))); + ASSERT_NE(nullptr, socket); + memset(socket, 0, sizeof(*socket)); + socket->data = server->data; + if (inspector_accept(server, socket, Agent::OnInspectorHandshakeIO) != 0) { + free(socket); + } + } +} + +// static +bool Agent::OnInspectorHandshakeIO(inspector_socket_t* socket, + enum inspector_handshake_event state, + const char* path) { + Agent* agent = static_cast(socket->data); + switch (state) { + case kInspectorHandshakeHttpGet: + return RespondToGet(socket, path); + case kInspectorHandshakeUpgrading: + return AcceptsConnection(socket, path); + case kInspectorHandshakeUpgraded: + agent->OnInspectorConnectionIO(socket); + return true; + case kInspectorHandshakeFailed: + return false; + default: + UNREACHABLE(); + } +} + +// static +void Agent::OnRemoteDataIO(uv_stream_t* stream, + ssize_t read, + const uv_buf_t* b) { + inspector_socket_t* socket = static_cast(stream->data); + Agent* agent = static_cast(socket->data); + if (read > 0) { + std::string str(b->base, read); + agent->PushPendingMessage(&agent->message_queue_, str); + free(b->base); + + // TODO(pfeldman): Instead of blocking execution while debugger + // engages, node should wait for the run callback from the remote client + // and initiate its startup. This is a change to node.cc that should be + // upstreamed separately. + if (agent->wait_ && str.find("\"Runtime.run\"") != std::string::npos) { + agent->wait_ = false; + uv_sem_post(&agent->start_sem_); + } + + agent->platform_->CallOnForegroundThread(agent->parent_env_->isolate(), + new DispatchOnInspectorBackendTask(agent)); + agent->parent_env_->isolate() + ->RequestInterrupt(InterruptCallback, agent); + uv_async_send(&agent->data_written_); + } else if (read < 0) { + if (agent->client_socket_ == socket) { + agent->client_socket_ = nullptr; + } + DisconnectAndDisposeIO(socket); + } else { + // EOF + if (agent->client_socket_ == socket) { + agent->client_socket_ = nullptr; + agent->platform_->CallOnForegroundThread(agent->parent_env_->isolate(), + new SetConnectedTask(agent, false)); + uv_async_send(&agent->data_written_); + } + } + uv_cond_broadcast(&agent->pause_cond_); +} + +void Agent::PushPendingMessage(std::vector* queue, + const std::string& message) { + uv_mutex_lock(&queue_lock_); + queue->push_back(message); + uv_mutex_unlock(&queue_lock_); +} + +void Agent::SwapBehindLock(std::vector Agent::*queue, + std::vector* output) { + uv_mutex_lock(&queue_lock_); + (this->*queue).swap(*output); + uv_mutex_unlock(&queue_lock_); +} + +// static +void Agent::WriteCbIO(uv_async_t* async) { + Agent* agent = static_cast(async->data); + inspector_socket_t* socket = agent->client_socket_; + if (socket) { + std::vector outgoing_messages; + agent->SwapBehindLock(&Agent::outgoing_message_queue_, &outgoing_messages); + for (auto const& message : outgoing_messages) + inspector_write(socket, message.c_str(), message.length()); + } +} + +void Agent::WorkerRunIO() { + sockaddr_in addr; + uv_tcp_t server; + int err = uv_async_init(&child_loop_, &io_thread_req_, Agent::WriteCbIO); + CHECK_EQ(0, err); + io_thread_req_.data = this; + uv_tcp_init(&child_loop_, &server); + uv_ip4_addr("0.0.0.0", port_, &addr); + server.data = this; + err = uv_tcp_bind(&server, + reinterpret_cast(&addr), 0); + if (err == 0) { + err = uv_listen(reinterpret_cast(&server), 1, + OnSocketConnectionIO); + } + if (err == 0) { + PrintDebuggerReadyMessage(port_); + } else { + fprintf(stderr, "Unable to open devtools socket: %s\n", uv_strerror(err)); + ABORT(); + } + if (!wait_) { + uv_sem_post(&start_sem_); + } + uv_run(&child_loop_, UV_RUN_DEFAULT); + uv_close(reinterpret_cast(&io_thread_req_), nullptr); + uv_close(reinterpret_cast(&server), nullptr); + uv_run(&child_loop_, UV_RUN_DEFAULT); +} + +void Agent::OnInspectorConnectionIO(inspector_socket_t* socket) { + if (client_socket_) { + return; + } + client_socket_ = socket; + inspector_read_start(socket, OnBufferAlloc, Agent::OnRemoteDataIO); + platform_->CallOnForegroundThread(parent_env_->isolate(), + new SetConnectedTask(this, true)); +} + +void Agent::PostMessages() { + if (dispatching_messages_) + return; + dispatching_messages_ = true; + std::vector messages; + SwapBehindLock(&Agent::message_queue_, &messages); + for (auto const& message : messages) + inspector_->dispatchMessageFromFrontend( + String16::fromUTF8(message.c_str(), message.length())); + uv_async_send(&data_written_); + dispatching_messages_ = false; +} + +void Agent::SetConnected(bool connected) { + if (connected_ == connected) + return; + + connected_ = connected; + if (connected) { + fprintf(stderr, "Debugger attached.\n"); + inspector_->connectFrontend(new ChannelImpl(this)); + } else { + if (!shutting_down_) + PrintDebuggerReadyMessage(port_); + inspector_->quitMessageLoopOnPause(); + inspector_->disconnectFrontend(); + } +} + +void Agent::Write(const std::string& message) { + PushPendingMessage(&outgoing_message_queue_, message); + ASSERT_EQ(0, uv_async_send(&io_thread_req_)); +} +} // namespace debugger +} // namespace node diff --git a/src/inspector_agent.h b/src/inspector_agent.h new file mode 100644 index 00000000000..65a4abeff7d --- /dev/null +++ b/src/inspector_agent.h @@ -0,0 +1,97 @@ +#ifndef SRC_INSPECTOR_AGENT_H_ +#define SRC_INSPECTOR_AGENT_H_ + +#if !HAVE_INSPECTOR +#error("This header can only be used when inspector is enabled") +#endif + +#include "inspector_socket.h" +#include "uv.h" +#include "v8.h" +#include "util.h" + +#include +#include + +namespace blink { +class V8Inspector; +} + +// Forward declaration to break recursive dependency chain with src/env.h. +namespace node { +class Environment; +} // namespace node + +namespace node { +namespace inspector { + +class ChannelImpl; + +class Agent { + public: + explicit Agent(node::Environment* env); + ~Agent(); + + // Start the inspector agent thread + void Start(v8::Platform* platform, int port, bool wait); + // Stop the inspector agent + void Stop(); + + bool IsStarted(); + bool connected() { return connected_; } + void WaitForDisconnect(); + + protected: + static void ThreadCbIO(void* agent); + static void OnSocketConnectionIO(uv_stream_t* server, int status); + static bool OnInspectorHandshakeIO(inspector_socket_t* socket, + enum inspector_handshake_event state, + const char* path); + static void OnRemoteDataIO(uv_stream_t* stream, ssize_t read, + const uv_buf_t* b); + static void WriteCbIO(uv_async_t* async); + + void WorkerRunIO(); + void OnInspectorConnectionIO(inspector_socket_t* socket); + void PushPendingMessage(std::vector* queue, + const std::string& message); + void SwapBehindLock(std::vector Agent::*queue, + std::vector* output); + void PostMessages(); + void SetConnected(bool connected); + void Write(const std::string& message); + + uv_sem_t start_sem_; + uv_cond_t pause_cond_; + uv_mutex_t queue_lock_; + uv_mutex_t pause_lock_; + uv_thread_t thread_; + uv_loop_t child_loop_; + uv_tcp_t server_; + + int port_; + bool wait_; + bool connected_; + bool shutting_down_; + node::Environment* parent_env_; + + uv_async_t data_written_; + uv_async_t io_thread_req_; + inspector_socket_t* client_socket_; + blink::V8Inspector* inspector_; + v8::Platform* platform_; + std::vector message_queue_; + std::vector outgoing_message_queue_; + bool dispatching_messages_; + + friend class ChannelImpl; + friend class DispatchOnInspectorBackendTask; + friend class SetConnectedTask; + friend class V8NodeInspector; + friend void InterruptCallback(v8::Isolate*, void* agent); +}; + +} // namespace inspector +} // namespace node + +#endif // SRC_INSPECTOR_AGENT_H_ diff --git a/src/inspector_socket.cc b/src/inspector_socket.cc new file mode 100644 index 00000000000..cb248ec59fe --- /dev/null +++ b/src/inspector_socket.cc @@ -0,0 +1,679 @@ +#include "inspector_socket.h" + +#define NODE_WANT_INTERNALS 1 +#include "base64.h" + +#include "openssl/sha.h" // Sha-1 hash + +#include +#include + +#define ACCEPT_KEY_LENGTH base64_encoded_size(20) +#define BUFFER_GROWTH_CHUNK_SIZE 1024 + +#define DUMP_READS 0 +#define DUMP_WRITES 0 + +static const char CLOSE_FRAME[] = {'\x88', '\x00'}; + +struct http_parsing_state_s { + http_parser parser; + http_parser_settings parser_settings; + handshake_cb callback; + bool parsing_value; + char* ws_key; + char* path; + char* current_header; +}; + +struct ws_state_s { + uv_alloc_cb alloc_cb; + uv_read_cb read_cb; + inspector_cb close_cb; + bool close_sent; + bool received_close; +}; + +enum ws_decode_result { + FRAME_OK, FRAME_INCOMPLETE, FRAME_CLOSE, FRAME_ERROR +}; + +#if DUMP_READS || DUMP_WRITES +static void dump_hex(const char* buf, size_t len) { + const char* ptr = buf; + const char* end = ptr + len; + const char* cptr; + char c; + int i; + + while (ptr < end) { + cptr = ptr; + for (i = 0; i < 16 && ptr < end; i++) { + printf("%2.2X ", *(ptr++)); + } + for (i = 72 - (i * 4); i > 0; i--) { + printf(" "); + } + for (i = 0; i < 16 && cptr < end; i++) { + c = *(cptr++); + printf("%c", (c > 0x19) ? c : '.'); + } + printf("\n"); + } + printf("\n\n"); +} +#endif + +static void dispose_inspector(uv_handle_t* handle) { + inspector_socket_t* inspector = + reinterpret_cast(handle->data); + inspector_cb close = + inspector->ws_mode ? inspector->ws_state->close_cb : nullptr; + free(inspector->buffer); + free(inspector->ws_state); + inspector->ws_state = nullptr; + inspector->buffer = nullptr; + inspector->buffer_size = 0; + inspector->data_len = 0; + inspector->last_read_end = 0; + if (close) { + close(inspector, 0); + } +} + +static void close_connection(inspector_socket_t* inspector) { + uv_handle_t* socket = reinterpret_cast(&inspector->client); + if (!uv_is_closing(socket)) { + uv_read_stop(reinterpret_cast(socket)); + uv_close(socket, dispose_inspector); + } else if (inspector->ws_state->close_cb) { + inspector->ws_state->close_cb(inspector, 0); + } +} + +// Cleanup +static void write_request_cleanup(uv_write_t* req, int status) { + free((reinterpret_cast(req->data))->base); + free(req->data); + free(req); +} + +static int write_to_client(inspector_socket_t* inspector, + const char* msg, + size_t len, + uv_write_cb write_cb = write_request_cleanup) { +#if DUMP_WRITES + printf("%s (%ld bytes):\n", __FUNCTION__, len); + dump_hex(msg, len); +#endif + + // Freed in write_request_cleanup + uv_buf_t* buf = reinterpret_cast(malloc(sizeof(uv_buf_t))); + uv_write_t* req = reinterpret_cast(malloc(sizeof(uv_write_t))); + CHECK_NE(buf, nullptr); + CHECK_NE(req, nullptr); + memset(req, 0, sizeof(*req)); + buf->base = reinterpret_cast(malloc(len)); + + CHECK_NE(buf->base, nullptr); + + memcpy(buf->base, msg, len); + buf->len = len; + req->data = buf; + + uv_stream_t* stream = reinterpret_cast(&inspector->client); + return uv_write(req, stream, buf, 1, write_cb) < 0; +} + +// Constants for hybi-10 frame format. + +typedef int OpCode; + +const OpCode kOpCodeContinuation = 0x0; +const OpCode kOpCodeText = 0x1; +const OpCode kOpCodeBinary = 0x2; +const OpCode kOpCodeClose = 0x8; +const OpCode kOpCodePing = 0x9; +const OpCode kOpCodePong = 0xA; + +const unsigned char kFinalBit = 0x80; +const unsigned char kReserved1Bit = 0x40; +const unsigned char kReserved2Bit = 0x20; +const unsigned char kReserved3Bit = 0x10; +const unsigned char kOpCodeMask = 0xF; +const unsigned char kMaskBit = 0x80; +const unsigned char kPayloadLengthMask = 0x7F; + +const size_t kMaxSingleBytePayloadLength = 125; +const size_t kTwoBytePayloadLengthField = 126; +const size_t kEightBytePayloadLengthField = 127; +const size_t kMaskingKeyWidthInBytes = 4; + +static std::vector encode_frame_hybi17(const char* message, + size_t data_length) { + std::vector frame; + OpCode op_code = kOpCodeText; + frame.push_back(kFinalBit | op_code); + if (data_length <= kMaxSingleBytePayloadLength) { + frame.push_back(static_cast(data_length)); + } else if (data_length <= 0xFFFF) { + frame.push_back(kTwoBytePayloadLengthField); + frame.push_back((data_length & 0xFF00) >> 8); + frame.push_back(data_length & 0xFF); + } else { + frame.push_back(kEightBytePayloadLengthField); + char extended_payload_length[8]; + size_t remaining = data_length; + // Fill the length into extended_payload_length in the network byte order. + for (int i = 0; i < 8; ++i) { + extended_payload_length[7 - i] = remaining & 0xFF; + remaining >>= 8; + } + frame.insert(frame.end(), extended_payload_length, + extended_payload_length + 8); + ASSERT_EQ(0, remaining); + } + frame.insert(frame.end(), message, message + data_length); + return frame; +} + +static ws_decode_result decode_frame_hybi17(const char* buffer_begin, + size_t data_length, + bool client_frame, + int* bytes_consumed, + std::vector* output, + bool* compressed) { + *bytes_consumed = 0; + if (data_length < 2) + return FRAME_INCOMPLETE; + + const char* p = buffer_begin; + const char* buffer_end = p + data_length; + + unsigned char first_byte = *p++; + unsigned char second_byte = *p++; + + bool final = (first_byte & kFinalBit) != 0; + bool reserved1 = (first_byte & kReserved1Bit) != 0; + bool reserved2 = (first_byte & kReserved2Bit) != 0; + bool reserved3 = (first_byte & kReserved3Bit) != 0; + int op_code = first_byte & kOpCodeMask; + bool masked = (second_byte & kMaskBit) != 0; + *compressed = reserved1; + if (!final || reserved2 || reserved3) + return FRAME_ERROR; // Only compression extension is supported. + + bool closed = false; + switch (op_code) { + case kOpCodeClose: + closed = true; + break; + case kOpCodeText: + break; + case kOpCodeBinary: // We don't support binary frames yet. + case kOpCodeContinuation: // We don't support binary frames yet. + case kOpCodePing: // We don't support binary frames yet. + case kOpCodePong: // We don't support binary frames yet. + default: + return FRAME_ERROR; + } + + // In Hybi-17 spec client MUST mask its frame. + if (client_frame && !masked) { + return FRAME_ERROR; + } + + uint64_t payload_length64 = second_byte & kPayloadLengthMask; + if (payload_length64 > kMaxSingleBytePayloadLength) { + int extended_payload_length_size; + if (payload_length64 == kTwoBytePayloadLengthField) { + extended_payload_length_size = 2; + } else if (payload_length64 == kEightBytePayloadLengthField) { + extended_payload_length_size = 8; + } else { + return FRAME_ERROR; + } + if (buffer_end - p < extended_payload_length_size) + return FRAME_INCOMPLETE; + payload_length64 = 0; + for (int i = 0; i < extended_payload_length_size; ++i) { + payload_length64 <<= 8; + payload_length64 |= static_cast(*p++); + } + } + + static const uint64_t max_payload_length = 0x7FFFFFFFFFFFFFFFull; + static const size_t max_length = SIZE_MAX; + if (payload_length64 > max_payload_length || + payload_length64 > max_length - kMaskingKeyWidthInBytes) { + // WebSocket frame length too large. + return FRAME_ERROR; + } + size_t payload_length = static_cast(payload_length64); + + if (data_length - kMaskingKeyWidthInBytes < payload_length) + return FRAME_INCOMPLETE; + + const char* masking_key = p; + const char* payload = p + kMaskingKeyWidthInBytes; + for (size_t i = 0; i < payload_length; ++i) // Unmask the payload. + output->insert(output->end(), + payload[i] ^ masking_key[i % kMaskingKeyWidthInBytes]); + + size_t pos = p + kMaskingKeyWidthInBytes + payload_length - buffer_begin; + *bytes_consumed = pos; + return closed ? FRAME_CLOSE : FRAME_OK; +} + +static void invoke_read_callback(inspector_socket_t* inspector, + int status, const uv_buf_t* buf) { + if (inspector->ws_state->read_cb) { + inspector->ws_state->read_cb( + reinterpret_cast(&inspector->client), status, buf); + } +} + +static void shutdown_complete(inspector_socket_t* inspector) { + if (inspector->ws_state->close_cb) { + inspector->ws_state->close_cb(inspector, 0); + } + close_connection(inspector); +} + +static void on_close_frame_written(uv_write_t* write, int status) { + inspector_socket_t* inspector = + reinterpret_cast(write->handle->data); + write_request_cleanup(write, status); + inspector->ws_state->close_sent = true; + if (inspector->ws_state->received_close) { + shutdown_complete(inspector); + } +} + +static void close_frame_received(inspector_socket_t* inspector) { + inspector->ws_state->received_close = true; + if (!inspector->ws_state->close_sent) { + invoke_read_callback(inspector, 0, 0); + write_to_client(inspector, CLOSE_FRAME, sizeof(CLOSE_FRAME), + on_close_frame_written); + } else { + shutdown_complete(inspector); + } +} + +static int parse_ws_frames(inspector_socket_t* inspector, size_t len) { + int bytes_consumed = 0; + std::vector output; + bool compressed = false; + + ws_decode_result r = decode_frame_hybi17(inspector->buffer, + len, true /* client_frame */, + &bytes_consumed, &output, + &compressed); + // Compressed frame means client is ignoring the headers and misbehaves + if (compressed || r == FRAME_ERROR) { + invoke_read_callback(inspector, UV_EPROTO, nullptr); + close_connection(inspector); + bytes_consumed = 0; + } else if (r == FRAME_CLOSE) { + close_frame_received(inspector); + bytes_consumed = 0; + } else if (r == FRAME_OK && inspector->ws_state->alloc_cb + && inspector->ws_state->read_cb) { + uv_buf_t buffer; + size_t len = output.size(); + inspector->ws_state->alloc_cb( + reinterpret_cast(&inspector->client), + len, &buffer); + CHECK_GE(buffer.len, len); + memcpy(buffer.base, &output[0], len); + invoke_read_callback(inspector, len, &buffer); + } + return bytes_consumed; +} + +static void prepare_buffer(uv_handle_t* stream, size_t len, uv_buf_t* buf) { + inspector_socket_t* inspector = + reinterpret_cast(stream->data); + + if (len > (inspector->buffer_size - inspector->data_len)) { + int new_size = (inspector->data_len + len + BUFFER_GROWTH_CHUNK_SIZE - 1) / + BUFFER_GROWTH_CHUNK_SIZE * + BUFFER_GROWTH_CHUNK_SIZE; + inspector->buffer_size = new_size; + inspector->buffer = reinterpret_cast(realloc(inspector->buffer, + inspector->buffer_size)); + ASSERT_NE(inspector->buffer, nullptr); + } + buf->base = inspector->buffer + inspector->data_len; + buf->len = len; + inspector->data_len += len; +} + +static void websockets_data_cb(uv_stream_t* stream, ssize_t nread, + const uv_buf_t* buf) { + inspector_socket_t* inspector = + reinterpret_cast(stream->data); + if (nread < 0 || nread == UV_EOF) { + inspector->connection_eof = true; + if (!inspector->shutting_down && inspector->ws_state->read_cb) { + inspector->ws_state->read_cb(stream, nread, nullptr); + } + } else { + #if DUMP_READS + printf("%s read %ld bytes\n", __FUNCTION__, nread); + if (nread > 0) { + dump_hex(buf->base, nread); + } + #endif + // 1. Move read bytes to continue the buffer + // Should be same as this is supposedly last buffer + ASSERT_EQ(buf->base + buf->len, inspector->buffer + inspector->data_len); + + // Should be noop... + memmove(inspector->buffer + inspector->last_read_end, buf->base, nread); + inspector->last_read_end += nread; + + // 2. Parse. + int processed = 0; + do { + processed = parse_ws_frames(inspector, inspector->last_read_end); + // 3. Fix the buffer size & length + if (processed > 0) { + memmove(inspector->buffer, inspector->buffer + processed, + inspector->last_read_end - processed); + inspector->last_read_end -= processed; + inspector->data_len = inspector->last_read_end; + } + } while (processed > 0 && inspector->data_len > 0); + } +} + +int inspector_read_start(inspector_socket_t* inspector, + uv_alloc_cb alloc_cb, uv_read_cb read_cb) { + ASSERT(inspector->ws_mode); + ASSERT(!inspector->shutting_down || read_cb == nullptr); + inspector->ws_state->close_sent = false; + inspector->ws_state->alloc_cb = alloc_cb; + inspector->ws_state->read_cb = read_cb; + int err = + uv_read_start(reinterpret_cast(&inspector->client), + prepare_buffer, + websockets_data_cb); + if (err < 0) { + close_connection(inspector); + } + return err; +} + +void inspector_read_stop(inspector_socket_t* inspector) { + uv_read_stop(reinterpret_cast(&inspector->client)); + inspector->ws_state->alloc_cb = nullptr; + inspector->ws_state->read_cb = nullptr; +} + +static void generate_accept_string(const char* client_key, char* buffer) { + // Magic string from websockets spec. + const char ws_magic[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + size_t key_len = strlen(client_key); + size_t magic_len = sizeof(ws_magic) - 1; + + char* buf = reinterpret_cast(malloc(key_len + magic_len)); + CHECK_NE(buf, nullptr); + memcpy(buf, client_key, key_len); + memcpy(buf + key_len, ws_magic, magic_len); + char hash[20]; + SHA1((unsigned char*) buf, key_len + magic_len, (unsigned char*) hash); + free(buf); + node::base64_encode(hash, 20, buffer, ACCEPT_KEY_LENGTH); + buffer[ACCEPT_KEY_LENGTH] = '\0'; +} + +static void append(char** value, const char* string, size_t length) { + const size_t INCREMENT = 500; // There should never be more then 1 chunk... + + int current_len = *value ? strlen(*value) : 0; + int new_len = current_len + length; + int adjusted = (new_len / INCREMENT + 1) * INCREMENT; + *value = reinterpret_cast(realloc(*value, adjusted)); + memcpy(*value + current_len, string, length); + (*value)[new_len] = '\0'; +} + +static int header_value_cb(http_parser* parser, const char* at, size_t length) { + char SEC_WEBSOCKET_KEY_HEADER[] = "Sec-WebSocket-Key"; + struct http_parsing_state_s* state = (struct http_parsing_state_s*) + (reinterpret_cast(parser->data))->http_parsing_state; + state->parsing_value = true; + if (state->current_header && strncmp(state->current_header, + SEC_WEBSOCKET_KEY_HEADER, + sizeof(SEC_WEBSOCKET_KEY_HEADER)) == 0) { + append(&state->ws_key, at, length); + } + return 0; +} + +static int header_field_cb(http_parser* parser, const char* at, size_t length) { + struct http_parsing_state_s* state = (struct http_parsing_state_s*) + (reinterpret_cast(parser->data))->http_parsing_state; + if (state->parsing_value) { + state->parsing_value = false; + if (state->current_header) + state->current_header[0] = '\0'; + } + append(&state->current_header, at, length); + return 0; +} + +static int path_cb(http_parser* parser, const char* at, size_t length) { + struct http_parsing_state_s* state = (struct http_parsing_state_s*) + (reinterpret_cast(parser->data))->http_parsing_state; + append(&state->path, at, length); + return 0; +} + +static void handshake_complete(inspector_socket_t* inspector) { + uv_read_stop(reinterpret_cast(&inspector->client)); + handshake_cb callback = inspector->http_parsing_state->callback; + inspector->ws_state = (struct ws_state_s*) malloc(sizeof(struct ws_state_s)); + ASSERT_NE(nullptr, inspector->ws_state); + memset(inspector->ws_state, 0, sizeof(struct ws_state_s)); + inspector->last_read_end = 0; + inspector->ws_mode = true; + callback(inspector, kInspectorHandshakeUpgraded, + inspector->http_parsing_state->path); +} + +static void cleanup_http_parsing_state(struct http_parsing_state_s* state) { + free(state->current_header); + free(state->path); + free(state->ws_key); + free(state); +} + +static void handshake_failed(inspector_socket_t* inspector) { + http_parsing_state_s* state = inspector->http_parsing_state; + const char HANDSHAKE_FAILED_RESPONSE[] = + "HTTP/1.0 400 Bad Request\r\n" + "Content-Type: text/html; charset=UTF-8\r\n\r\n" + "WebSockets request was expected\r\n"; + write_to_client(inspector, HANDSHAKE_FAILED_RESPONSE, + sizeof(HANDSHAKE_FAILED_RESPONSE) - 1); + close_connection(inspector); + inspector->http_parsing_state = nullptr; + state->callback(inspector, kInspectorHandshakeFailed, state->path); +} + +// init_handshake references message_complete_cb +static void init_handshake(inspector_socket_t* inspector); + +static int message_complete_cb(http_parser* parser) { + inspector_socket_t* inspector = + reinterpret_cast(parser->data); + struct http_parsing_state_s* state = + (struct http_parsing_state_s*) inspector->http_parsing_state; + if (parser->method != HTTP_GET) { + handshake_failed(inspector); + } else if (!parser->upgrade) { + if (state->callback(inspector, kInspectorHandshakeHttpGet, state->path)) { + init_handshake(inspector); + } else { + handshake_failed(inspector); + } + } else if (!state->ws_key) { + handshake_failed(inspector); + } else if (state->callback(inspector, kInspectorHandshakeUpgrading, + state->path)) { + char accept_string[ACCEPT_KEY_LENGTH + 1]; + generate_accept_string(state->ws_key, accept_string); + + const char accept_ws_prefix[] = "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: "; + const char accept_ws_suffix[] = "\r\n\r\n"; + // Format has two chars (%s) that are replaced with actual key + char accept_response[sizeof(accept_ws_prefix) - 1 + + sizeof(accept_ws_suffix) - 1 + + ACCEPT_KEY_LENGTH]; + memcpy(accept_response, accept_ws_prefix, sizeof(accept_ws_prefix) - 1); + memcpy(accept_response + sizeof(accept_ws_prefix) - 1, + accept_string, ACCEPT_KEY_LENGTH); + memcpy(accept_response + sizeof(accept_ws_prefix) - 1 + ACCEPT_KEY_LENGTH, + accept_ws_suffix, sizeof(accept_ws_suffix) - 1); + int len = sizeof(accept_response); + if (write_to_client(inspector, accept_response, len) >= 0) { + handshake_complete(inspector); + } else { + state->callback(inspector, kInspectorHandshakeFailed, nullptr); + close_connection(inspector); + } + inspector->http_parsing_state = nullptr; + } else { + handshake_failed(inspector); + } + return 0; +} + +static void data_received_cb(uv_stream_s* client, ssize_t nread, + const uv_buf_t* buf) { +#if DUMP_READS + if (nread >= 0) { + printf("%s (%ld bytes)\n", __FUNCTION__, nread); + dump_hex(buf->base, nread); + } else { + printf("[%s:%d] %s\n", __FUNCTION__, __LINE__, uv_err_name(nread)); + } +#endif + inspector_socket_t* inspector = + reinterpret_cast((client->data)); + http_parsing_state_s* state = inspector->http_parsing_state; + if (nread < 0 || nread == UV_EOF) { + inspector->http_parsing_state->callback(inspector, + kInspectorHandshakeFailed, + nullptr); + close_connection(inspector); + inspector->http_parsing_state = nullptr; + } else { + http_parser* parser = &state->parser; + ssize_t parsed = http_parser_execute(parser, &state->parser_settings, + inspector->buffer, + nread); + if (parsed < nread) { + handshake_failed(inspector); + } + inspector->data_len = 0; + } + + if (inspector->http_parsing_state == nullptr) { + cleanup_http_parsing_state(state); + } +} + +static void init_handshake(inspector_socket_t* inspector) { + http_parsing_state_s* state = inspector->http_parsing_state; + CHECK_NE(state, nullptr); + if (state->current_header) { + state->current_header[0] = '\0'; + } + if (state->ws_key) { + state->ws_key[0] = '\0'; + } + if (state->path) { + state->path[0] = '\0'; + } + http_parser_init(&state->parser, HTTP_REQUEST); + state->parser.data = inspector; + http_parser_settings* settings = &state->parser_settings; + http_parser_settings_init(settings); + settings->on_header_field = header_field_cb; + settings->on_header_value = header_value_cb; + settings->on_message_complete = message_complete_cb; + settings->on_url = path_cb; +} + +int inspector_accept(uv_stream_t* server, inspector_socket_t* inspector, + handshake_cb callback) { + ASSERT_NE(callback, nullptr); + // The only field that users should care about. + void* data = inspector->data; + memset(inspector, 0, sizeof(*inspector)); + inspector->data = data; + + inspector->http_parsing_state = (struct http_parsing_state_s*) + malloc(sizeof(struct http_parsing_state_s)); + ASSERT_NE(nullptr, inspector->http_parsing_state); + memset(inspector->http_parsing_state, 0, sizeof(struct http_parsing_state_s)); + uv_stream_t* client = reinterpret_cast(&inspector->client); + CHECK_NE(client, nullptr); + int err = uv_tcp_init(server->loop, &inspector->client); + + if (err == 0) { + err = uv_accept(server, client); + } + if (err == 0) { + client->data = inspector; + init_handshake(inspector); + inspector->http_parsing_state->callback = callback; + err = uv_read_start(client, prepare_buffer, + data_received_cb); + } + if (err != 0) { + uv_close(reinterpret_cast(client), NULL); + } + return err; +} + +void inspector_write(inspector_socket_t* inspector, const char* data, + size_t len) { + if (inspector->ws_mode) { + std::vector output = encode_frame_hybi17(data, len); + write_to_client(inspector, &output[0], output.size()); + } else { + write_to_client(inspector, data, len); + } +} + +void inspector_close(inspector_socket_t* inspector, + inspector_cb callback) { + // libuv throws assertions when closing stream that's already closed - we + // need to do the same. + ASSERT(!uv_is_closing(reinterpret_cast(&inspector->client))); + ASSERT(!inspector->shutting_down); + inspector->shutting_down = true; + inspector->ws_state->close_cb = callback; + if (inspector->connection_eof) { + close_connection(inspector); + } else { + inspector_read_stop(inspector); + write_to_client(inspector, CLOSE_FRAME, sizeof(CLOSE_FRAME), + on_close_frame_written); + inspector_read_start(inspector, nullptr, nullptr); + } +} + +bool inspector_is_active(const struct inspector_socket_s* inspector) { + const uv_handle_t* client = + reinterpret_cast(&inspector->client); + return !inspector->shutting_down && !uv_is_closing(client); +} diff --git a/src/inspector_socket.h b/src/inspector_socket.h new file mode 100644 index 00000000000..3e52762e715 --- /dev/null +++ b/src/inspector_socket.h @@ -0,0 +1,57 @@ +#ifndef SRC_INSPECTOR_SOCKET_H_ +#define SRC_INSPECTOR_SOCKET_H_ + +#include "http_parser.h" +#include "uv.h" + +enum inspector_handshake_event { + kInspectorHandshakeUpgrading, + kInspectorHandshakeUpgraded, + kInspectorHandshakeHttpGet, + kInspectorHandshakeFailed +}; + +struct inspector_socket_s; + +typedef void (*inspector_cb)(struct inspector_socket_s*, int); +// Notifies as handshake is progressing. Returning false as a response to +// kInspectorHandshakeUpgrading or kInspectorHandshakeHttpGet event will abort +// the connection. inspector_write can be used from the callback. +typedef bool (*handshake_cb)(struct inspector_socket_s*, + enum inspector_handshake_event state, + const char* path); + +struct http_parsing_state_s; +struct ws_state_s; + +struct inspector_socket_s { + void* data; + struct http_parsing_state_s* http_parsing_state; + struct ws_state_s* ws_state; + char* buffer; + size_t buffer_size; + size_t data_len; + size_t last_read_end; + uv_tcp_t client; + bool ws_mode; + bool shutting_down; + bool connection_eof; +}; + +typedef struct inspector_socket_s inspector_socket_t; + +int inspector_accept(uv_stream_t* server, struct inspector_socket_s* inspector, + handshake_cb callback); + +void inspector_close(struct inspector_socket_s* inspector, + inspector_cb callback); + +// Callbacks will receive handles that has inspector in data field... +int inspector_read_start(struct inspector_socket_s* inspector, uv_alloc_cb, + uv_read_cb); +void inspector_read_stop(struct inspector_socket_s* inspector); +void inspector_write(struct inspector_socket_s* inspector, + const char* data, size_t len); +bool inspector_is_active(const struct inspector_socket_s* inspector); + +#endif // SRC_INSPECTOR_SOCKET_H_ diff --git a/src/node.cc b/src/node.cc index cda2bac0962..258ebb596a0 100644 --- a/src/node.cc +++ b/src/node.cc @@ -137,6 +137,9 @@ static bool track_heap_objects = false; static const char* eval_string = nullptr; static unsigned int preload_module_count = 0; static const char** preload_modules = nullptr; +#if HAVE_INSPECTOR +static bool use_inspector = false; +#endif static bool use_debug_agent = false; static bool debug_wait_connect = false; static int debug_port = 5858; @@ -3412,6 +3415,22 @@ static bool ParseDebugOpt(const char* arg) { port = arg + sizeof("--debug-brk=") - 1; } else if (!strncmp(arg, "--debug-port=", sizeof("--debug-port=") - 1)) { port = arg + sizeof("--debug-port=") - 1; +#if HAVE_INSPECTOR + // Specifying both --inspect and --debug means debugging is on, using Chromium + // inspector. + } else if (!strcmp(arg, "--inspect")) { + use_debug_agent = true; + use_inspector = true; + } else if (!strncmp(arg, "--inspect=", sizeof("--inspect=") - 1)) { + use_debug_agent = true; + use_inspector = true; + port = arg + sizeof("--inspect=") - 1; +#else + } else if (!strncmp(arg, "--inspect", sizeof("--inspect") - 1)) { + fprintf(stderr, + "Inspector support is not available with this Node.js build\n"); + return false; +#endif } else { return false; } @@ -3682,10 +3701,19 @@ static void DispatchMessagesDebugAgentCallback(Environment* env) { static void StartDebug(Environment* env, bool wait) { CHECK(!debugger_running); +#if HAVE_INSPECTOR + if (use_inspector) { + env->inspector_agent()->Start(default_platform, debug_port, wait); + debugger_running = true; + } else { +#endif + env->debugger_agent()->set_dispatch_handler( + DispatchMessagesDebugAgentCallback); + debugger_running = env->debugger_agent()->Start(debug_port, wait); +#if HAVE_INSPECTOR + } +#endif - env->debugger_agent()->set_dispatch_handler( - DispatchMessagesDebugAgentCallback); - debugger_running = env->debugger_agent()->Start(debug_port, wait); if (debugger_running == false) { fprintf(stderr, "Starting debugger on port %d failed\n", debug_port); fflush(stderr); @@ -3697,6 +3725,11 @@ static void StartDebug(Environment* env, bool wait) { // Called from the main thread. static void EnableDebug(Environment* env) { CHECK(debugger_running); +#if HAVE_INSPECTOR + if (use_inspector) { + return; + } +#endif // Send message to enable debug in workers HandleScope handle_scope(env->isolate()); @@ -3991,7 +4024,15 @@ static void DebugPause(const FunctionCallbackInfo& args) { static void DebugEnd(const FunctionCallbackInfo& args) { if (debugger_running) { Environment* env = Environment::GetCurrent(args); - env->debugger_agent()->Stop(); +#if HAVE_INSPECTOR + if (use_inspector) { + env->inspector_agent()->Stop(); + } else { +#endif + env->debugger_agent()->Stop(); +#if HAVE_INSPECTOR + } +#endif debugger_running = false; } } @@ -4420,6 +4461,24 @@ static void StartNodeInstance(void* arg) { instance_data->set_exit_code(exit_code); RunAtExit(env); +#if HAVE_INSPECTOR + if (env->inspector_agent()->connected()) { + // Restore signal dispositions, the app is done and is no longer + // capable of handling signals. +#ifdef __POSIX__ + struct sigaction act; + memset(&act, 0, sizeof(act)); + for (unsigned nr = 1; nr < 32; nr += 1) { + if (nr == SIGKILL || nr == SIGSTOP || nr == SIGPROF) + continue; + act.sa_handler = (nr == SIGPIPE) ? SIG_IGN : SIG_DFL; + CHECK_EQ(0, sigaction(nr, &act, nullptr)); + } +#endif + env->inspector_agent()->WaitForDisconnect(); + } +#endif + #if defined(LEAK_SANITIZER) __lsan_do_leak_check(); #endif diff --git a/src/node_internals.h b/src/node_internals.h index 2875f5ac798..64134d9ab8d 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -221,7 +221,7 @@ class ArrayBufferAllocator : public v8::ArrayBuffer::Allocator { // by clearing all callbacks that could handle the error. void ClearFatalExceptionHandlers(Environment* env); -enum NodeInstanceType { MAIN, WORKER }; +enum NodeInstanceType { MAIN, WORKER, REMOTE_DEBUG_SERVER }; class NodeInstanceData { public: @@ -265,6 +265,10 @@ class NodeInstanceData { return node_instance_type_ == WORKER; } + bool is_remote_debug_server() { + return node_instance_type_ == REMOTE_DEBUG_SERVER; + } + int argc() { return argc_; } diff --git a/src/signal_wrap.cc b/src/signal_wrap.cc index 3ee0251f9b2..8d31dbf6233 100644 --- a/src/signal_wrap.cc +++ b/src/signal_wrap.cc @@ -65,6 +65,15 @@ class SignalWrap : public HandleWrap { SignalWrap* wrap; ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); int signum = args[0]->Int32Value(); +#if defined(__POSIX__) && defined(HAVE_INSPECTOR) + if (signum == SIGPROF) { + Environment* env = Environment::GetCurrent(args); + if (env->inspector_agent()->IsStarted()) { + fprintf(stderr, "process.on(SIGPROF) is reserved while debugging\n"); + return; + } + } +#endif int err = uv_signal_start(&wrap->handle_, OnSignal, signum); args.GetReturnValue().Set(err); } diff --git a/test/cctest/test_inspector_socket.cc b/test/cctest/test_inspector_socket.cc new file mode 100644 index 00000000000..ebe4215af5e --- /dev/null +++ b/test/cctest/test_inspector_socket.cc @@ -0,0 +1,864 @@ +#include "inspector_socket.h" + +#include "gtest/gtest.h" + +#define PORT 9444 + +static const int MAX_LOOP_ITERATIONS = 10000; + +#define SPIN_WHILE(condition) \ + { \ + bool timed_out = false; \ + timeout_timer.data = &timed_out; \ + uv_timer_start(&timeout_timer, set_timeout_flag, 5000, 0); \ + while (((condition)) && !timed_out) { \ + uv_run(&loop, UV_RUN_NOWAIT); \ + } \ + ASSERT_FALSE((condition)); \ + uv_timer_stop(&timeout_timer); \ + } + +static uv_timer_t timeout_timer; +static bool connected = false; +static bool inspector_ready = false; +static int handshake_events = 0; +static enum inspector_handshake_event last_event = kInspectorHandshakeHttpGet; +static uv_loop_t loop; +static uv_tcp_t server, client_socket; +static inspector_socket_t inspector; +static char last_path[100]; +static void (*handshake_delegate)(enum inspector_handshake_event state, + const char* path, bool* should_continue); + +struct read_expects { + const char* expected; + size_t expected_len; + size_t pos; + bool read_expected; + bool callback_called; +}; + +static const char HANDSHAKE_REQ[] = "GET /ws/path HTTP/1.1\r\n" + "Host: localhost:9222\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Key: aaa==\r\n" + "Sec-WebSocket-Version: 13\r\n\r\n"; + +static void set_timeout_flag(uv_timer_t* timer) { + *(static_cast(timer->data)) = true; +} + +static void stop_if_stop_path(enum inspector_handshake_event state, + const char* path, bool* cont) { + *cont = path == nullptr || strcmp(path, "/close") != 0; +} + +static bool connected_cb(inspector_socket_t* socket, + enum inspector_handshake_event state, + const char* path) { + inspector_ready = state == kInspectorHandshakeUpgraded; + last_event = state; + if (!path) { + strcpy(last_path, "@@@ Nothing Recieved @@@"); + } else { + strncpy(last_path, path, sizeof(last_path) - 1); + } + handshake_events++; + bool should_continue = true; + handshake_delegate(state, path, &should_continue); + return should_continue; +} + +static void on_new_connection(uv_stream_t* server, int status) { + GTEST_ASSERT_EQ(0, status); + connected = true; + inspector_accept(server, reinterpret_cast(server->data), + connected_cb); +} + +void write_done(uv_write_t* req, int status) { req->data = nullptr; } + +static void do_write(const char* data, int len) { + uv_write_t req; + bool done = false; + req.data = &done; + uv_buf_t buf[1]; + buf[0].base = const_cast(data); + buf[0].len = len; + uv_write(&req, reinterpret_cast(&client_socket), buf, 1, + write_done); + SPIN_WHILE(req.data); +} + +static void buffer_alloc_cb(uv_handle_t* stream, size_t len, uv_buf_t* buf) { + buf->base = static_cast(malloc(len)); + buf->len = len; +} + +static void check_data_cb(read_expects* expectation, ssize_t nread, + const uv_buf_t* buf, bool* retval) { + *retval = false; + EXPECT_TRUE(nread >= 0 && nread != UV_EOF); + ssize_t i; + char c, actual; + ASSERT_TRUE(expectation->expected_len > 0); + for (i = 0; i < nread && expectation->pos <= expectation->expected_len; i++) { + c = expectation->expected[expectation->pos++]; + actual = buf->base[i]; + if (c != actual) { + fprintf(stderr, "Unexpected character at position %ld\n", + expectation->pos - 1); + GTEST_ASSERT_EQ(c, actual); + } + } + GTEST_ASSERT_EQ(i, nread); + free(buf->base); + if (expectation->pos == expectation->expected_len) { + expectation->read_expected = true; + *retval = true; + } +} + +static void check_data_cb(uv_stream_t* stream, ssize_t nread, + const uv_buf_t* buf) { + bool retval = false; + read_expects* expects = static_cast(stream->data); + expects->callback_called = true; + check_data_cb(expects, nread, buf, &retval); + if (retval) { + stream->data = nullptr; + uv_read_stop(stream); + } +} + +static read_expects prepare_expects(const char* data, size_t len) { + read_expects expectation; + expectation.expected = data; + expectation.expected_len = len; + expectation.pos = 0; + expectation.read_expected = false; + expectation.callback_called = false; + return expectation; +} + +static void fail_callback(uv_stream_t* stream, ssize_t nread, + const uv_buf_t* buf) { + if (nread < 0) { + fprintf(stderr, "IO error: %s\n", uv_strerror(nread)); + } else { + fprintf(stderr, "Read %ld bytes\n", nread); + } + ASSERT_TRUE(false); // Shouldn't have been called +} + +static void expect_nothing_on_client() { + int err = uv_read_start(reinterpret_cast(&client_socket), + buffer_alloc_cb, fail_callback); + GTEST_ASSERT_EQ(0, err); + for (int i = 0; i < MAX_LOOP_ITERATIONS; i++) + uv_run(&loop, UV_RUN_NOWAIT); +} + +static void expect_on_client(const char* data, size_t len) { + read_expects expectation = prepare_expects(data, len); + client_socket.data = ℰ + uv_read_start(reinterpret_cast(&client_socket), + buffer_alloc_cb, check_data_cb); + SPIN_WHILE(!expectation.read_expected); +} + +struct expectations { + char* actual_data; + size_t actual_offset; + size_t actual_end; + int err_code; +}; + +static void grow_expects_buffer(uv_handle_t* stream, size_t size, uv_buf_t* b) { + expectations* expects = static_cast( + (static_cast(stream->data))->data); + size_t end = expects->actual_end; + // Grow the buffer in chunks of 64k. + size_t new_length = (end + size + 65535) & ~((size_t) 0xFFFF); + expects->actual_data = + static_cast(realloc(expects->actual_data, new_length)); + *b = uv_buf_init(expects->actual_data + end, new_length - end); +} + +// static void dump_hex(const char* buf, size_t len) { +// const char* ptr = buf; +// const char* end = ptr + len; +// const char* cptr; +// char c; +// int i; + +// while (ptr < end) { +// cptr = ptr; +// for (i = 0; i < 16 && ptr < end; i++) { +// printf("%2.2X ", *(ptr++)); +// } +// for (i = 72 - (i * 4); i > 0; i--) { +// printf(" "); +// } +// for (i = 0; i < 16 && cptr < end; i++) { +// c = *(cptr++); +// printf("%c", (c > 0x19) ? c : '.'); +// } +// printf("\n"); +// } +// printf("\n\n"); +// } + +static void save_read_data(uv_stream_t* stream, ssize_t nread, + const uv_buf_t* buf) { + expectations* expects =static_cast( + (static_cast(stream->data))->data); + expects->err_code = nread < 0 ? nread : 0; + if (nread > 0) { + expects->actual_end += nread; + } +} + +static void setup_inspector_expecting() { + if (inspector.data) { + return; + } + expectations* expects = static_cast(malloc(sizeof(*expects))); + memset(expects, 0, sizeof(*expects)); + inspector.data = expects; + inspector_read_start(&inspector, grow_expects_buffer, save_read_data); +} + +static void expect_on_server(const char* data, size_t len) { + setup_inspector_expecting(); + expectations* expects = static_cast(inspector.data); + for (size_t i = 0; i < len;) { + SPIN_WHILE(expects->actual_offset == expects->actual_end); + for (; i < len && expects->actual_offset < expects->actual_end; i++) { + char actual = expects->actual_data[expects->actual_offset++]; + char expected = data[i]; + if (expected != actual) { + fprintf(stderr, "Character %ld:\n", i); + GTEST_ASSERT_EQ(expected, actual); + } + } + } + expects->actual_end -= expects->actual_offset; + if (!expects->actual_end) { + memmove(expects->actual_data, + expects->actual_data + expects->actual_offset, + expects->actual_end); + } + expects->actual_offset = 0; +} + +static void inspector_record_error_code(uv_stream_t* stream, ssize_t nread, + const uv_buf_t* buf) { + inspector_socket_t *inspector = + reinterpret_cast(stream->data); + // Increment instead of assign is to ensure the function is only called once + *(static_cast(inspector->data)) += nread; +} + +static void expect_server_read_error() { + setup_inspector_expecting(); + expectations* expects = static_cast(inspector.data); + SPIN_WHILE(expects->err_code != UV_EPROTO); +} + +static void expect_handshake() { + const char UPGRADE_RESPONSE[] = + "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: Dt87H1OULVZnSJo/KgMUYI7xPCg=\r\n\r\n"; + expect_on_client(UPGRADE_RESPONSE, sizeof(UPGRADE_RESPONSE) - 1); +} + +static void expect_handshake_failure() { + const char UPGRADE_RESPONSE[] = + "HTTP/1.0 400 Bad Request\r\n" + "Content-Type: text/html; charset=UTF-8\r\n\r\n" + "WebSockets request was expected\r\n"; + expect_on_client(UPGRADE_RESPONSE, sizeof(UPGRADE_RESPONSE) - 1); +} + +static bool waiting_to_close = true; + +void handle_closed(uv_handle_t* handle) { waiting_to_close = false; } + +static void really_close(uv_tcp_t* socket) { + waiting_to_close = true; + if (!uv_is_closing(reinterpret_cast(socket))) { + uv_close(reinterpret_cast(socket), handle_closed); + SPIN_WHILE(waiting_to_close); + } +} + +// Called when the test leaves inspector socket in active state +static void manual_inspector_socket_cleanup() { + EXPECT_EQ(0, uv_is_active( + reinterpret_cast(&inspector.client))); + free(inspector.ws_state); + free(inspector.http_parsing_state); + free(inspector.buffer); + inspector.buffer = nullptr; +} + +static void on_connection(uv_connect_t* connect, int status) { + GTEST_ASSERT_EQ(0, status); + connect->data = connect; +} + +class InspectorSocketTest : public ::testing::Test { +protected: + virtual void SetUp() { + handshake_delegate = stop_if_stop_path; + handshake_events = 0; + connected = false; + inspector_ready = false; + last_event = kInspectorHandshakeHttpGet; + uv_loop_init(&loop); + memset(&inspector, 0, sizeof(inspector)); + memset(&server, 0, sizeof(server)); + memset(&client_socket, 0, sizeof(client_socket)); + server.data = &inspector; + sockaddr_in addr; + uv_timer_init(&loop, &timeout_timer); + uv_tcp_init(&loop, &server); + uv_tcp_init(&loop, &client_socket); + uv_ip4_addr("localhost", PORT, &addr); + uv_tcp_bind(&server, reinterpret_cast(&addr), 0); + int err = uv_listen(reinterpret_cast(&server), + 0, on_new_connection); + GTEST_ASSERT_EQ(0, err); + uv_connect_t connect; + connect.data = nullptr; + uv_tcp_connect(&connect, &client_socket, + reinterpret_cast(&addr), on_connection); + uv_tcp_nodelay(&client_socket, 1); // The buffering messes up the test + SPIN_WHILE(!connect.data || !connected); + really_close(&server); + uv_unref(reinterpret_cast(&server)); + } + + virtual void TearDown() { + really_close(&client_socket); + for (int i = 0; i < MAX_LOOP_ITERATIONS; i++) + uv_run(&loop, UV_RUN_NOWAIT); + EXPECT_EQ(nullptr, inspector.buffer); + uv_stop(&loop); + int err = uv_run(&loop, UV_RUN_ONCE); + if (err != 0) { + uv_print_active_handles(&loop, stderr); + } + EXPECT_EQ(0, err); + expectations* expects = static_cast(inspector.data); + if (expects != nullptr) { + GTEST_ASSERT_EQ(expects->actual_end, expects->actual_offset); + free(expects->actual_data); + expects->actual_data = nullptr; + free(expects); + inspector.data = nullptr; + } + uv_loop_close(&loop); + } +}; + +TEST_F(InspectorSocketTest, ReadsAndWritesInspectorMessage) { + ASSERT_TRUE(connected); + ASSERT_FALSE(inspector_ready); + do_write(const_cast(HANDSHAKE_REQ), sizeof(HANDSHAKE_REQ) - 1); + SPIN_WHILE(!inspector_ready); + expect_handshake(); + + // 2. Brief exchange + const char SERVER_MESSAGE[] = "abcd"; + const char CLIENT_FRAME[] = {'\x81', '\x04', 'a', 'b', 'c', 'd'}; + inspector_write(&inspector, SERVER_MESSAGE, sizeof(SERVER_MESSAGE) - 1); + expect_on_client(CLIENT_FRAME, sizeof(CLIENT_FRAME)); + + const char SERVER_FRAME[] = {'\x81', '\x84', '\x7F', '\xC2', '\x66', + '\x31', '\x4E', '\xF0', '\x55', '\x05'}; + const char CLIENT_MESSAGE[] = "1234"; + do_write(SERVER_FRAME, sizeof(SERVER_FRAME)); + expect_on_server(CLIENT_MESSAGE, sizeof(CLIENT_MESSAGE) - 1); + + // 3. Close + const char CLIENT_CLOSE_FRAME[] = {'\x88', '\x80', '\x2D', + '\x0E', '\x1E', '\xFA'}; + const char SERVER_CLOSE_FRAME[] = {'\x88', '\x00'}; + do_write(CLIENT_CLOSE_FRAME, sizeof(CLIENT_CLOSE_FRAME)); + expect_on_client(SERVER_CLOSE_FRAME, sizeof(SERVER_CLOSE_FRAME)); + GTEST_ASSERT_EQ(0, uv_is_active( + reinterpret_cast(&client_socket))); +} + +TEST_F(InspectorSocketTest, BufferEdgeCases) { + + do_write(const_cast(HANDSHAKE_REQ), sizeof(HANDSHAKE_REQ) - 1); + expect_handshake(); + + const char MULTIPLE_REQUESTS[] = { + '\x81', '\xCB', '\x76', '\xCA', '\x06', '\x0C', '\x0D', '\xE8', '\x6F', + '\x68', '\x54', '\xF0', '\x37', '\x3E', '\x5A', '\xE8', '\x6B', '\x69', + '\x02', '\xA2', '\x69', '\x68', '\x54', '\xF0', '\x24', '\x5B', '\x19', + '\xB8', '\x6D', '\x69', '\x04', '\xE4', '\x75', '\x69', '\x02', '\x8B', + '\x73', '\x78', '\x19', '\xA9', '\x69', '\x62', '\x18', '\xAF', '\x65', + '\x78', '\x22', '\xA5', '\x51', '\x63', '\x04', '\xA1', '\x63', '\x7E', + '\x05', '\xE8', '\x2A', '\x2E', '\x06', '\xAB', '\x74', '\x6D', '\x1B', + '\xB9', '\x24', '\x36', '\x0D', '\xE8', '\x70', '\x6D', '\x1A', '\xBF', + '\x63', '\x2E', '\x4C', '\xBE', '\x74', '\x79', '\x13', '\xB7', '\x7B', + '\x81', '\xA2', '\xFC', '\x9E', '\x0D', '\x15', '\x87', '\xBC', '\x64', + '\x71', '\xDE', '\xA4', '\x3C', '\x26', '\xD0', '\xBC', '\x60', '\x70', + '\x88', '\xF6', '\x62', '\x71', '\xDE', '\xA4', '\x2F', '\x42', '\x93', + '\xEC', '\x66', '\x70', '\x8E', '\xB0', '\x68', '\x7B', '\x9D', '\xFC', + '\x61', '\x70', '\xDE', '\xE3', '\x81', '\xA4', '\x4E', '\x37', '\xB0', + '\x22', '\x35', '\x15', '\xD9', '\x46', '\x6C', '\x0D', '\x81', '\x16', + '\x62', '\x15', '\xDD', '\x47', '\x3A', '\x5F', '\xDF', '\x46', '\x6C', + '\x0D', '\x92', '\x72', '\x3C', '\x58', '\xD6', '\x4B', '\x22', '\x52', + '\xC2', '\x0C', '\x2B', '\x59', '\xD1', '\x40', '\x22', '\x52', '\x92', + '\x5F', '\x81', '\xCB', '\xCD', '\xF0', '\x30', '\xC5', '\xB6', '\xD2', + '\x59', '\xA1', '\xEF', '\xCA', '\x01', '\xF0', '\xE1', '\xD2', '\x5D', + '\xA0', '\xB9', '\x98', '\x5F', '\xA1', '\xEF', '\xCA', '\x12', '\x95', + '\xBF', '\x9F', '\x56', '\xAC', '\xA1', '\x95', '\x42', '\xEB', '\xBE', + '\x95', '\x44', '\x96', '\xAC', '\x9D', '\x40', '\xA9', '\xA4', '\x9E', + '\x57', '\x8C', '\xA3', '\x84', '\x55', '\xB7', '\xBB', '\x91', '\x5C', + '\xE7', '\xE1', '\xD2', '\x40', '\xA4', '\xBF', '\x91', '\x5D', '\xB6', + '\xEF', '\xCA', '\x4B', '\xE7', '\xA4', '\x9E', '\x44', '\xA0', '\xBF', + '\x86', '\x51', '\xA9', '\xEF', '\xCA', '\x01', '\xF5', '\xFD', '\x8D', + '\x4D', '\x81', '\xA9', '\x74', '\x6B', '\x72', '\x43', '\x0F', '\x49', + '\x1B', '\x27', '\x56', '\x51', '\x43', '\x75', '\x58', '\x49', '\x1F', + '\x26', '\x00', '\x03', '\x1D', '\x27', '\x56', '\x51', '\x50', '\x10', + '\x11', '\x19', '\x04', '\x2A', '\x17', '\x0E', '\x25', '\x2C', '\x06', + '\x00', '\x17', '\x31', '\x5A', '\x0E', '\x1C', '\x22', '\x16', '\x07', + '\x17', '\x61', '\x09', '\x81', '\xB8', '\x7C', '\x1A', '\xEA', '\xEB', + '\x07', '\x38', '\x83', '\x8F', '\x5E', '\x20', '\xDB', '\xDC', '\x50', + '\x38', '\x87', '\x8E', '\x08', '\x72', '\x85', '\x8F', '\x5E', '\x20', + '\xC8', '\xA5', '\x19', '\x6E', '\x9D', '\x84', '\x0E', '\x71', '\xC4', + '\x88', '\x1D', '\x74', '\xAF', '\x86', '\x09', '\x76', '\x8B', '\x9F', + '\x19', '\x54', '\x8F', '\x9F', '\x0B', '\x75', '\x98', '\x80', '\x3F', + '\x75', '\x84', '\x8F', '\x15', '\x6E', '\x83', '\x84', '\x12', '\x69', + '\xC8', '\x96'}; + + const char EXPECT[] = { + "{\"id\":12,\"method\":\"Worker.setAutoconnectToWorkers\"," + "\"params\":{\"value\":true}}" + "{\"id\":13,\"method\":\"Worker.enable\"}" + "{\"id\":14,\"method\":\"Profiler.enable\"}" + "{\"id\":15,\"method\":\"Profiler.setSamplingInterval\"," + "\"params\":{\"interval\":100}}" + "{\"id\":16,\"method\":\"ServiceWorker.enable\"}" + "{\"id\":17,\"method\":\"Network.canEmulateNetworkConditions\"}"}; + + do_write(MULTIPLE_REQUESTS, sizeof(MULTIPLE_REQUESTS)); + expect_on_server(EXPECT, sizeof(EXPECT) - 1); + inspector_read_stop(&inspector); + manual_inspector_socket_cleanup(); +} + +TEST_F(InspectorSocketTest, AcceptsRequestInSeveralWrites) { + ASSERT_TRUE(connected); + ASSERT_FALSE(inspector_ready); + // Specifically, break up the request in the "Sec-WebSocket-Key" header name + // and value + const int write1 = 95; + const int write2 = 5; + const int write3 = sizeof(HANDSHAKE_REQ) - write1 - write2 - 1; + do_write(const_cast(HANDSHAKE_REQ), write1); + ASSERT_FALSE(inspector_ready); + do_write(const_cast(HANDSHAKE_REQ) + write1, write2); + ASSERT_FALSE(inspector_ready); + do_write(const_cast(HANDSHAKE_REQ) + write1 + write2, write3); + SPIN_WHILE(!inspector_ready); + expect_handshake(); + inspector_read_stop(&inspector); + GTEST_ASSERT_EQ(uv_is_active(reinterpret_cast(&client_socket)), 0); + manual_inspector_socket_cleanup(); +} + +TEST_F(InspectorSocketTest, ExtraTextBeforeRequest) { + last_event = kInspectorHandshakeUpgraded; + char UNCOOL_BRO[] = "Uncool, bro: Text before the first req\r\n"; + do_write(const_cast(UNCOOL_BRO), sizeof(UNCOOL_BRO) - 1); + + ASSERT_FALSE(inspector_ready); + do_write(const_cast(HANDSHAKE_REQ), sizeof(HANDSHAKE_REQ) - 1); + SPIN_WHILE(last_event != kInspectorHandshakeFailed); + expect_handshake_failure(); + EXPECT_EQ(uv_is_active(reinterpret_cast(&client_socket)), 0); + EXPECT_EQ(uv_is_active(reinterpret_cast(&socket)), 0); +} + +TEST_F(InspectorSocketTest, ExtraLettersBeforeRequest) { + char UNCOOL_BRO[] = "Uncool!!"; + do_write(const_cast(UNCOOL_BRO), sizeof(UNCOOL_BRO) - 1); + + ASSERT_FALSE(inspector_ready); + do_write(const_cast(HANDSHAKE_REQ), sizeof(HANDSHAKE_REQ) - 1); + SPIN_WHILE(last_event != kInspectorHandshakeFailed); + expect_handshake_failure(); + EXPECT_EQ(uv_is_active(reinterpret_cast(&client_socket)), 0); + EXPECT_EQ(uv_is_active(reinterpret_cast(&socket)), 0); +} + +TEST_F(InspectorSocketTest, RequestWithoutKey) { + const char BROKEN_REQUEST[] = "GET / HTTP/1.1\r\n" + "Host: localhost:9222\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Version: 13\r\n\r\n"; + ; + + do_write(const_cast(BROKEN_REQUEST), sizeof(BROKEN_REQUEST) - 1); + SPIN_WHILE(last_event != kInspectorHandshakeFailed); + expect_handshake_failure(); + EXPECT_EQ(uv_is_active(reinterpret_cast(&client_socket)), 0); + EXPECT_EQ(uv_is_active(reinterpret_cast(&socket)), 0); +} + +TEST_F(InspectorSocketTest, KillsConnectionOnProtocolViolation) { + ASSERT_TRUE(connected); + ASSERT_FALSE(inspector_ready); + do_write(const_cast(HANDSHAKE_REQ), sizeof(HANDSHAKE_REQ) - 1); + SPIN_WHILE(!inspector_ready); + ASSERT_TRUE(inspector_ready); + expect_handshake(); + const char SERVER_FRAME[] = "I'm not a good WS frame. Nope!"; + do_write(SERVER_FRAME, sizeof(SERVER_FRAME)); + expect_server_read_error(); + GTEST_ASSERT_EQ(uv_is_active(reinterpret_cast(&client_socket)), 0); +} + +TEST_F(InspectorSocketTest, CanStopReadingFromInspector) { + ASSERT_TRUE(connected); + ASSERT_FALSE(inspector_ready); + do_write(const_cast(HANDSHAKE_REQ), sizeof(HANDSHAKE_REQ) - 1); + expect_handshake(); + ASSERT_TRUE(inspector_ready); + + // 2. Brief exchange + const char SERVER_FRAME[] = {'\x81', '\x84', '\x7F', '\xC2', '\x66', + '\x31', '\x4E', '\xF0', '\x55', '\x05'}; + const char CLIENT_MESSAGE[] = "1234"; + do_write(SERVER_FRAME, sizeof(SERVER_FRAME)); + expect_on_server(CLIENT_MESSAGE, sizeof(CLIENT_MESSAGE) - 1); + + inspector_read_stop(&inspector); + do_write(SERVER_FRAME, sizeof(SERVER_FRAME)); + GTEST_ASSERT_EQ(uv_is_active( + reinterpret_cast(&client_socket)), 0); + manual_inspector_socket_cleanup(); +} + +static bool inspector_closed; + +void inspector_closed_cb(inspector_socket_t *inspector, int code) { + inspector_closed = true; +} + +TEST_F(InspectorSocketTest, CloseDoesNotNotifyReadCallback) { + inspector_closed = false; + do_write(const_cast(HANDSHAKE_REQ), sizeof(HANDSHAKE_REQ) - 1); + expect_handshake(); + + int error_code = 0; + inspector.data = &error_code; + inspector_read_start(&inspector, buffer_alloc_cb, + inspector_record_error_code); + inspector_close(&inspector, inspector_closed_cb); + char CLOSE_FRAME[] = {'\x88', '\x00'}; + expect_on_client(CLOSE_FRAME, sizeof(CLOSE_FRAME)); + ASSERT_FALSE(inspector_closed); + const char CLIENT_CLOSE_FRAME[] = {'\x88', '\x80', '\x2D', + '\x0E', '\x1E', '\xFA'}; + do_write(CLIENT_CLOSE_FRAME, sizeof(CLIENT_CLOSE_FRAME)); + EXPECT_NE(UV_EOF, error_code); + SPIN_WHILE(!inspector_closed); + inspector.data = nullptr; +} + +TEST_F(InspectorSocketTest, CloseWorksWithoutReadEnabled) { + inspector_closed = false; + do_write(const_cast(HANDSHAKE_REQ), sizeof(HANDSHAKE_REQ) - 1); + expect_handshake(); + inspector_close(&inspector, inspector_closed_cb); + char CLOSE_FRAME[] = {'\x88', '\x00'}; + expect_on_client(CLOSE_FRAME, sizeof(CLOSE_FRAME)); + ASSERT_FALSE(inspector_closed); + const char CLIENT_CLOSE_FRAME[] = {'\x88', '\x80', '\x2D', + '\x0E', '\x1E', '\xFA'}; + do_write(CLIENT_CLOSE_FRAME, sizeof(CLIENT_CLOSE_FRAME)); + SPIN_WHILE(!inspector_closed); +} + +// Make sure buffering works +static void send_in_chunks(const char* data, size_t len) { + const int step = 7; + size_t i = 0; + // Do not send it all at once - test the buffering! + for (; i < len - step; i += step) { + do_write(data + i, step); + } + if (i < len) { + do_write(data + i, len - i); + } +} + +static const char TEST_SUCCESS[] = "Test Success\n\n"; + +static void ReportsHttpGet_handshake(enum inspector_handshake_event state, + const char* path, bool* cont) { + *cont = true; + enum inspector_handshake_event expected_state = kInspectorHandshakeHttpGet; + const char* expected_path; + switch (handshake_events) { + case 1: + expected_path = "/some/path"; + break; + case 2: + expected_path = "/respond/withtext"; + inspector_write(&inspector, TEST_SUCCESS, sizeof(TEST_SUCCESS) - 1); + break; + case 3: + expected_path = "/some/path2"; + break; + case 5: + expected_state = kInspectorHandshakeFailed; + case 4: + expected_path = "/close"; + *cont = false; + break; + default: + expected_path = nullptr; + ASSERT_TRUE(false); + } + EXPECT_EQ(expected_state, state); + EXPECT_STREQ(expected_path, path); +} + +TEST_F(InspectorSocketTest, ReportsHttpGet) { + handshake_delegate = ReportsHttpGet_handshake; + + const char GET_REQ[] = "GET /some/path HTTP/1.1\r\n" + "Host: localhost:9222\r\n" + "Sec-WebSocket-Key: aaa==\r\n" + "Sec-WebSocket-Version: 13\r\n\r\n"; + send_in_chunks(GET_REQ, sizeof(GET_REQ) - 1); + + expect_nothing_on_client(); + + const char WRITE_REQUEST[] = "GET /respond/withtext HTTP/1.1\r\n" + "Host: localhost:9222\r\n\r\n"; + send_in_chunks(WRITE_REQUEST, sizeof(WRITE_REQUEST) - 1); + + expect_on_client(TEST_SUCCESS, sizeof(TEST_SUCCESS) - 1); + + const char GET_REQS[] = "GET /some/path2 HTTP/1.1\r\n" + "Host: localhost:9222\r\n" + "Sec-WebSocket-Key: aaa==\r\n" + "Sec-WebSocket-Version: 13\r\n\r\n" + "GET /close HTTP/1.1\r\n" + "Host: localhost:9222\r\n" + "Sec-WebSocket-Key: aaa==\r\n" + "Sec-WebSocket-Version: 13\r\n\r\n"; + send_in_chunks(GET_REQS, sizeof(GET_REQS) - 1); + + expect_handshake_failure(); + EXPECT_EQ(5, handshake_events); +} + +static void +HandshakeCanBeCanceled_handshake(enum inspector_handshake_event state, + const char* path, bool* cont) { + switch (handshake_events - 1) { + case 0: + EXPECT_EQ(kInspectorHandshakeUpgrading, state); + break; + case 1: + EXPECT_EQ(kInspectorHandshakeFailed, state); + break; + default: + EXPECT_TRUE(false); + break; + } + EXPECT_STREQ("/ws/path", path); + *cont = false; +} + +TEST_F(InspectorSocketTest, HandshakeCanBeCanceled) { + handshake_delegate = HandshakeCanBeCanceled_handshake; + + do_write(const_cast(HANDSHAKE_REQ), sizeof(HANDSHAKE_REQ) - 1); + + expect_handshake_failure(); + EXPECT_EQ(2, handshake_events); +} + +static void GetThenHandshake_handshake(enum inspector_handshake_event state, + const char* path, bool* cont) { + *cont = true; + const char* expected_path = "/ws/path"; + switch (handshake_events - 1) { + case 0: + EXPECT_EQ(kInspectorHandshakeHttpGet, state); + expected_path = "/respond/withtext"; + inspector_write(&inspector, TEST_SUCCESS, sizeof(TEST_SUCCESS) - 1); + break; + case 1: + EXPECT_EQ(kInspectorHandshakeUpgrading, state); + break; + case 2: + EXPECT_EQ(kInspectorHandshakeUpgraded, state); + break; + default: + EXPECT_TRUE(false); + break; + } + EXPECT_STREQ(expected_path, path); +} + +TEST_F(InspectorSocketTest, GetThenHandshake) { + handshake_delegate = GetThenHandshake_handshake; + const char WRITE_REQUEST[] = "GET /respond/withtext HTTP/1.1\r\n" + "Host: localhost:9222\r\n\r\n"; + send_in_chunks(WRITE_REQUEST, sizeof(WRITE_REQUEST) - 1); + + expect_on_client(TEST_SUCCESS, sizeof(TEST_SUCCESS) - 1); + + do_write(const_cast(HANDSHAKE_REQ), sizeof(HANDSHAKE_REQ) - 1); + expect_handshake(); + EXPECT_EQ(3, handshake_events); + manual_inspector_socket_cleanup(); +} + +static void WriteBeforeHandshake_close_cb(uv_handle_t* handle) { + *(static_cast(handle->data)) = true; +} + +TEST_F(InspectorSocketTest, WriteBeforeHandshake) { + const char MESSAGE1[] = "Message 1"; + const char MESSAGE2[] = "Message 2"; + const char EXPECTED[] = "Message 1Message 2"; + + inspector_write(&inspector, MESSAGE1, sizeof(MESSAGE1) - 1); + inspector_write(&inspector, MESSAGE2, sizeof(MESSAGE2) - 1); + expect_on_client(EXPECTED, sizeof(EXPECTED) - 1); + bool flag = false; + client_socket.data = &flag; + uv_close(reinterpret_cast(&client_socket), + WriteBeforeHandshake_close_cb); + SPIN_WHILE(!flag); +} + +static void CleanupSocketAfterEOF_close_cb(inspector_socket_t* inspector, + int status) { + *(static_cast(inspector->data)) = true; +} + +static void CleanupSocketAfterEOF_read_cb(uv_stream_t* stream, ssize_t nread, + const uv_buf_t* buf) { + EXPECT_EQ(UV_EOF, nread); + inspector_socket_t* insp = + reinterpret_cast(stream->data); + inspector_close(insp, CleanupSocketAfterEOF_close_cb); +} + +TEST_F(InspectorSocketTest, CleanupSocketAfterEOF) { + do_write(const_cast(HANDSHAKE_REQ), sizeof(HANDSHAKE_REQ) - 1); + expect_handshake(); + + inspector_read_start(&inspector, buffer_alloc_cb, + CleanupSocketAfterEOF_read_cb); + + for (int i = 0; i < MAX_LOOP_ITERATIONS; ++i) { + uv_run(&loop, UV_RUN_NOWAIT); + } + + uv_close(reinterpret_cast(&client_socket), nullptr); + bool flag = false; + inspector.data = &flag; + SPIN_WHILE(!flag); + inspector.data = nullptr; +} + +TEST_F(InspectorSocketTest, EOFBeforeHandshake) { + const char MESSAGE[] = "We'll send EOF afterwards"; + inspector_write(&inspector, MESSAGE, sizeof(MESSAGE) - 1); + expect_on_client(MESSAGE, sizeof(MESSAGE) - 1); + uv_close(reinterpret_cast(&client_socket), nullptr); + SPIN_WHILE(last_event != kInspectorHandshakeFailed); +} + +static void fill_message(char* buffer, size_t len) { + buffer[len - 1] = '\0'; + for (size_t i = 0; i < len - 1; i++) { + buffer[i] = 'a' + (i % ('z' - 'a')); + } +} + +static void mask_message(const char* message, + char* buffer, const char mask[]) { + const size_t mask_len = 4; + int i = 0; + while (*message != '\0') { + *buffer++ = *message++ ^ mask[i++ % mask_len]; + } +} + +TEST_F(InspectorSocketTest, Send1Mb) { + ASSERT_TRUE(connected); + ASSERT_FALSE(inspector_ready); + do_write(const_cast(HANDSHAKE_REQ), sizeof(HANDSHAKE_REQ) - 1); + SPIN_WHILE(!inspector_ready); + expect_handshake(); + + const size_t message_len = 1000000; + + // 2. Brief exchange + char* message = static_cast(malloc(message_len + 1)); + fill_message(message, message_len + 1); + + // 1000000 is 0xF4240 hex + const char EXPECTED_FRAME_HEADER[] = { + '\x81', '\x7f', '\x00', '\x00', '\x00', '\x00', '\x00', '\x0F', + '\x42', '\x40' + }; + char* expected = + static_cast(malloc(sizeof(EXPECTED_FRAME_HEADER) + message_len)); + + memcpy(expected, EXPECTED_FRAME_HEADER, sizeof(EXPECTED_FRAME_HEADER)); + memcpy(expected + sizeof(EXPECTED_FRAME_HEADER), message, message_len); + + inspector_write(&inspector, message, message_len); + expect_on_client(expected, sizeof(EXPECTED_FRAME_HEADER) + message_len); + + char MASK[4] = {'W', 'h', 'O', 'a'}; + + const char FRAME_TO_SERVER_HEADER[] = { + '\x81', '\xff', '\x00', '\x00', '\x00', '\x00', '\x00', '\x0F', + '\x42', '\x40', MASK[0], MASK[1], MASK[2], MASK[3] + }; + + const size_t outgoing_len = sizeof(FRAME_TO_SERVER_HEADER) + message_len; + char* outgoing = static_cast(malloc(outgoing_len)); + memcpy(outgoing, FRAME_TO_SERVER_HEADER, sizeof(FRAME_TO_SERVER_HEADER)); + mask_message(message, outgoing + sizeof(FRAME_TO_SERVER_HEADER), MASK); + + setup_inspector_expecting(); // Buffer on the client side. + do_write(outgoing, outgoing_len); + expect_on_server(message, message_len); + + // 3. Close + const char CLIENT_CLOSE_FRAME[] = {'\x88', '\x80', '\x2D', + '\x0E', '\x1E', '\xFA'}; + const char SERVER_CLOSE_FRAME[] = {'\x88', '\x00'}; + do_write(CLIENT_CLOSE_FRAME, sizeof(CLIENT_CLOSE_FRAME)); + expect_on_client(SERVER_CLOSE_FRAME, sizeof(SERVER_CLOSE_FRAME)); + GTEST_ASSERT_EQ(0, uv_is_active( + reinterpret_cast(&client_socket))); + free(outgoing); + free(expected); + free(message); +}