From 26115a989e057f7f69b259cb2fb59001f40fc10b Mon Sep 17 00:00:00 2001 From: Tomas Martykan Date: Thu, 14 Nov 2024 14:50:41 +0100 Subject: [PATCH] feat(suite): connect popup display connecting process and web origin --- packages/suite-desktop-api/src/messages.ts | 2 + .../suite-desktop-core/src/libs/connect-ws.ts | 18 ++++- .../src/libs/find-process-from-port.ts | 69 +++++++++++++++++++ .../UserContextModal/ConnectPopupModal.tsx | 22 +++++- .../UserContextModal/UserContextModal.tsx | 4 +- .../connect-init/src/connectInitThunks.ts | 7 ++ suite-common/suite-types/src/modal.ts | 3 + 7 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 packages/suite-desktop-core/src/libs/find-process-from-port.ts diff --git a/packages/suite-desktop-api/src/messages.ts b/packages/suite-desktop-api/src/messages.ts index 350c187b45c6..9b7276891267 100644 --- a/packages/suite-desktop-api/src/messages.ts +++ b/packages/suite-desktop-api/src/messages.ts @@ -131,6 +131,8 @@ export type ConnectPopupCall = { id: number; method: string; payload: any; + processName?: string; + origin?: string; }; export type ConnectPopupResponse = { diff --git a/packages/suite-desktop-core/src/libs/connect-ws.ts b/packages/suite-desktop-core/src/libs/connect-ws.ts index c592f6a99517..b67a3b3d5e52 100644 --- a/packages/suite-desktop-core/src/libs/connect-ws.ts +++ b/packages/suite-desktop-core/src/libs/connect-ws.ts @@ -6,6 +6,7 @@ import { createDeferred, Deferred } from '@trezor/utils'; import { createHttpReceiver } from './http-receiver'; import { Dependencies } from '../modules'; +import { findProcessFromIncomingPort } from './find-process-from-port'; const LOG_PREFIX = 'connect-ws'; @@ -30,7 +31,20 @@ export const exposeConnectWs = ({ logger.info(LOG_PREFIX, 'Websocket server is listening'); }); - wss.on('connection', ws => { + wss.on('connection', async (ws, req) => { + const ip = req.socket.remoteAddress; + const port = req.socket.remotePort; + if ((ip !== '127.0.0.1' && ip !== '::1') || !port) { + logger.error(LOG_PREFIX, `invalid connection attempt from ${ip}:${port}`); + ws.close(); + + return; + } + logger.info(LOG_PREFIX, `new connection from ${ip}:${port}`); + const processOnPort = await findProcessFromIncomingPort(port); + const { origin } = req.headers; + logger.info(LOG_PREFIX, `origin: ${origin}`); + ws.on('error', err => { logger.error(LOG_PREFIX, err.message); }); @@ -85,6 +99,8 @@ export const exposeConnectWs = ({ id: message.id, method, payload: rest, + origin, + processName: processOnPort?.name, }); // wait for response diff --git a/packages/suite-desktop-core/src/libs/find-process-from-port.ts b/packages/suite-desktop-core/src/libs/find-process-from-port.ts new file mode 100644 index 000000000000..0f3f179592a9 --- /dev/null +++ b/packages/suite-desktop-core/src/libs/find-process-from-port.ts @@ -0,0 +1,69 @@ +import { spawn } from 'child_process'; + +export function spawnAndCollectStdout(command: string): Promise { + return new Promise((resolve, reject) => { + const child = spawn(command, { shell: true }); + let stdout = ''; + let stderr = ''; + child.stdout.on('data', data => { + stdout += data.toString(); + }); + child.stderr.on('data', data => { + stderr += data.toString(); + }); + child.on('close', code => { + if (code !== 0) { + reject(new Error(`Command failed with code ${code}: ${stderr}`)); + } else { + resolve(stdout); + } + }); + }); +} + +export async function findProcessFromIncomingPort(port: number) { + switch (process.platform) { + case 'darwin': + case 'linux': { + const command = `lsof -iTCP:${port} -sTCP:ESTABLISHED -n -P +c0`; + const stdout = await spawnAndCollectStdout(command); + const lines = stdout.split('\n'); + const process = lines.find(line => line.includes(`:${port}->`)); + if (process) { + const name = process.split(/\s+/)[0].replace(/\\x\d{2}/g, ' '); + const pid = process.split(/\s+/)[1]; + const user = process.split(/\s+/)[2]; + + return { name, pid, user }; + } + + return undefined; + } + case 'win32': { + const command = `netstat -ano | findstr :${port} | findstr ESTABLISHED`; + const stdout = await spawnAndCollectStdout(command); + const lines = stdout.split('\n'); + const record = lines + .map(line => { + const parts = line.split(/\s+/); + const pid = parts[parts.length - 1]; + const local = parts[2]; + + return { pid, local }; + }) + .find(({ local }) => local.endsWith(`:${port}`)); + if (record) { + const processInfo = await spawnAndCollectStdout( + `tasklist /FI "PID eq ${record.pid}" | findstr ${record.pid}`, + ); + const parts = processInfo.split(/\s+/); + const name = parts[0]; + const user = parts[2]; + + return { name, pid: record.pid, user }; + } + + return undefined; + } + } +} diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ConnectPopupModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ConnectPopupModal.tsx index edcb332b8321..0bed22cf22a4 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ConnectPopupModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ConnectPopupModal.tsx @@ -5,11 +5,19 @@ import { Translation } from 'src/components/suite'; interface ConnectPopupModalProps { method: string; + processName?: string; + origin?: string; onConfirm: () => void; onCancel: () => void; } -export const ConnectPopupModal = ({ method, onConfirm, onCancel }: ConnectPopupModalProps) => { +export const ConnectPopupModal = ({ + method, + processName, + origin, + onConfirm, + onCancel, +}: ConnectPopupModalProps) => { return ( } >

{method}

+ + {processName && ( + + Process: {processName} + + )} + {origin && ( + + Web Origin: {origin} + + )} + A 3rd party application is trying to connect to your device. Do you want to allow this action? diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UserContextModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UserContextModal.tsx index 61dbaf771f54..215e70cb0a69 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UserContextModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UserContextModal.tsx @@ -210,9 +210,11 @@ export const UserContextModal = ({ case 'connect-popup': return ( ); default: diff --git a/suite-common/connect-init/src/connectInitThunks.ts b/suite-common/connect-init/src/connectInitThunks.ts index 533157b40ffa..d0c9ec40c36d 100644 --- a/suite-common/connect-init/src/connectInitThunks.ts +++ b/suite-common/connect-init/src/connectInitThunks.ts @@ -177,10 +177,14 @@ export const connectPopupCallThunk = createThunk( id, method, payload, + processName, + origin, }: { id: number; method: string; payload: any; + processName?: string; + origin?: string; }, { dispatch, getState, extra }, ) => { @@ -210,8 +214,11 @@ export const connectPopupCallThunk = createThunk( dispatch( extra.actions.openModal({ type: 'connect-popup', + onCancel: () => confirmation.reject(ERRORS.TypedError('Method_Cancel')), onConfirm: () => confirmation.resolve(), method: methodInfo.payload.info, + processName, + origin, }), ); await confirmation.promise; diff --git a/suite-common/suite-types/src/modal.ts b/suite-common/suite-types/src/modal.ts index b99acab1cb2d..17a1a6bd1d81 100644 --- a/suite-common/suite-types/src/modal.ts +++ b/suite-common/suite-types/src/modal.ts @@ -187,5 +187,8 @@ export type UserContextPayload = | { type: 'connect-popup'; onConfirm: () => void; + onCancel: () => void; method: string; + processName?: string; + origin?: string; };