microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1.11.2

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/cdp-proxy/debuggerEndpointHelper.ts

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