microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
5884987e1e77357b25c6f9bfec909aa4116c17ec

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/cdp-proxy/debuggerEndpointHelper.ts

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