microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
4cd259621ddfbd348fade892a2f3ee87fd1924c5

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/cdp-proxy/debuggerEndpointHelper.ts

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