microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
e23d18413023bf3cf53b60f216b32f9b33263a28

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/cdp-proxy/debuggerEndpointHelper.ts

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