microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1.0.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/cdp-proxy/debuggerEndpointHelper.ts

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