microsoft/vscode-react-native

Public

mirrored from https://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1.9.3

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

src/cdp-proxy/debuggerEndpointHelper.ts

189lines · modecode

1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT license. See LICENSE file in the project root for details.
3
4import * as URL from "url";
5import * as http from "http";
6import * as https from "https";
7import { promises as dns } from "dns";
8import * as ipModule from "ip";
9import { CancellationToken } from "vscode";
10import { InternalErrorCode } from "../common/error/internalErrorCode";
11import { ErrorHelper } from "../common/error/errorHelper";
12import { PromiseUtil } from "../common/node/promise";
13
14interface DebuggableEndpointData {
15 webSocketDebuggerUrl: string;
16 title: string;
17}
18
19export class DebuggerEndpointHelper {
20 private localv4: Buffer;
21 private localv6: Buffer;
22
23 constructor() {
24 this.localv4 = ipModule.toBuffer("127.0.0.1");
25 this.localv6 = ipModule.toBuffer("::1");
26 }
27
28 /**
29 * Attempts to retrieve the debugger websocket URL for a process listening
30 * at the given address, retrying until available.
31 * @param browserURL -- Address like `http://localhost:1234`
32 * @param cancellationToken -- Cancellation for this operation
33 */
34 public async retryGetWSEndpoint(
35 browserURL: string,
36 attemptNumber: number,
37 cancellationToken: CancellationToken,
38 isHermes: boolean = false,
39 ): Promise<string> {
40 while (true) {
41 try {
42 return await this.getWSEndpoint(browserURL, isHermes);
43 } catch (err) {
44 if (attemptNumber < 1 || cancellationToken.isCancellationRequested) {
45 const internalError = ErrorHelper.getInternalError(
46 InternalErrorCode.CouldNotConnectToDebugTarget,
47 browserURL,
48 err.message,
49 );
50
51 if (cancellationToken.isCancellationRequested) {
52 throw ErrorHelper.getNestedError(
53 internalError,
54 InternalErrorCode.CancellationTokenTriggered,
55 );
56 }
57
58 throw internalError;
59 }
60
61 await PromiseUtil.delay(700);
62 }
63 }
64 }
65
66 /**
67 * Returns the debugger websocket URL a process listening at the given address.
68 * @param browserURL -- Address like `http://localhost:1234`
69 */
70 public async getWSEndpoint(browserURL: string, isHermes: boolean = false): Promise<string> {
71 const jsonVersion = await this.fetchJson<{ webSocketDebuggerUrl?: string }>(
72 URL.resolve(browserURL, "/json/version"),
73 );
74 if (jsonVersion.webSocketDebuggerUrl) {
75 return jsonVersion.webSocketDebuggerUrl;
76 }
77
78 // Chrome its top-level debugg on /json/version, while Node does not.
79 // Request both and return whichever one got us a string.
80 const jsonList = await this.fetchJson<DebuggableEndpointData[]>(
81 URL.resolve(browserURL, "/json/list"),
82 );
83 if (jsonList.length) {
84 return isHermes
85 ? this.tryToGetHermesImprovedChromeReloadsWebSocketDebuggerUrl(jsonList)
86 : jsonList[0].webSocketDebuggerUrl;
87 }
88
89 throw new Error("Could not find any debuggable target");
90 }
91
92 private tryToGetHermesImprovedChromeReloadsWebSocketDebuggerUrl(
93 jsonList: DebuggableEndpointData[],
94 ): string {
95 const target = jsonList.find(
96 target => target.title === "React Native Experimental (Improved Chrome Reloads)",
97 );
98 return target ? target.webSocketDebuggerUrl : jsonList[0].webSocketDebuggerUrl;
99 }
100
101 /**
102 * Fetches JSON content from the given URL.
103 */
104 private async fetchJson<T>(url: string): Promise<T> {
105 const data = await this.fetch(url);
106 try {
107 return JSON.parse(data);
108 } catch (err) {
109 return {} as T;
110 }
111 }
112
113 /**
114 * Fetches content from the given URL.
115 */
116 private async fetch(url: string): Promise<string> {
117 const isSecure = !url.startsWith("http://");
118 const driver = isSecure ? https : http;
119 const targetAddressIsLoopback = await this.isLoopback(url);
120
121 return new Promise<string>((fulfill, reject) => {
122 const requestOptions: https.RequestOptions = {};
123
124 if (isSecure && targetAddressIsLoopback) {
125 requestOptions.rejectUnauthorized = false;
126 }
127
128 const request = driver.get(url, requestOptions, response => {
129 let data = "";
130 response.setEncoding("utf8");
131 response.on("data", (chunk: string) => {
132 data += chunk;
133 });
134 response.on("end", () => fulfill(data));
135 response.on("error", reject);
136 });
137
138 request.on("error", reject);
139 request.end();
140 });
141 }
142
143 /**
144 * Gets whether the IP is a loopback address.
145 */
146 private async isLoopback(address: string) {
147 let ipOrHostname: string;
148 try {
149 const url = new URL.URL(address);
150 // replace brackets in ipv6 addresses:
151 ipOrHostname = url.hostname.replace(/^\[|]$/g, "");
152 } catch {
153 ipOrHostname = address;
154 }
155
156 if (this.isLoopbackIp(ipOrHostname)) {
157 return true;
158 }
159
160 try {
161 const resolved = await dns.lookup(ipOrHostname);
162 return this.isLoopbackIp(resolved.address);
163 } catch {
164 return false;
165 }
166 }
167
168 /**
169 * Checks if the given address, well-formed loopback IPs. We don't need exotic
170 * variations like `127.1` because `dns.lookup()` will resolve the proper
171 * version for us. The "right" way would be to parse the IP to an integer
172 * like Go does (https://golang.org/pkg/net/#IP.IsLoopback), but this
173 * is lightweight and works.
174 */
175 private isLoopbackIp(ipOrLocalhost: string) {
176 if (ipOrLocalhost.toLowerCase() === "localhost") {
177 return true;
178 }
179
180 let buf: Buffer;
181 try {
182 buf = ipModule.toBuffer(ipOrLocalhost);
183 } catch {
184 return false;
185 }
186
187 return buf.equals(this.localv4) || buf.equals(this.localv6);
188 }
189}
190