microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
bfcc8a2951f4241857d9489629d5cd03f6b6f673

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/cdp-proxy/debuggerEndpointHelper.ts

187lines · 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 ipModule from "ip";
6const dns = require("dns").promises;
7import * as http from "http";
8import * as https from "https";
9import { PromiseUtil } from "../common/node/promise";
10import { ErrorHelper } from "../common/error/errorHelper";
11import { InternalErrorCode } from "../common/error/internalErrorCode";
12import { CancellationToken } from "vscode";
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) => (data += chunk));
132 response.on("end", () => fulfill(data));
133 response.on("error", reject);
134 });
135
136 request.on("error", reject);
137 request.end();
138 });
139 }
140
141 /**
142 * Gets whether the IP is a loopback address.
143 */
144 private async isLoopback(address: string) {
145 let ipOrHostname: string;
146 try {
147 const url = new URL.URL(address);
148 // replace brackets in ipv6 addresses:
149 ipOrHostname = url.hostname.replace(/^\[|\]$/g, "");
150 } catch {
151 ipOrHostname = address;
152 }
153
154 if (this.isLoopbackIp(ipOrHostname)) {
155 return true;
156 }
157
158 try {
159 const resolved = await dns.lookup(ipOrHostname);
160 return this.isLoopbackIp(resolved.address);
161 } catch {
162 return false;
163 }
164 }
165
166 /**
167 * Checks if the given address, well-formed loopback IPs. We don't need exotic
168 * variations like `127.1` because `dns.lookup()` will resolve the proper
169 * version for us. The "right" way would be to parse the IP to an integer
170 * like Go does (https://golang.org/pkg/net/#IP.IsLoopback), but this
171 * is lightweight and works.
172 */
173 private isLoopbackIp(ipOrLocalhost: string) {
174 if (ipOrLocalhost.toLowerCase() === "localhost") {
175 return true;
176 }
177
178 let buf: Buffer;
179 try {
180 buf = ipModule.toBuffer(ipOrLocalhost);
181 } catch {
182 return false;
183 }
184
185 return buf.equals(this.localv4) || buf.equals(this.localv6);
186 }
187}
188