diff --git a/src/inspector/inspector_socket_server.cpp b/src/inspector/inspector_socket_server.cpp new file mode 100644 index 0000000000000000000000000000000000000000..5f0133a415346e59b670eb32d900e56654976c91 --- /dev/null +++ b/src/inspector/inspector_socket_server.cpp @@ -0,0 +1,641 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "inspector_socket_server.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "jsvm_version.h" +#include "uv.h" +#include "v8_inspector_protocol_json.h" +#include "zlib.h" + +namespace jsvm { +namespace inspector { + +// Function is declared in inspector_io.h so the rest of the node does not +// depend on inspector_socket_server.h +std::string FormatWsAddress(const std::string& host, int port, const std::string& targetId, bool includeProtocol); +namespace { +std::string FormatHostPort(const std::string& host, int port) +{ + // Host is valid (socket was bound) so colon means it's a v6 IP address + bool v6 = host.find(':') != std::string::npos; + std::ostringstream url; + if (v6) { + url << '['; + } + url << host; + if (v6) { + url << ']'; + } + url << ':' << port; + return url.str(); +} + +void Escape(std::string* string) +{ + for (char& c : *string) { + c = (c == '\"' || c == '\\') ? '_' : c; + } +} + +std::string FormatAddress(const std::string& host, const std::string& targetId, bool includeProtocol) +{ + std::ostringstream url; + if (includeProtocol) { + url << "ws://"; + } + url << host << '/' << targetId; + return url.str(); +} + +std::string MapToString(const std::map& object) +{ + bool first = true; + std::ostringstream json; + json << "{\n"; + for (const auto& nameValue : object) { + if (!first) { + json << ",\n"; + } + first = false; + json << " \"" << nameValue.first << "\": \""; + json << nameValue.second << "\""; + } + json << "\n} "; + return json.str(); +} + +std::string MapsToString(const std::vector>& array) +{ + bool isFirst = true; + std::ostringstream json; + json << "[ "; + for (const auto& object : array) { + if (!isFirst) { + json << ", "; + } + isFirst = false; + json << MapToString(object); + } + json << "]\n\n"; + return json.str(); +} + +void SendHttpResponse(InspectorSocket* socket, const std::string& response, int code) +{ + const char headers[] = "HTTP/1.0 %d OK\r\n" + "Content-Type: application/json; charset=UTF-8\r\n" + "Cache-Control: no-cache\r\n" + "Content-Length: %zu\r\n" + "\r\n"; + char header[sizeof(headers) + 20]; + int headerLen = snprintf_s(header, sizeof(header), sizeof(header) - 1, headers, code, response.size()); + CHECK(headerLen >= 0); + socket->Write(header, headerLen); + socket->Write(response.data(), response.size()); +} + +const char* MatchPathSegment(const char* path, const char* expected) +{ + size_t len = strlen(expected); + if (StringEqualNoCaseN(path, expected, len)) { + if (path[len] == '/') { + return path + len + 1; + } + if (path[len] == '\0') { + return path + len; + } + } + return nullptr; +} + +enum HttpStatusCode : int { + HTTP_OK = 200, + HTTP_NOT_FOUND = 404, +}; + +void SendHttpNotFound(InspectorSocket* socket) +{ + SendHttpResponse(socket, "", HttpStatusCode::HTTP_NOT_FOUND); +} + +void SendVersionResponse(InspectorSocket* socket) +{ + std::map response; + response["Protocol-Version"] = "1.1"; + response["Browser"] = "jsvm/" JSVM_VERSION_STRING; + SendHttpResponse(socket, MapToString(response), HttpStatusCode::HTTP_OK); +} + +void SendProtocolJson(InspectorSocket* socket) +{ + z_stream strm; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + CHECK_EQ(inflateInit(&strm), Z_OK); + static const size_t decompressedSize = PROTOCOL_JSON[0] * 0x10000u + PROTOCOL_JSON[1] * 0x100u + PROTOCOL_JSON[2]; + strm.next_in = const_cast(PROTOCOL_JSON + ByteOffset::BYTE_3); + strm.avail_in = sizeof(PROTOCOL_JSON) - ByteOffset::BYTE_3; + std::string data(decompressedSize, '\0'); + strm.next_out = reinterpret_cast(data.data()); + strm.avail_out = data.size(); + CHECK_EQ(inflate(&strm, Z_FINISH), Z_STREAM_END); + CHECK_EQ(strm.avail_out, 0); + CHECK_EQ(inflateEnd(&strm), Z_OK); + SendHttpResponse(socket, data, HttpStatusCode::HTTP_OK); +} +} // namespace + +std::string FormatWsAddress(const std::string& host, int port, const std::string& targetId, bool includeProtocol) +{ + return FormatAddress(FormatHostPort(host, port), targetId, includeProtocol); +} + +class SocketSession { +public: + SocketSession(InspectorSocketServer* server, int id, int serverPort); + void Close() + { + wsSocket.reset(); + } + void Send(const std::string& message); + void Own(InspectorSocket::Pointer wsSocketParam) + { + wsSocket = std::move(wsSocketParam); + } + int GetId() const + { + return id; + } + int ServerPort() + { + return serverPort; + } + InspectorSocket* GetWsSocket() + { + return wsSocket.get(); + } + void Accept(const std::string& wsKey) + { + wsSocket->AcceptUpgrade(wsKey); + } + void Decline() + { + wsSocket->CancelHandshake(); + } + + class Delegate : public InspectorSocket::Delegate { + public: + Delegate(InspectorSocketServer* server, int sessionId) : server(server), usessionId(sessionId) {} + ~Delegate() override + { + server->SessionTerminated(usessionId); + } + void OnHttpGet(const std::string& host, const std::string& path) override; + void OnSocketUpgrade(const std::string& host, const std::string& path, const std::string& wsKey) override; + void OnWsFrame(const std::vector& data) override; + + private: + SocketSession* Session() + { + return server->Session(usessionId); + } + + InspectorSocketServer* server; + int usessionId; + }; + +private: + const int id; + InspectorSocket::Pointer wsSocket; + const int serverPort; +}; + +class ServerSocket { +public: + explicit ServerSocket(InspectorSocketServer* server) + : tcpSocket(uv_tcp_t()), server(server), unixSocket(uv_pipe_t()) + {} + int Listen(sockaddr* addr, uv_loop_t* loop, int pid = -1); + void Close() + { + uv_close(reinterpret_cast(&tcpSocket), FreeOnCloseCallback); + } + void CloseUnix() + { + if (unixSocketOn) { + uv_close(reinterpret_cast(&unixSocket), nullptr); + unixSocketOn = false; + } + } + int GetPort() const + { + return port; + } + +private: + template + static ServerSocket* FromTcpSocket(UvHandle* socket) + { + return jsvm::inspector::ContainerOf(&ServerSocket::tcpSocket, reinterpret_cast(socket)); + } + static void SocketConnectedCallback(uv_stream_t* tcpSocket, int status); + static void UnixSocketConnectedCallback(uv_stream_t* unixSocket, int status); + static void FreeOnCloseCallback(uv_handle_t* tcpSocket) + { + delete FromTcpSocket(tcpSocket); + } + int DetectPort(uv_loop_t* loop, int pid); + ~ServerSocket() = default; + + uv_tcp_t tcpSocket; + InspectorSocketServer* server; + uv_pipe_t unixSocket; + int port = -1; + bool unixSocketOn = false; +}; + +void PrintDebuggerReadyMessage(const std::string& host, + const std::vector& serverSockets, + const std::vector& ids, + const char* verb, + bool publishUidStderr, + FILE* out) +{ + if (!publishUidStderr || out == nullptr) { + return; + } + for (const auto& serverSocket : serverSockets) { + for (const std::string& id : ids) { + if (fprintf(out, "Debugger %s on %s\n", verb, + FormatWsAddress(host, serverSocket->GetPort(), id, true).c_str()) < 0) { + return; + } + } + } + if (fprintf(out, "For help, see: %s\n", "https://nodejs.org/en/docs/inspector") < 0) { + return; + } + if (fflush(out) != 0) { + return; + } +} + +InspectorSocketServer::InspectorSocketServer(std::unique_ptr delegateParam, + uv_loop_t* loop, + const std::string& host, + int port, + const InspectPublishUid& inspectPublishUid, + FILE* out, + int pid) + : loop(loop), delegate(std::move(delegateParam)), host(host), port(port), inspectPublishUid(inspectPublishUid), + nextSessionId(0), out(out), pid(pid) +{ + delegate->AssignServer(this); + state = ServerState::NEW; +} + +InspectorSocketServer::~InspectorSocketServer() = default; + +SocketSession* InspectorSocketServer::Session(int sessionId) +{ + auto it = connectedSessions.find(sessionId); + return it == connectedSessions.end() ? nullptr : it->second.second.get(); +} + +void InspectorSocketServer::SessionStarted(int sessionId, const std::string& targetId, const std::string& wsKey) +{ + SocketSession* session = Session(sessionId); + if (!TargetExists(targetId)) { + session->Decline(); + return; + } + connectedSessions[sessionId].first = targetId; + session->Accept(wsKey); + delegate->StartSession(sessionId, targetId); +} + +void InspectorSocketServer::SessionTerminated(int sessionId) +{ + if (Session(sessionId) == nullptr) { + return; + } + bool wasAttached = connectedSessions[sessionId].first != ""; + if (wasAttached) { + delegate->EndSession(sessionId); + } + connectedSessions.erase(sessionId); + if (connectedSessions.empty()) { + if (wasAttached && state == ServerState::RUNNING && !serverSockets.empty()) { + PrintDebuggerReadyMessage(host, serverSockets, delegate->GetTargetIds(), "ending", + inspectPublishUid.console, out); + } + if (state == ServerState::STOPPED) { + delegate.reset(); + } + } +} + +bool InspectorSocketServer::HandleGetRequest(int sessionId, const std::string& hostName, const std::string& path) +{ + SocketSession* session = Session(sessionId); + InspectorSocket* socket = session->GetWsSocket(); + if (!inspectPublishUid.http) { + SendHttpNotFound(socket); + return true; + } + const char* command = MatchPathSegment(path.c_str(), "/json"); + if (!command) { + return false; + } + + bool hasHandled = false; + if (MatchPathSegment(command, "list") || command[0] == '\0') { + SendListResponse(socket, hostName, session); + hasHandled = true; + } else if (MatchPathSegment(command, "protocol")) { + SendProtocolJson(socket); + hasHandled = true; + } else if (MatchPathSegment(command, "version")) { + SendVersionResponse(socket); + hasHandled = true; + } + return hasHandled; +} + +void InspectorSocketServer::SendListResponse(InspectorSocket* socket, + const std::string& hostName, + SocketSession* session) +{ + std::vector> response; + for (const std::string& id : delegate->GetTargetIds()) { + response.push_back(std::map()); + std::map& targetMap = response.back(); + targetMap["description"] = "jsvm instance"; + targetMap["id"] = id; + targetMap["title"] = delegate->GetTargetTitle(id); + Escape(&targetMap["title"]); + targetMap["type"] = "node"; + // This attribute value is a "best effort" URL that is passed as a JSON + // string. It is not guaranteed to resolve to a valid resource. + targetMap["url"] = delegate->GetTargetUrl(id); + Escape(&targetMap["url"]); + + std::string detectedHost = hostName; + if (detectedHost.empty()) { + detectedHost = FormatHostPort(socket->GetHost(), session->ServerPort()); + } + std::string formattedAddress = FormatAddress(detectedHost, id, false); + targetMap["devtoolsFrontendUrl"] = GetFrontendURL(false, formattedAddress); + // The compat URL is for Chrome browsers older than 66.0.3345.0 + targetMap["devtoolsFrontendUrlCompat"] = GetFrontendURL(true, formattedAddress); + targetMap["webSocketDebuggerUrl"] = FormatAddress(detectedHost, id, true); + } + SendHttpResponse(socket, MapsToString(response), HttpStatusCode::HTTP_OK); +} + +std::string InspectorSocketServer::GetFrontendURL(bool isCompat, const std::string& formattedAddress) +{ + std::ostringstream frontendUrl; + frontendUrl << "devtools://devtools/bundled/"; + frontendUrl << (isCompat ? "inspector" : "js_app"); + frontendUrl << ".html?v8only=true&ws="; + frontendUrl << formattedAddress; + return frontendUrl.str(); +} + +bool InspectorSocketServer::Start() +{ + CHECK_NOT_NULL(delegate); + CHECK_EQ(state, ServerState::NEW); + std::unique_ptr delegateHolder; + // We will return it if startup is successful + delegate.swap(delegateHolder); + struct addrinfo hints; + memset_s(&hints, sizeof(hints), 0, sizeof(hints)); + hints.ai_flags = AI_NUMERICSERV; + hints.ai_socktype = SOCK_STREAM; + uv_getaddrinfo_t req; + const std::string portString = std::to_string(port); + int err = uv_getaddrinfo(loop, &req, nullptr, host.c_str(), portString.c_str(), &hints); + if (err < 0) { + if (out != nullptr) { + if (fprintf(out, "Unable to resolve \"%s\": %s\n", host.c_str(), uv_strerror(err)) < 0) { + return false; + } + } + return false; + } + for (addrinfo* address = req.addrinfo; address != nullptr; address = address->ai_next) { + auto serverSocket = ServerSocketPtr(new ServerSocket(this)); + err = serverSocket->Listen(address->ai_addr, loop, pid); + if (err == 0) { + serverSockets.push_back(std::move(serverSocket)); + } + } + uv_freeaddrinfo(req.addrinfo); + + // We only show error if we failed to start server on all addresses. We only + // show one error, for the last address. + if (serverSockets.empty()) { + if (out != nullptr) { + if (fprintf(out, "Starting inspector on %s:%d failed: %s\n", host.c_str(), port, uv_strerror(err)) < 0) { + return false; + } + if (fflush(out) != 0) { + return false; + } + } + return false; + } + delegate.swap(delegateHolder); + state = ServerState::RUNNING; + PrintDebuggerReadyMessage(host, serverSockets, delegate->GetTargetIds(), "listening", inspectPublishUid.console, + out); + return true; +} + +void InspectorSocketServer::Stop() +{ + if (state == ServerState::STOPPED) { + return; + } + CHECK_EQ(state, ServerState::RUNNING); + state = ServerState::STOPPED; + serverSockets.clear(); + if (Done()) { + delegate.reset(); + } +} + +void InspectorSocketServer::TerminateConnections() +{ + for (const auto& keyValue : connectedSessions) { + keyValue.second.second->Close(); + } +} + +bool InspectorSocketServer::TargetExists(const std::string& id) +{ + const std::vector& targetIds = delegate->GetTargetIds(); + const auto& found = std::find(targetIds.begin(), targetIds.end(), id); + return found != targetIds.end(); +} + +int InspectorSocketServer::GetPort() const +{ + if (!serverSockets.empty()) { + return serverSockets[0]->GetPort(); + } + return port; +} + +void InspectorSocketServer::Accept(int serverPort, uv_stream_t* serverSocket) +{ + std::unique_ptr session = std::make_unique(this, nextSessionId++, serverPort); + + InspectorSocket::DelegatePointer delegatePointer = + InspectorSocket::DelegatePointer(new SocketSession::Delegate(this, session->GetId())); + + InspectorSocket::Pointer inspector = InspectorSocket::Accept(serverSocket, std::move(delegatePointer)); + if (inspector) { + session->Own(std::move(inspector)); + connectedSessions[session->GetId()].second = std::move(session); + } +} + +void InspectorSocketServer::Send(int sessionId, const std::string& message) +{ + SocketSession* session = Session(sessionId); + if (session != nullptr) { + session->Send(message); + } +} + +void InspectorSocketServer::CloseServerSocket(ServerSocket* server) +{ + server->Close(); + server->CloseUnix(); +} + +// InspectorSession tracking +SocketSession::SocketSession(InspectorSocketServer* server, int id, int serverPort) : id(id), serverPort(serverPort) {} + +void SocketSession::Send(const std::string& message) +{ + wsSocket->Write(message.data(), message.length()); +} + +void SocketSession::Delegate::OnHttpGet(const std::string& host, const std::string& path) +{ + if (!server->HandleGetRequest(usessionId, host, path)) { + Session()->GetWsSocket()->CancelHandshake(); + } +} + +void SocketSession::Delegate::OnSocketUpgrade(const std::string& host, + const std::string& path, + const std::string& wsKey) +{ + std::string id = path.empty() ? path : path.substr(1); + server->SessionStarted(usessionId, id, wsKey); +} + +void SocketSession::Delegate::OnWsFrame(const std::vector& data) +{ + server->MessageReceived(usessionId, std::string(data.data(), data.size())); +} + +// ServerSocket implementation +int ServerSocket::DetectPort(uv_loop_t* loop, int pid) +{ + sockaddr_storage addr; + int len = sizeof(addr); + int err = uv_tcp_getsockname(&tcpSocket, reinterpret_cast(&addr), &len); + if (err != 0) { + return err; + } + int portNum; + if (addr.ss_family == AF_INET6) { + portNum = reinterpret_cast(&addr)->sin6_port; + } else { + portNum = reinterpret_cast(&addr)->sin_port; + } + port = ntohs(portNum); + if (!unixSocketOn && pid != -1) { + auto unixDomainSocketPath = "jsvm_devtools_remote_" + std::to_string(port) + "_" + std::to_string(pid); + auto* abstract = new char[unixDomainSocketPath.length() + 2]; + abstract[0] = '\0'; + int ret = strcpy_s(abstract + 1, unixDomainSocketPath.length() + 2, unixDomainSocketPath.c_str()); + CHECK(ret == 0); + auto status = uv_pipe_init(loop, &unixSocket, 0); + if (status == 0) { + status = uv_pipe_bind2(&unixSocket, abstract, unixDomainSocketPath.length() + 1, 0); + } + if (status == 0) { + constexpr int unixBacklog = 128; + status = uv_listen(reinterpret_cast(&unixSocket), unixBacklog, + ServerSocket::UnixSocketConnectedCallback); + } + unixSocketOn = status == 0; + delete[] abstract; + } + return err; +} + +int ServerSocket::Listen(sockaddr* addr, uv_loop_t* loop, int pid) +{ + uv_tcp_t* server = &tcpSocket; + CHECK_EQ(0, uv_tcp_init(loop, server)); + int err = uv_tcp_bind(server, addr, 0); + if (err == 0) { + // 511 is the value used by a 'net' module by default + err = uv_listen(reinterpret_cast(server), 511, ServerSocket::SocketConnectedCallback); + } + if (err == 0) { + err = DetectPort(loop, pid); + } + return err; +} + +// static +void ServerSocket::SocketConnectedCallback(uv_stream_t* tcpSocket, int status) +{ + if (status == 0) { + ServerSocket* serverSocket = ServerSocket::FromTcpSocket(tcpSocket); + // Memory is freed when the socket closes. + serverSocket->server->Accept(serverSocket->port, tcpSocket); + } +} + +void ServerSocket::UnixSocketConnectedCallback(uv_stream_t* unixSocket, int status) +{ + if (status == 0) { + (void)unixSocket; + } +} +} // namespace inspector +} // namespace jsvm diff --git a/src/inspector/inspector_socket_server.h b/src/inspector/inspector_socket_server.h new file mode 100644 index 0000000000000000000000000000000000000000..41c369e7861d164bd16e34333bdb9c0830fe675d --- /dev/null +++ b/src/inspector/inspector_socket_server.h @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SRC_INSPECTOR_SOCKET_SERVER_H_ +#define SRC_INSPECTOR_SOCKET_SERVER_H_ + +#include +#include +#include + +#include "inspector_socket.h" +#include "jsvm_host_port.h" +#include "uv.h" + +#ifndef ENABLE_INSPECTOR +#error("This header can only be used when inspector is enabled") +#endif + +namespace jsvm { +namespace inspector { + +std::string FormatWsAddress(const std::string& host, int port, const std::string& targetId, bool includeProtocol); + +class InspectorSocketServer; +class SocketSession; +class ServerSocket; + +class SocketServerDelegate { +public: + virtual ~SocketServerDelegate() = default; + + virtual void AssignServer(InspectorSocketServer* server) = 0; + + virtual void StartSession(int sessionId, const std::string& targetId) = 0; + + virtual void EndSession(int sessionId) = 0; + + virtual void MessageReceived(int sessionId, const std::string& message) = 0; + + virtual std::vector GetTargetIds() = 0; + + virtual std::string GetTargetTitle(const std::string& id) = 0; + + virtual std::string GetTargetUrl(const std::string& id) = 0; +}; + +// HTTP Server, writes messages requested as TransportActions, and responds to HTTP requests and WS upgrades. + +class InspectorSocketServer { +public: + InspectorSocketServer(std::unique_ptr delegateParam, + uv_loop_t* loop, + const std::string& host, + int port, + const InspectPublishUid& inspectPublishUid, + FILE* out = stderr, + int pid = -1); + + ~InspectorSocketServer(); + + // Start listening on host/port + bool Start(); + + // Called by the TransportAction sent with InspectorIo::Write(): kKill and kStop + void Stop(); + + // kSendMessage + void Send(int sessionId, const std::string& message); + + // kKill + void TerminateConnections(); + + int GetPort() const; + + // Session connection lifecycle + void Accept(int serverPort, uv_stream_t* serverSocket); + + bool HandleGetRequest(int sessionId, const std::string& hostName, const std::string& path); + + void SessionStarted(int sessionId, const std::string& targetId, const std::string& wsKey); + + void SessionTerminated(int sessionId); + + void MessageReceived(int sessionId, const std::string& message) + { + delegate->MessageReceived(sessionId, message); + } + + SocketSession* Session(int sessionId); + + bool Done() const + { + return serverSockets.empty() && connectedSessions.empty(); + } + + static void CloseServerSocket(ServerSocket* server); + using ServerSocketPtr = DeleteFnPtr; + +private: + void SendListResponse(InspectorSocket* socket, const std::string& hostName, SocketSession* session); + std::string GetFrontendURL(bool isCompat, const std::string& formattedAddress); + bool TargetExists(const std::string& id); + + enum class ServerState { NEW, RUNNING, STOPPED }; + uv_loop_t* loop; + std::unique_ptr delegate; + const std::string host; + int port; + InspectPublishUid inspectPublishUid; + std::vector serverSockets; + std::map>> connectedSessions; + int nextSessionId; + FILE* out; + ServerState state; + int pid; +}; + +} // namespace inspector +} // namespace jsvm + +#endif // SRC_INSPECTOR_SOCKET_SERVER_H_