microsoft/vscode-react-native

Public

mirrored fromhttps://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
sourcemap-handle-null-sections

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/cdp-proxy/debuggerEndpointHelper.ts

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