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); +}