sending origin information in the first message

This commit is contained in:
antelle 2021-04-18 18:50:03 +02:00
parent 4280b84459
commit cbb9a8dbd0
No known key found for this signature in database
GPG Key ID: 63C9777AAB7C563C
3 changed files with 164 additions and 80 deletions

View File

@ -4,6 +4,7 @@ const path = require('path');
const net = require('net'); const net = require('net');
const { ipcMain, app } = require('electron'); const { ipcMain, app } = require('electron');
const { Logger } = require('../logger'); const { Logger } = require('../logger');
const { getProcessInfo } = require('../util/process-utils');
ipcMain.handle('browserExtensionConnectorStart', browserExtensionConnectorStart); ipcMain.handle('browserExtensionConnectorStart', browserExtensionConnectorStart);
ipcMain.handle('browserExtensionConnectorStop', browserExtensionConnectorStop); ipcMain.handle('browserExtensionConnectorStop', browserExtensionConnectorStop);
@ -13,25 +14,34 @@ ipcMain.handle('browserExtensionConnectorSocketEvent', browserExtensionConnector
const logger = new Logger('browser-extension-connector'); const logger = new Logger('browser-extension-connector');
const MaxIncomingDataLength = 10_000; const MaxIncomingDataLength = 10_000;
const ExtensionOrigins = {
'chrome-extension://aphablpbogbpmocgkpeeadeljldnphon/': 'keeweb-connect',
'safari-keeweb-connect': 'keeweb-connect',
'keeweb-connect@keeweb.info': 'keeweb-connect',
'chrome-extension://oboonakemofpalcgghocfoadofidjkkk/': 'keepassxc-browser',
'keepassxc-browser@keepassxc.org': 'keepassxc-browser',
'chrome-extension://pdffhmdngciaglkoonimfcmckehcpafo/': 'keepassxc-browser'
};
let connectedSockets = new Map(); let connectedSockets = new Map();
let connectedSocketState = new WeakMap(); let connectedSocketState = new WeakMap();
let server; let server;
let serverSocketName;
let socketId = 0; let socketId = 0;
async function browserExtensionConnectorStart(e, config) { async function browserExtensionConnectorStart(e, config) {
await prepareBrowserExtensionSocket(config); serverSocketName = getBrowserExtensionSocketName(config);
const sockName = getBrowserExtensionSocketName(config); await prepareBrowserExtensionSocket();
if (isSocketNameTooLong(sockName)) { if (isSocketNameTooLong(serverSocketName)) {
logger.error( logger.error(
"Socket name is too long, browser connection won't be possible, probably OS username is very long.", "Socket name is too long, browser connection won't be possible, probably OS username is very long.",
sockName serverSocketName
); );
return; return;
} }
server = net.createServer((socket) => { server = net.createServer(async (socket) => {
socketId++; socketId++;
logger.info(`New connection with socket ${socketId}`); logger.info(`New connection with socket ${socketId}`);
@ -39,12 +49,10 @@ async function browserExtensionConnectorStart(e, config) {
connectedSockets.set(socketId, socket); connectedSockets.set(socketId, socket);
connectedSocketState.set(socket, { socketId }); connectedSocketState.set(socket, { socketId });
checkSocketIdentity(socket);
socket.on('data', (data) => onSocketData(socket, data)); socket.on('data', (data) => onSocketData(socket, data));
socket.on('close', () => onSocketClose(socket)); socket.on('close', () => onSocketClose(socket));
}); });
server.listen(sockName); server.listen(serverSocketName);
logger.info('Started'); logger.info('Started');
} }
@ -83,22 +91,21 @@ function getBrowserExtensionSocketName(config) {
} }
} }
function prepareBrowserExtensionSocket(config) { function prepareBrowserExtensionSocket() {
return new Promise((resolve) => { return new Promise((resolve) => {
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
const sockName = getBrowserExtensionSocketName(config); fs.access(serverSocketName, fs.constants.F_OK, (err) => {
fs.access(sockName, fs.constants.F_OK, (err) => {
if (err) { if (err) {
const dir = path.dirname(sockName); const dir = path.dirname(serverSocketName);
fs.mkdir(dir, () => resolve()); fs.mkdir(dir, () => resolve());
} else { } else {
fs.unlink(sockName, () => resolve()); fs.unlink(serverSocketName, () => resolve());
} }
}); });
} else if (process.platform === 'win32') { } else if (process.platform === 'win32') {
return resolve(); return resolve();
} else { } else {
fs.unlink(getBrowserExtensionSocketName(config), () => resolve()); fs.unlink(serverSocketName, () => resolve());
} }
}); });
} }
@ -108,38 +115,6 @@ function isSocketNameTooLong(socketName) {
return socketName.length > maxLength; return socketName.length > maxLength;
} }
function checkSocketIdentity(socket) {
const state = connectedSocketState.get(socket);
if (!state) {
return;
}
// TODO: implement this
state.active = true;
state.appName = 'TODO';
state.extensionName = 'TODO';
state.pid = 0;
state.supportsNotifications = state.appName !== 'Safari';
logger.info(
`Socket ${state.socketId} activated`,
`app: ${state.appName}`,
`extension: ${state.extensionName}`,
`pid: ${state.pid}`
);
sendToRenderer('browserExtensionSocketConnected', state.socketId, {
connectionId: state.socketId,
appName: state.appName,
extensionName: state.extensionName,
pid: state.pid,
supportsNotifications: state.supportsNotifications
});
processPendingSocketData(socket);
}
function onSocketClose(socket) { function onSocketClose(socket) {
const state = connectedSocketState.get(socket); const state = connectedSocketState.get(socket);
connectedSocketState.delete(socket); connectedSocketState.delete(socket);
@ -174,7 +149,7 @@ function onSocketData(socket, data) {
async function processPendingSocketData(socket) { async function processPendingSocketData(socket) {
const state = connectedSocketState.get(socket); const state = connectedSocketState.get(socket);
if (!state?.active) { if (!state) {
return; return;
} }
if (!state.pendingData || state.processingData) { if (!state.pendingData || state.processingData) {
@ -218,6 +193,11 @@ async function processPendingSocketData(socket) {
return; return;
} }
if (!state.active) {
await processFirstMessageFromSocket(socket, request);
return;
}
logger.debug(`Extension[${state.socketId}] -> KeeWeb`, request); logger.debug(`Extension[${state.socketId}] -> KeeWeb`, request);
if (!request) { if (!request) {
@ -251,6 +231,78 @@ async function processPendingSocketData(socket) {
sendToRenderer('browserExtensionSocketRequest', state.socketId, request); sendToRenderer('browserExtensionSocketRequest', state.socketId, request);
} }
async function processFirstMessageFromSocket(socket, message) {
const state = connectedSocketState.get(socket);
if (!state) {
return;
}
logger.debug(`Init connection ${state.socketId}`, message);
if (!message.origin) {
logger.error('Empty origin');
socket.destroy();
return;
}
if (!message.pid) {
logger.error('Empty pid');
socket.destroy();
return;
}
const extensionName = ExtensionOrigins[message.origin] || 'unknown';
const isSafari = message.origin === 'safari-keeweb-connect';
let appName;
if (isSafari) {
appName = 'Safari';
} else {
if (!message.ppid) {
logger.error('Empty ppid');
socket.destroy();
return;
}
let parentProcessInfo;
try {
parentProcessInfo = await getProcessInfo(message.ppid);
} catch (e) {
logger.error(`Cannot get info for PID ${message.ppid}: ${e}`);
socket.destroy();
return;
}
appName = parentProcessInfo.commandLine.split('/').pop();
appName = appName[0].toUpperCase() + appName.substr(1);
}
state.active = true;
state.appName = appName;
state.extensionName = extensionName;
state.pid = message.pid;
state.ppid = message.ppid;
state.supportsNotifications = state.appName !== 'Safari';
state.processingData = false;
logger.info(
`Socket ${state.socketId} activated for ` +
`app: "${state.appName}", ` +
`extension: "${state.extensionName}", ` +
`pid: ${state.pid}, ` +
`ppid: ${state.ppid}`
);
sendToRenderer('browserExtensionSocketConnected', state.socketId, {
connectionId: state.socketId,
appName: state.appName,
extensionName: state.extensionName,
pid: state.pid,
supportsNotifications: state.supportsNotifications
});
processPendingSocketData(socket);
}
function sendResultToSocket(socketId, result) { function sendResultToSocket(socketId, result) {
const socket = connectedSockets.get(socketId); const socket = connectedSockets.get(socketId);
if (socket) { if (socket) {

View File

@ -0,0 +1,40 @@
const childProcess = require('child_process');
function getProcessInfo(pid) {
return new Promise((resolve, reject) => {
const process = childProcess.spawn('/bin/ps', ['-opid=,ppid=,command=', '-p', pid]);
const data = [];
process.stdout.on('data', (chunk) => data.push(chunk));
process.on('close', () => {
const output = Buffer.concat(data).toString();
try {
const result = parsePsOutput(output);
if (result.pid !== pid) {
throw new Error(`PS pid mismatch: ${result.pid} <> ${pid}`);
}
resolve(result);
} catch (e) {
reject(e);
}
});
process.on('error', (e) => {
reject(e);
});
});
}
function parsePsOutput(output) {
const match = output.trim().match(/^(\d+)\s+(\d+)\s+(.+)$/);
if (!match) {
throw new Error(`Bad PS output: ${output}`);
}
return {
pid: match[1] | 0,
parentPid: match[2] | 0,
commandLine: match[3]
};
}
module.exports = { getProcessInfo };

View File

@ -1,7 +1,6 @@
#include <uv.h> #include <uv.h>
#include <algorithm> #include <algorithm>
#include <array>
#include <filesystem> #include <filesystem>
#include <iostream> #include <iostream>
#include <queue> #include <queue>
@ -12,20 +11,8 @@
// https://developer.chrome.com/docs/apps/nativeMessaging/#native-messaging-host-protocol // https://developer.chrome.com/docs/apps/nativeMessaging/#native-messaging-host-protocol
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging#app_side // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging#app_side
constexpr std::array kAllowedOrigins = {
// KeeWeb Connect: Chrome
std::string_view("chrome-extension://npmnaajonabmkjekongmjhdjpjdlhpkp/"),
// KeeWeb Connect: Firefox
std::string_view("keeweb-connect@keeweb.info"),
// KeePassXC-Browser: Chrome
std::string_view("chrome-extension://oboonakemofpalcgghocfoadofidjkkk/"),
// KeePassXC-Browser: Firefox
std::string_view("keepassxc-browser@keepassxc.org"),
// KeePassXC-Browser: Edge
std::string_view("chrome-extension://pdffhmdngciaglkoonimfcmckehcpafo/"),
};
struct State { struct State {
std::string origin;
uv_stream_t *tty_in = nullptr; uv_stream_t *tty_in = nullptr;
uv_stream_t *tty_out = nullptr; uv_stream_t *tty_out = nullptr;
uv_stream_t *keeweb_pipe = nullptr; uv_stream_t *keeweb_pipe = nullptr;
@ -44,24 +31,6 @@ void process_stdout_queue();
void close_keeweb_pipe(); void close_keeweb_pipe();
void connect_keeweb_pipe(); void connect_keeweb_pipe();
bool check_args(int argc, char *argv[]) {
if (argc < 2) {
std::cerr << "Expected origin argument" << std::endl;
return false;
}
for (int arg = 1; arg < argc; arg++) {
std::string origin = argv[arg];
auto found = std::find(kAllowedOrigins.begin(), kAllowedOrigins.end(), origin);
if (found != kAllowedOrigins.end()) {
return true;
}
}
std::cerr << "Bad origin" << std::endl;
return false;
}
void alloc_buf(uv_handle_t *, size_t size, uv_buf_t *buf) { void alloc_buf(uv_handle_t *, size_t size, uv_buf_t *buf) {
buf->base = new char[size]; buf->base = new char[size];
buf->len = static_cast<decltype(uv_buf_t::len)>(size); buf->len = static_cast<decltype(uv_buf_t::len)>(size);
@ -235,6 +204,25 @@ void connect_keeweb_pipe() {
void start_reading_stdin() { uv_read_start(state.tty_in, alloc_buf, stdin_read_cb); } void start_reading_stdin() { uv_read_start(state.tty_in, alloc_buf, stdin_read_cb); }
void push_first_message_to_keeweb() {
auto origin = state.origin;
std::replace(origin.begin(), origin.end(), '"', '\'');
auto message = "{\"pid\":" + std::to_string(uv_os_getpid()) +
",\"ppid\":" + std::to_string(uv_os_getppid()) + ",\"origin\":\"" + origin +
"\"}";
auto message_length = message.length() + sizeof(uint32_t);
auto data = new char[message_length];
auto size_ptr = reinterpret_cast<uint32_t *>(data);
*size_ptr = message.length();
memcpy(data + sizeof(uint32_t), message.c_str(), message.length());
state.pending_to_keeweb.emplace(
uv_buf_init(data, static_cast<decltype(uv_buf_t::len)>(message_length)));
}
void init_tty() { void init_tty() {
auto stdin_tty = new uv_tty_t{}; auto stdin_tty = new uv_tty_t{};
uv_tty_init(uv_default_loop(), stdin_tty, 0, 0); uv_tty_init(uv_default_loop(), stdin_tty, 0, 0);
@ -246,9 +234,13 @@ void init_tty() {
} }
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
if (!check_args(argc, argv)) { if (argc < 2) {
std::cerr << "Expected origin argument" << std::endl;
return 1; return 1;
} }
state.origin = argv[1];
push_first_message_to_keeweb();
init_tty(); init_tty();
start_reading_stdin(); start_reading_stdin();