microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1.5.1

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/networkInspector/networkInspectorServer.ts

394lines · 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 { RSocketServer } from "rsocket-core";
5import RSocketTCPServer from "rsocket-tcp-server";
6import { AdbHelper } from "../android/adb";
7import { Single } from "rsocket-flowable";
8import { appNameWithUpdateHint, buildClientId } from "./clientUtils";
9import { SecureClientQuery, ClientCsrQuery, ClientDevice, ClientQuery } from "./clientDevice";
10import { OutputChannelLogger } from "../log/OutputChannelLogger";
11import { Responder, Payload, ReactiveSocket } from "rsocket-types";
12import {
13 CertificateProvider,
14 SecureServerConfig,
15 CertificateExchangeMedium,
16} from "./certificateProvider";
17import { ClientOS } from "./clientUtils";
18import * as net from "net";
19import * as tls from "tls";
20import * as nls from "vscode-nls";
21import { InspectorViewType } from "./views/inspectorView";
22nls.config({
23 messageFormat: nls.MessageFormat.bundle,
24 bundleFormat: nls.BundleFormat.standalone,
25})();
26const localize = nls.loadMessageBundle();
27
28/**
29 * @preserve
30 * Start region: the code is borrowed from https://github.com/facebook/flipper/blob/master/desktop/app/src/server.tsx
31 *
32 * Copyright (c) Facebook, Inc. and its affiliates.
33 *
34 * This source code is licensed under the MIT license found in the
35 * LICENSE file in the root directory of this source tree.
36 *
37 * @format
38 */
39
40function transformCertificateExchangeMediumToType(
41 medium: number | undefined,
42): CertificateExchangeMedium {
43 if (medium == 1) {
44 return "FS_ACCESS";
45 } else if (medium == 2) {
46 return "WWW";
47 } else {
48 return "FS_ACCESS";
49 }
50}
51
52export const NETWORK_INSPECTOR_LOG_CHANNEL_NAME = "Network Inspector";
53
54export class NetworkInspectorServer {
55 public static readonly SecureServerPort = 8088;
56 public static readonly InsecureServerPort = 8089;
57
58 private connections: Map<string, ClientDevice>;
59 private secureServer: RSocketServer<any, any> | null;
60 private insecureServer: RSocketServer<any, any> | null;
61 private certificateProvider: CertificateProvider;
62 private initialisePromise: Promise<void> | null;
63 private logger: OutputChannelLogger;
64
65 constructor() {
66 this.connections = new Map<string, ClientDevice>();
67 this.logger = OutputChannelLogger.getChannel(NETWORK_INSPECTOR_LOG_CHANNEL_NAME);
68 }
69
70 public async start(adbHelper: AdbHelper): Promise<void> {
71 this.logger.info(localize("StartNetworkinspector", "Starting Network inspector"));
72 this.initialisePromise = new Promise(async (resolve, reject) => {
73 this.certificateProvider = new CertificateProvider(adbHelper);
74
75 try {
76 let options = await this.certificateProvider.loadSecureServerConfig();
77 this.secureServer = await this.startServer(
78 NetworkInspectorServer.SecureServerPort,
79 options,
80 );
81 this.insecureServer = await this.startServer(
82 NetworkInspectorServer.InsecureServerPort,
83 );
84 } catch (err) {
85 return reject(err);
86 }
87
88 this.logger.info(localize("NetworkInspectorWorking", "Network inspector is working"));
89 resolve();
90 });
91 return this.initialisePromise;
92 }
93
94 public async stop(): Promise<void> {
95 if (this.initialisePromise) {
96 try {
97 await this.initialisePromise;
98 } catch (err) {
99 this.logger.error(err.toString());
100 }
101 if (this.secureServer) {
102 this.secureServer.stop();
103 }
104 if (this.insecureServer) {
105 this.insecureServer.stop();
106 }
107 }
108 this.logger.info(localize("NetworkInspectorStopped", "Network inspector has been stopped"));
109 }
110
111 private async startServer(
112 port: number,
113 sslConfig?: SecureServerConfig,
114 ): Promise<RSocketServer<any, any>> {
115 return new Promise((resolve, reject) => {
116 let rsServer: RSocketServer<any, any> | undefined; // eslint-disable-line prefer-const
117 const serverFactory = (onConnect: (socket: net.Socket) => void) => {
118 const transportServer = sslConfig
119 ? tls.createServer(sslConfig, socket => {
120 onConnect(socket);
121 })
122 : net.createServer(onConnect);
123 transportServer
124 .on("error", err => {
125 this.logger.error(
126 localize(
127 "ErrorOpeningNetworkInspectorServerOnPort",
128 "Error while opening Network inspector server on port {0}",
129 port,
130 ),
131 );
132 reject(err);
133 })
134 .on("listening", () => {
135 this.logger.debug(
136 `${
137 sslConfig ? "Secure" : "Certificate"
138 } server started on port ${port}`,
139 );
140 resolve(rsServer!);
141 });
142 return transportServer;
143 };
144 rsServer = new RSocketServer({
145 getRequestHandler: sslConfig
146 ? this.trustedRequestHandler
147 : this.untrustedRequestHandler,
148 transport: new RSocketTCPServer({
149 port: port,
150 serverFactory: serverFactory,
151 }),
152 });
153 rsServer && rsServer.start();
154 });
155 }
156
157 private trustedRequestHandler = (
158 socket: ReactiveSocket<string, any>,
159 payload: Payload<string, any>,
160 ): Partial<Responder<string, any>> => {
161 // eslint-disable-next-line @typescript-eslint/no-this-alias
162 const server = this;
163 if (!payload.data) {
164 return {};
165 }
166
167 const clientData: SecureClientQuery = JSON.parse(payload.data);
168
169 const { app, os, device, device_id, sdk_version, csr, csr_path, medium } = clientData;
170 const transformedMedium = transformCertificateExchangeMediumToType(medium);
171
172 const client: Promise<ClientDevice> = this.addConnection(
173 socket,
174 {
175 app,
176 os,
177 device,
178 device_id,
179 sdk_version,
180 medium: transformedMedium,
181 },
182 { csr, csr_path },
183 ).then(client => {
184 return (resolvedClient = client);
185 });
186 let resolvedClient: ClientDevice | undefined;
187
188 socket.connectionStatus().subscribe({
189 onNext(payload) {
190 if (payload.kind == "ERROR" || payload.kind == "CLOSED") {
191 client.then(client => {
192 server.logger.info(
193 localize(
194 "NIDeviceDisconnected",
195 "Device disconnected {0} from the Network inspector",
196 client.id,
197 ),
198 );
199 server.removeConnection(client.id);
200 });
201 }
202 },
203 onSubscribe(subscription) {
204 subscription.request(Number.MAX_SAFE_INTEGER);
205 },
206 onError(error) {
207 server.logger.error("Network inspector server connection status error ", error);
208 },
209 });
210
211 return {
212 fireAndForget: (payload: { data: string }) => {
213 if (resolvedClient) {
214 resolvedClient.onMessage(payload.data);
215 } else {
216 client.then(client => {
217 client.onMessage(payload.data);
218 });
219 }
220 },
221 };
222 };
223
224 private untrustedRequestHandler = (
225 _socket: ReactiveSocket<string, any>,
226 payload: Payload<string, any>,
227 ): Partial<Responder<string, any>> => {
228 if (!payload.data) {
229 return {};
230 }
231 const clientData: ClientQuery = JSON.parse(payload.data);
232
233 return {
234 requestResponse: (payload: Payload<string, any>): Single<Payload<string, any>> => {
235 if (typeof payload.data !== "string") {
236 return new Single(() => {});
237 }
238
239 let rawData;
240 try {
241 rawData = JSON.parse(payload.data);
242 } catch (err) {
243 this.logger.error(`Network inspector: invalid JSON: ${payload.data}`);
244 return new Single(() => {});
245 }
246
247 const json: {
248 method: "signCertificate";
249 csr: string;
250 destination: string;
251 medium: number | undefined; // OSS's older Client SDK might not send medium information. This is not an issue for internal FB users, as Flipper release is insync with client SDK through launcher.
252 } = rawData;
253
254 if (json.method === "signCertificate") {
255 this.logger.debug("CSR received from device");
256
257 const { csr, destination, medium } = json;
258 return new Single(subscriber => {
259 subscriber.onSubscribe(undefined);
260 this.certificateProvider
261 .processCertificateSigningRequest(
262 csr,
263 clientData.os,
264 destination,
265 transformCertificateExchangeMediumToType(medium),
266 )
267 .then(result => {
268 subscriber.onComplete({
269 data: JSON.stringify({
270 deviceId: result.deviceId,
271 }),
272 metadata: "",
273 });
274 })
275 .catch(e => {
276 this.logger.error(e.toString());
277 subscriber.onError(e);
278 });
279 });
280 }
281 return new Single(() => {});
282 },
283
284 // Leaving this here for a while for backwards compatibility,
285 // but for up to date SDKs it will no longer used.
286 // We can delete it after the SDK change has been using requestResponse for a few weeks.
287 fireAndForget: (payload: Payload<string, any>) => {
288 if (typeof payload.data !== "string") {
289 return;
290 }
291
292 let json:
293 | {
294 method: "signCertificate";
295 csr: string;
296 destination: string;
297 medium: number | undefined;
298 }
299 | undefined;
300 try {
301 json = JSON.parse(payload.data);
302 } catch (err) {
303 this.logger.error(`Network inspector: invalid JSON: ${payload.data}`);
304 return;
305 }
306
307 if (json && json.method === "signCertificate") {
308 this.logger.debug("CSR received from device");
309 const { csr, destination, medium } = json;
310 this.certificateProvider
311 .processCertificateSigningRequest(
312 csr,
313 clientData.os,
314 destination,
315 transformCertificateExchangeMediumToType(medium),
316 )
317 .catch(e => {
318 this.logger.error(e.toString());
319 });
320 }
321 },
322 };
323 };
324
325 private async addConnection(
326 conn: ReactiveSocket<any, any>,
327 query: ClientQuery & { medium: CertificateExchangeMedium },
328 csrQuery: ClientCsrQuery,
329 ): Promise<ClientDevice> {
330 // try to get id by comparing giving `csr` to file from `csr_path`
331 // otherwise, use given device_id
332 const { csr_path, csr } = csrQuery;
333 // For iOS we do not need to confirm the device id, as it never changes unlike android.
334 return (csr_path && csr && query.os !== ClientOS.iOS
335 ? this.certificateProvider.extractAppNameFromCSR(csr).then(appName => {
336 return this.certificateProvider.getTargetDeviceId(
337 query.os,
338 appName,
339 csr_path,
340 csr,
341 );
342 })
343 : Promise.resolve(query.device_id)
344 ).then(async csrId => {
345 query.device_id = csrId;
346 query.app = appNameWithUpdateHint(query);
347
348 const id = buildClientId(
349 {
350 app: query.app,
351 os: query.os,
352 device: query.device,
353 device_id: csrId,
354 },
355 this.logger,
356 );
357 this.logger.info(localize("NIDeviceConnected", "Device connected: {0}", id));
358
359 const client = new ClientDevice(
360 id,
361 query,
362 conn,
363 InspectorViewType.console,
364 this.logger,
365 );
366
367 client.init().then(() => {
368 this.logger.debug(`Device client initialised: ${id}`);
369 /* If a device gets disconnected without being cleaned up properly,
370 * Flipper won't be aware until it attempts to reconnect.
371 * When it does we need to terminate the zombie connection.
372 */
373 this.removeConnection(id);
374
375 this.connections.set(id, client);
376 });
377
378 return client;
379 });
380 }
381
382 /**
383 * @preserve
384 * End region: https://github.com/facebook/flipper/blob/master/desktop/app/src/server.tsx
385 */
386
387 private removeConnection(id: string) {
388 const clientDevice = this.connections.get(id);
389 if (clientDevice) {
390 clientDevice.connection && clientDevice.connection.close();
391 this.connections.delete(id);
392 }
393 }
394}
395