From 0995140a90f7c13a08339e08edc98604367028f6 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. This code is currently behind a --with-inspector configure flag. --- INSPECTOR_README.md | 19 + common.gypi | 2 + configure | 6 + lib/internal/bootstrap_node.js | 4 + node.gyp | 27 +- src/env-inl.h | 3 + src/env.h | 12 + src/inspector_agent.cc | 515 +++++++++++++++++++++ src/inspector_agent.h | 94 ++++ src/inspector_socket.cc | 762 ++++++++++++++++++++++++++++++++ src/inspector_socket.h | 65 +++ src/node.cc | 63 ++- src/node_internals.h | 6 +- src/signal_wrap.cc | 9 + test/cctest/inspector_socket.cc | 732 ++++++++++++++++++++++++++++++ 15 files changed, 2313 insertions(+), 6 deletions(-) create mode 100644 INSPECTOR_README.md 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/inspector_socket.cc diff --git a/INSPECTOR_README.md b/INSPECTOR_README.md new file mode 100644 index 00000000000000..8c7c6d4eec83a8 --- /dev/null +++ b/INSPECTOR_README.md @@ -0,0 +1,19 @@ +V8 Inspector Integration for Node.js +==================================== + +V8 Inspector integration allows attaching Chrome DevTools to Node.js +instances for debugging and profiling. + +## Building + +To enable V8 Inspector integration, run configure script with `--with-inspector` +flag. Afterwards, use `make` to build Node.js as usual. + +## Running + +V8 Inspector can be enabled by passing `--inspect` flag when starting Node.js +application. It is also possible to supply a custom port with that flag, e.g. +`--inspect=9222` will expect DevTools connection on the port 9222. + +To break on the first line of the application code, provide the `--debug-brk` +flag in addition to `--inspect`. diff --git a/common.gypi b/common.gypi index 8da603d00f618e..e0f7ce0c926316 100644 --- a/common.gypi +++ b/common.gypi @@ -324,6 +324,7 @@ ['_type!="static_library"', { 'xcode_settings': { 'OTHER_LDFLAGS': [ + '-stdlib=libc++', '-Wl,-no_pie', '-Wl,-search_paths_first', ], @@ -341,6 +342,7 @@ 'xcode_settings': { 'GCC_VERSION': 'com.apple.compilers.llvm.clang.1_0', 'CLANG_CXX_LANGUAGE_STANDARD': 'gnu++0x', # -std=gnu++0x + 'OTHER_CPLUSPLUSFLAGS' : ['-stdlib=libc++'], }, }], ], diff --git a/configure b/configure index 983ae070d02431..b28e14e07dac94 100755 --- a/configure +++ b/configure @@ -408,6 +408,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('--with-inspector', + action='store_true', + dest='inspector', + help='enable experimental V8 inspector support') + (options, args) = parser.parse_args() # Expand ~ in the install prefix now, it gets written to multiple files. @@ -806,6 +811,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(options.inspector) def configure_library(lib, output): shared_lib = 'shared_' + lib diff --git a/lib/internal/bootstrap_node.js b/lib/internal/bootstrap_node.js index 7143ff2720d4cb..56cdca5796d3b3 100644 --- a/lib/internal/bootstrap_node.js +++ b/lib/internal/bootstrap_node.js @@ -81,6 +81,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 86dffef5c6716c..7aec75cef40731 100644 --- a/node.gyp +++ b/node.gyp @@ -117,7 +117,7 @@ 'tools/msvs/genfiles', 'deps/uv/src/ares', '<(SHARED_INTERMEDIATE_DIR)', # for node_natives.h - 'deps/v8' # include/v8_platform.h + 'deps/v8', # include/v8_platform.h ], 'sources': [ @@ -250,6 +250,26 @@ '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 + ], + }], [ 'node_use_openssl=="true"', { 'defines': [ 'HAVE_OPENSSL=1' ], 'sources': [ @@ -690,7 +710,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' ], @@ -708,7 +731,9 @@ 'GTEST_DONT_DEFINE_ASSERT_NE=1', ], 'sources': [ + 'src/inspector_socket.cc', 'test/cctest/util.cc', + 'test/cctest/inspector_socket.cc', ], } ], # end targets diff --git a/src/env-inl.h b/src/env-inl.h index 2c6248ae608306..40c6005bc86a6f 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -223,6 +223,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 afbade5dd81e70..c4b58a0da6dcfc 100644 --- a/src/env.h +++ b/src/env.h @@ -3,6 +3,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" @@ -547,6 +550,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; @@ -584,6 +593,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 00000000000000..95dd153cdf9bb0 --- /dev/null +++ b/src/inspector_agent.cc @@ -0,0 +1,515 @@ +#include "inspector_agent.h" + +// Avoid conflicts between Blink and Node macros +// TODO(eostroukhov): Remove once the Blink code switches to STL pointers +#pragma push_macro("ASSERT") +#pragma push_macro("NO_RETURN") +#undef ASSERT +#undef NO_RETURN + +#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" + +#pragma pop_macro("NO_RETURN") +#pragma pop_macro("ASSERT") + +#include "env.h" +#include "env-inl.h" +#include "node.h" +#include "node_version.h" +#include "v8-platform.h" +#include "util.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 { + +const char DEVTOOLS_PATH[] = "/node"; + +void PrintDebuggerReadyMessage(int port) { + fprintf(stderr, "Debugger listening on port %d. " + "To start debugging, open 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 DisposeAsyncCb(uv_handle_t* handle) { + free(handle); +} + +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 = reinterpret_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(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); + size_t len = snprintf(buffer, sizeof(buffer), LIST_RESPONSE_TEMPLATE, + getpid(), title, DEVTOOLS_PATH); + ASSERT(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 node { +namespace inspector { + +using blink::protocol::DictionaryValue; +using blink::protocol::String16; + +void InterruptCallback(v8::Isolate*, void* agent) { + reinterpret_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, + PassOwnPtr message) + override { + sendMessageToFrontend(message); + } + + virtual void sendProtocolNotification(PassOwnPtr message) + override { + sendMessageToFrontend(message); + } + + virtual void flush() override { } + + void sendMessageToFrontend(PassOwnPtr message) { + agent_->Write(message->toJSONString()); + } + + Agent* const agent_; +}; + +class AsyncWriteRequest { + public: + AsyncWriteRequest(Agent* agent, const String16& message) : + agent_(agent), message_(message) {} + void perform() { + inspector_socket_t* socket = agent_->client_socket_; + if (socket) { + inspector_write(socket, message_.utf8().c_str(), message_.length()); + } + } + private: + Agent* const agent_; + const String16 message_; +}; + +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 contextGroupId) override { + if (running_nested_loop_) + return; + 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), + 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_, + reinterpret_cast(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() { + inspector_->runMessageLoopOnPause(0); +} + +// static +void Agent::ThreadCbIO(Agent* agent) { + agent->WorkerRunIO(); +} + +// static +void Agent::OnSocketConnectionIO(uv_stream_t* server, int status) { + if (status == 0) { + inspector_socket_t* socket = reinterpret_cast( + malloc(sizeof(inspector_socket_t))); + ASSERT_NE(nullptr, socket); + memset(socket, 0, sizeof(inspector_socket_t)); + 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 = reinterpret_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: + ASSERT(false); + } +} + +// static +void Agent::OnRemoteDataIO(uv_stream_t* stream, + ssize_t read, + const uv_buf_t* b) { + inspector_socket_t* socket = + reinterpret_cast(stream->data); + Agent* agent = reinterpret_cast(socket->data); + if (read > 0) { + uv_mutex_lock(&agent->queue_lock_); + blink::protocol::String16 str(b->base, read - 1); + agent->message_queue_.push_back(str); + uv_mutex_unlock(&agent->queue_lock_); + 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(blink::protocol::String16("\"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_); +} + +// static +void Agent::WriteCbIO(uv_async_t* async) { + auto req = reinterpret_cast(async->data); + req->perform(); + delete req; + uv_close(reinterpret_cast(async), DisposeAsyncCb); +} + +void Agent::WorkerRunIO() { + int err; + sockaddr_in addr; + uv_tcp_t server; + uv_tcp_init(&child_loop_, &server); + uv_ip4_addr("0.0.0.0", port_, &addr); + server.data = this; + err = uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0); + if (err == 0) { + err = uv_listen(reinterpret_cast(&server), 0, + OnSocketConnectionIO); + } + if (err == 0) { + PrintDebuggerReadyMessage(port_); + } else { + fprintf(stderr, "Unable to open devtools socket: %s\n", uv_strerror(err)); + assert(false); + } + if (!wait_) { + uv_sem_post(&start_sem_); + } + uv_run(&child_loop_, UV_RUN_DEFAULT); + uv_close(reinterpret_cast(&server), nullptr); + uv_run(&child_loop_, UV_RUN_NOWAIT); +} + +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; + uv_mutex_lock(&queue_lock_); + messages.swap(message_queue_); + uv_mutex_unlock(&queue_lock_); + + for (auto const& message : messages) + inspector_->dispatchMessageFromFrontend(message); + 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 { + PrintDebuggerReadyMessage(port_); + inspector_->quitMessageLoopOnPause(); + inspector_->disconnectFrontend(); + } +} + +void Agent::Write(const String16& message) { + uv_async_t* async = reinterpret_cast(malloc(sizeof(uv_async_t))); + ASSERT_NE(async, nullptr); + uv_async_init(&child_loop_, async, Agent::WriteCbIO); + async->data = new AsyncWriteRequest(this, message); + ASSERT_EQ(0, uv_async_send(async)); +} + +} // namespace debugger +} // namespace node diff --git a/src/inspector_agent.h b/src/inspector_agent.h new file mode 100644 index 00000000000000..3160a4defa3d00 --- /dev/null +++ b/src/inspector_agent.h @@ -0,0 +1,94 @@ +#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 + +namespace blink { +class V8Inspector; +namespace protocol { + class String16; +} +} + +// 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(Agent* 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 PostMessages(); + void SetConnected(bool connected); + void Write(const blink::protocol::String16& 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_; + node::Environment* parent_env_; + + uv_async_t data_written_; + inspector_socket_t* client_socket_; + blink::V8Inspector* inspector_; + v8::Platform* platform_; + std::vector message_queue_; + bool dispatching_messages_; + + friend class AsyncWriteRequest; + 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 00000000000000..2ae92aba73f24d --- /dev/null +++ b/src/inspector_socket.cc @@ -0,0 +1,762 @@ +#include "inspector_socket.h" + +#include "node_internals.h" + +#include "openssl/sha.h" // Sha-1 hash + +#include +#include + +#define ACCEPT_KEY_LENGTH 28 // SHA1 has is 20 bytes, 28 in base64 +#define BUFFER_GROWTH_CHUNK_SIZE 1024 + +#define DUMP_READS 0 +#define DUMP_WRITES 0 + +#define MAX(a, b) ((a) > (b) ? (a) : (b)) + +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 ", *((unsigned char*) 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 void encode_frame_hybi17(const char* message, + size_t data_length, + int masking_key, + bool compressed, + char** output, + size_t* output_len) { + std::vector frame; + OpCode op_code = kOpCodeText; + int reserved1 = compressed ? kReserved1Bit : 0; + frame.push_back(kFinalBit | op_code | reserved1); + char mask_key_bit = masking_key != 0 ? kMaskBit : 0; + if (data_length <= kMaxSingleBytePayloadLength) { + frame.push_back(static_cast(data_length) | mask_key_bit); + } else if (data_length <= 0xFFFF) { + frame.push_back(kTwoBytePayloadLengthField | mask_key_bit); + frame.push_back((data_length & 0xFF00) >> 8); + frame.push_back(data_length & 0xFF); + } else { + frame.push_back(kEightBytePayloadLengthField | mask_key_bit); + 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(!remaining); + } + + if (masking_key != 0) { + const char* mask_bytes = reinterpret_cast(&masking_key); + frame.insert(frame.end(), mask_bytes, mask_bytes + 4); + for (size_t i = 0; i < data_length; ++i) // Mask the payload. + frame.push_back(message[i] ^ mask_bytes[i % kMaskingKeyWidthInBytes]); + } else { + frame.insert(frame.end(), message, message + data_length); + } + *output = reinterpret_cast(malloc(frame.size() + 1)); + memcpy(*output, &frame[0], frame.size()); + (*output)[frame.size()] = '\0'; + *output_len = frame.size(); +} + +static ws_decode_result decode_frame_hybi17(const char* buffer_begin, + size_t data_length, + bool client_frame, + int* bytes_consumed, + char** output, + bool* compressed) { + *bytes_consumed = 0; + *output = nullptr; + 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 { + assert(payload_length64 == kEightBytePayloadLengthField); + extended_payload_length_size = 8; + } + 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++); + } + } + + size_t actual_masking_key_length = masked ? kMaskingKeyWidthInBytes : 0; + static const uint64_t max_payload_length = 0x7FFFFFFFFFFFFFFFull; + static size_t max_length = SIZE_MAX; + if (payload_length64 > max_payload_length || + payload_length64 + actual_masking_key_length > max_length) { + // WebSocket frame length too large. + return FRAME_ERROR; + } + size_t payload_length = static_cast(payload_length64); + + size_t total_length = actual_masking_key_length + payload_length; + if (static_cast(buffer_end - p) < total_length) + return FRAME_INCOMPLETE; + + // Add 1 sizeof(char) for 0 terminator + *output = reinterpret_cast(malloc(payload_length + 1)); + if (masked) { + const char* masking_key = p; + char* payload = const_cast(p + kMaskingKeyWidthInBytes); + for (size_t i = 0; i < payload_length; ++i) // Unmask the payload. + (*output)[i] = payload[i] ^ masking_key[i % kMaskingKeyWidthInBytes]; + } else { + strncpy(*output, p, payload_length); + } + (*output)[payload_length] = '\0'; + + size_t pos = p + actual_masking_key_length + 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; + char* output = nullptr; + 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 = strlen(output) + 1; + inspector->ws_state->alloc_cb( + reinterpret_cast(&inspector->client), + len, &buffer); + CHECK_GE(len, buffer.len); + strncpy(buffer.base, output, len); + invoke_read_callback(inspector, len, &buffer); + } + free(output); + 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; +} + +// From string_bytes.cc +static size_t base64_encode(const unsigned char* src, + size_t slen, + char* dst, + size_t dlen) { + unsigned a; + unsigned b; + unsigned c; + unsigned i; + unsigned k; + unsigned n; + + static const char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + + i = 0; + k = 0; + n = slen / 3 * 3; + + while (i < n) { + a = src[i + 0] & 0xff; + b = src[i + 1] & 0xff; + c = src[i + 2] & 0xff; + + dst[k + 0] = table[a >> 2]; + dst[k + 1] = table[((a & 3) << 4) | (b >> 4)]; + dst[k + 2] = table[((b & 0x0f) << 2) | (c >> 6)]; + dst[k + 3] = table[c & 0x3f]; + + i += 3; + k += 4; + } + + if (n != slen) { + switch (slen - n) { + case 1: + a = src[i + 0] & 0xff; + dst[k + 0] = table[a >> 2]; + dst[k + 1] = table[(a & 3) << 4]; + dst[k + 2] = '='; + dst[k + 3] = '='; + break; + + case 2: + a = src[i + 0] & 0xff; + b = src[i + 1] & 0xff; + dst[k + 0] = table[a >> 2]; + dst[k + 1] = table[((a & 3) << 4) | (b >> 4)]; + dst[k + 2] = table[(b & 0x0f) << 2]; + dst[k + 3] = '='; + break; + } + } + return dlen; +} + +static void generate_accept_string(const char* clientKey, char* buffer) { + // Magic string from websockets spec. + const char wsMagic[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + // sizeof is computed at compile time and accounts for terminating '\0' + int len = strlen(clientKey) + sizeof(wsMagic); + + char* buf = reinterpret_cast(malloc(len)); + CHECK_NE(buf, nullptr); + snprintf(buf, len, "%s%s", clientKey, wsMagic); + unsigned char hash[20]; + SHA1((unsigned char*) buf, len - 1, hash); + free(buf); + 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_format[] = "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: %s\r\n\r\n"; + // Format has two chars (%s) that are replaced with actual key + char accept_response[sizeof(accept_ws_format) + ACCEPT_KEY_LENGTH - 2]; + snprintf(accept_response, sizeof(accept_response), + accept_ws_format, accept_string); + int len = sizeof(accept_response) - 1; + 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; + size_t parsed = http_parser_execute(parser, &state->parser_settings, + inspector->buffer, + nread); + if (parsed == 0) { + 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) { + char* output; + size_t output_len; + encode_frame_hybi17(data, len, 0, false, &output, &output_len); + write_to_client(inspector, output, output_len); + free(output); + } 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 00000000000000..a2543e063e6e16 --- /dev/null +++ b/src/inspector_socket.h @@ -0,0 +1,65 @@ +#ifndef SRC_INSPECTOR_SOCKET_H_ +#define SRC_INSPECTOR_SOCKET_H_ + +#include "http_parser.h" +#include "uv.h" + +#ifdef __cplusplus +extern "C" { +#endif + +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); + +#ifdef __cplusplus +} +#endif + +#endif // SRC_INSPECTOR_SOCKET_H_ diff --git a/src/node.cc b/src/node.cc index e7c8eb177af162..627ffc066a4f30 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; @@ -3405,6 +3408,17 @@ 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; +#endif } else { return false; } @@ -3675,10 +3689,20 @@ 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); @@ -3690,6 +3714,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()); @@ -3984,7 +4013,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; } } @@ -4413,6 +4450,24 @@ static void StartNodeInstance(void* arg) { instance_data->set_exit_code(exit_code); RunAtExit(env); +#if HAVE_INSPECTOR + if (env->inspector_agent()->connected()) { + // TODO(repenaxa): Need to handle non-posix platforms too. + // Restore signal dispositions, the app is done and is no longer + // capable of handling signals. + 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)); + } + fprintf(stderr, "Waiting for the debugger to disconnect...\n"); + 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 0d660705c8efb4..0a8c1354718a8a 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -217,7 +217,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: @@ -261,6 +261,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 ec052366f27f28..3946db40e82242 100644 --- a/src/signal_wrap.cc +++ b/src/signal_wrap.cc @@ -64,6 +64,15 @@ class SignalWrap : public HandleWrap { static void Start(const FunctionCallbackInfo& args) { SignalWrap* wrap = Unwrap(args.Holder()); int signum = args[0]->Int32Value(); +#if 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/inspector_socket.cc b/test/cctest/inspector_socket.cc new file mode 100644 index 00000000000000..6c940d602d6a63 --- /dev/null +++ b/test/cctest/inspector_socket.cc @@ -0,0 +1,732 @@ +#include "inspector_socket.h" + +#include "gtest/gtest.h" + +#define PORT 9444 + +static const int MAX_LOOP_ITERATIONS = 10000; + +#define SPIN_WHILE(condition) { \ + int iterations_count = 0; \ + while((condition)) { \ + if (++iterations_count > MAX_LOOP_ITERATIONS) break; \ + uv_run(&loop, UV_RUN_NOWAIT); \ + } \ + ASSERT_FALSE((condition)); \ +} + +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 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, (inspector_socket_t*) 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 = (char*) data; + buf[0].len = len; + uv_write(&req, (uv_stream_t*) &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 = (char*) 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 = (read_expects*) stream->data; + expects->callback_called = true; + check_data_cb(expects, nread, buf, &retval); + if (retval) { + stream->data = nullptr; + uv_read_stop(stream); + } +} + +static void inspector_check_data_cb(uv_stream_t* stream, ssize_t nread, + const uv_buf_t* buf) { + inspector_socket_t* inspector = (inspector_socket_t*) stream->data; + const char* expectation = (const char*) inspector->data; + if (nread <= 0) { + EXPECT_EQ(expectation, nullptr); + return; + } else { + EXPECT_STREQ(expectation, (const char*) buf->base); + } + inspector->data = nullptr; + free(buf->base); +} + +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((uv_stream_t*) &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((uv_stream_t*) &client_socket, buffer_alloc_cb, check_data_cb); + SPIN_WHILE(!expectation.read_expected); +} + +static void expect_on_server(const char* data, size_t len) { + inspector.data = (void*) data; + inspector_read_start(&inspector, buffer_alloc_cb, inspector_check_data_cb); + SPIN_WHILE(inspector.data != nullptr) +} + +static void inspector_record_error_code(uv_stream_t* stream, ssize_t nread, + const uv_buf_t* buf) { + inspector_socket_t* inspector = + (inspector_socket_t*) stream->data; + // Increment instead of assign is to ensure the function is only called once + *((int*) inspector->data) += nread; +} + +static void expect_server_read_error() { + int error_code = 0; + inspector.data = &error_code; + inspector_read_start(&inspector, buffer_alloc_cb, + inspector_record_error_code); + SPIN_WHILE(error_code != UV_EPROTO); + GTEST_ASSERT_EQ(UV_EPROTO, error_code); +} + +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((uv_handle_t*) socket)) { + uv_close((uv_handle_t*) 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((uv_handle_t*) &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_tcp_init(&loop, &server); + uv_tcp_init(&loop, &client_socket); + uv_ip4_addr("localhost", PORT, &addr); + uv_tcp_bind(&server, (const struct sockaddr*) &addr, 0); + int err = uv_listen((uv_stream_t*) &server, 0, on_new_connection); + GTEST_ASSERT_EQ(0, err); + uv_connect_t connect; + connect.data = nullptr; + uv_tcp_connect(&connect, &client_socket, (const sockaddr*) &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((uv_handle_t*) &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); + } +}; + +TEST_F(InspectorSocketTest, ReadsAndWritesInspectorMessage) { + ASSERT_TRUE(connected); + ASSERT_FALSE(inspector_ready); + do_write((char*) 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((uv_handle_t*) &client_socket)); +} + +void expect_data(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) { + inspector_socket_t* inspector = + (inspector_socket_t*) stream->data; + const char** next_line = (const char**) inspector->data; + EXPECT_STREQ(*next_line, buf->base); + inspector->data = next_line + 1; + free(buf->base); +} + +TEST_F(InspectorSocketTest, BufferEdgeCases) { + + do_write((char*) 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\"}", + nullptr + }; + + do_write(MULTIPLE_REQUESTS, sizeof(MULTIPLE_REQUESTS)); + inspector.data = EXPECT; + inspector_read_start(&inspector, buffer_alloc_cb, expect_data); + SPIN_WHILE(*((char**) inspector.data) != nullptr); + 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((char*) HANDSHAKE_REQ, write1); + ASSERT_FALSE(inspector_ready); + do_write((char*) HANDSHAKE_REQ + write1, write2); + ASSERT_FALSE(inspector_ready); + do_write((char*) HANDSHAKE_REQ + write1 + write2, write3); + SPIN_WHILE(!inspector_ready); + expect_handshake(); + inspector_read_stop(&inspector); + GTEST_ASSERT_EQ(uv_is_active((uv_handle_t*) &client_socket), 0); + manual_inspector_socket_cleanup(); +} + +TEST_F(InspectorSocketTest, ExtraTextBeforeRequest) { + + char UNCOOL_BRO[] = "Uncool, bro: Text before the first req\r\n"; + do_write((char*) UNCOOL_BRO, sizeof(UNCOOL_BRO) - 1); + + last_event = kInspectorHandshakeUpgraded; + ASSERT_FALSE(inspector_ready); + do_write((char*) HANDSHAKE_REQ, sizeof(HANDSHAKE_REQ) - 1); + SPIN_WHILE(last_event != kInspectorHandshakeFailed); + expect_handshake_failure(); + EXPECT_EQ(uv_is_active((uv_handle_t*) &client_socket), 0); + EXPECT_EQ(uv_is_active((uv_handle_t*) &socket), 0); +} + +TEST_F(InspectorSocketTest, ExtraLettersBeforeRequest) { + + char UNCOOL_BRO[] = "Uncool!!"; + do_write((char*) UNCOOL_BRO, sizeof(UNCOOL_BRO) - 1); + + ASSERT_FALSE(inspector_ready); + do_write((char*) HANDSHAKE_REQ, sizeof(HANDSHAKE_REQ) - 1); + SPIN_WHILE(last_event != kInspectorHandshakeFailed); + expect_handshake_failure(); + EXPECT_EQ(uv_is_active((uv_handle_t*) &client_socket), 0); + EXPECT_EQ(uv_is_active((uv_handle_t*) &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((char*) BROKEN_REQUEST, sizeof(BROKEN_REQUEST) - 1); + SPIN_WHILE(last_event != kInspectorHandshakeFailed); + expect_handshake_failure(); + EXPECT_EQ(uv_is_active((uv_handle_t*) &client_socket), 0); + EXPECT_EQ(uv_is_active((uv_handle_t*) &socket), 0); +} + +TEST_F(InspectorSocketTest, KillsConnectionOnProtocolViolation) { + ASSERT_TRUE(connected); + ASSERT_FALSE(inspector_ready); + do_write((char*) 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((uv_handle_t*) &client_socket), 0); +} + + +TEST_F(InspectorSocketTest, CanStopReadingFromInspector) { + ASSERT_TRUE(connected); + ASSERT_FALSE(inspector_ready); + do_write((char*) 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((uv_handle_t*) &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((char*) 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); +} + +TEST_F(InspectorSocketTest, CloseWorksWithoutReadEnabled) { + inspector_closed = false; + do_write((char*) 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((char*) 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((char*) 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) { + *((bool*) 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((uv_handle_t*) &client_socket, WriteBeforeHandshake_close_cb); + SPIN_WHILE(!flag); +} + +static void CleanupSocketAfterEOF_close_cb(inspector_socket_t* inspector, + int status) { + *((bool*) 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 = (inspector_socket_t*) stream->data; + inspector_close(insp, CleanupSocketAfterEOF_close_cb); +} + +TEST_F(InspectorSocketTest, CleanupSocketAfterEOF) { + do_write((char*) 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((uv_handle_t*) &client_socket, nullptr); + bool flag = false; + inspector.data = &flag; + SPIN_WHILE(!flag); +} + +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((uv_handle_t*) &client_socket, nullptr); + SPIN_WHILE(last_event != kInspectorHandshakeFailed); +} + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + // ::testing::GTEST_FLAG(filter) = + // "InspectorSocketTest.CleanupSocketAfterEOF"; + return RUN_ALL_TESTS(); +}