Refactor native messaging wrapper

This commit is contained in:
hensm
2022-08-14 01:33:35 +01:00
parent 9f719132bf
commit c06c9e59a9
2 changed files with 87 additions and 115 deletions

View File

@@ -4,7 +4,7 @@ import semver from "semver";
import logger from "./logger"; import logger from "./logger";
import { Port } from "../messaging"; import { Port } from "../messaging";
import nativeMessaging from "./nativeMessaging"; import * as nativeMessaging from "./nativeMessaging";
import options from "./options"; import options from "./options";
export const BRIDGE_TIMEOUT = 5000; export const BRIDGE_TIMEOUT = 5000;

View File

@@ -8,108 +8,95 @@ import { Message, Port } from "../messaging";
type DisconnectListener = (port: Port) => void; type DisconnectListener = (port: Port) => void;
type MessageListener = (message: Message) => void; type MessageListener = (message: Message) => void;
function connectNative(application: string): Port { /**
/** * Create backup server URL from configured options.
* In order to preserve the synchronous API, messages are */
* queued before either the native messaging host or the async function getBackupServerUrl() {
* WebSocket connection is ready to send data. const { bridgeBackupHost, bridgeBackupPort, bridgeBackupPassword } =
*/ await options.getAll();
let messageQueue: object[] = [];
/** const url = new URL(`ws://${bridgeBackupHost}:${bridgeBackupPort}`);
* Set once the native messaging host is known to be either if (bridgeBackupPassword) {
* present/missing. Determines whether messages go to the url.searchParams.append("password", bridgeBackupPassword);
* message queue. }
*/
let isNativeHostStatusKnown = false;
const port = browser.runtime.connectNative(application); return url;
}
let socket: WebSocket; /**
* `browser.runtime.connectNative()` wrapper.
*/
export function connectNative(application: string): Port {
/** Whether native host or backup is ready for messages. */
let isNativeHostReady = false;
const onDisconnectListeners = new Set<DisconnectListener>(); let backupSocket: Nullable<WebSocket> = null;
const onMessageListeners = new Set<MessageListener>(); let backupMessageQueue: Message[] = [];
// Make initial connection to native host
const port = browser.runtime.connectNative(application); //
const messageListeners = new Set<MessageListener>();
const disconnectListeners = new Set<DisconnectListener>();
// Port proxy API
const portObject: Port = { const portObject: Port = {
name: "", name: "",
onDisconnect: { onDisconnect: {
addListener(cb: DisconnectListener) { addListener(cb: DisconnectListener) {
onDisconnectListeners.add(cb); disconnectListeners.add(cb);
}, },
removeListener(cb: DisconnectListener) { removeListener(cb: DisconnectListener) {
onDisconnectListeners.delete(cb); disconnectListeners.delete(cb);
}, },
hasListener(cb: DisconnectListener) { hasListener(cb: DisconnectListener) {
return onDisconnectListeners.has(cb); return disconnectListeners.has(cb);
}, },
hasListeners() { hasListeners() {
return onDisconnectListeners.size > 0; return disconnectListeners.size > 0;
} }
}, },
onMessage: { onMessage: {
addListener(cb: MessageListener) { addListener(cb: MessageListener) {
onMessageListeners.add(cb); messageListeners.add(cb);
}, },
removeListener(cb: MessageListener) { removeListener(cb: MessageListener) {
onMessageListeners.delete(cb); messageListeners.delete(cb);
}, },
hasListener(cb: MessageListener) { hasListener(cb: MessageListener) {
return onMessageListeners.has(cb); return messageListeners.has(cb);
}, },
hasListeners() { hasListeners() {
return onMessageListeners.size > 0; return messageListeners.size > 0;
} }
}, },
disconnect() { disconnect() {
if (socket) { if (backupSocket) {
socket.close(); backupSocket.close();
} else { } else {
port.disconnect(); port.disconnect();
} }
}, },
postMessage(message) { postMessage(message) {
if (socket) { if (!isNativeHostReady) {
switch (socket.readyState) { // Queue messages until ready
case WebSocket.CONNECTING: { backupMessageQueue.push(message);
// Queue message until WebSocket is ready } else if (backupSocket) {
messageQueue.push(message); backupSocket.send(JSON.stringify(message));
break; return;
}
case WebSocket.OPEN: {
socket.send(JSON.stringify(message));
break;
}
}
} else {
if (!isNativeHostStatusKnown) {
// Queue message until native messaging host is ready
messageQueue.push(message);
}
port.postMessage(message);
} }
port.postMessage(message);
} }
}; };
port.onDisconnect.addListener(async () => { port.onDisconnect.addListener(async () => {
const { const bridgeBackupEnabled = await options.get("bridgeBackupEnabled");
bridgeBackupEnabled,
bridgeBackupHost,
bridgeBackupPort,
bridgeBackupPassword
} = await options.getAll();
if (!bridgeBackupEnabled) { if (!bridgeBackupEnabled) {
portObject.error = { portObject.error = { message: "" };
message: "" for (const listener of disconnectListeners) {
};
for (const listener of onDisconnectListeners) {
listener(portObject); listener(portObject);
} }
@@ -118,38 +105,35 @@ function connectNative(application: string): Port {
); );
} }
if (port.error && !isNativeHostStatusKnown) { /**
isNativeHostStatusKnown = true; * If port disconnected because of an error and native host
* status had not already been resolved.
*/
if (port.error && !isNativeHostReady) {
backupSocket = new WebSocket(await getBackupServerUrl());
const url = new URL(`ws://${bridgeBackupHost}:${bridgeBackupPort}`); backupSocket.addEventListener("open", () => {
if (bridgeBackupPassword) { isNativeHostReady = true;
url.searchParams.append("password", bridgeBackupPassword);
}
socket = new WebSocket(url.href);
socket.addEventListener("open", () => {
// Send all messages in queue // Send all messages in queue
while (messageQueue.length) { while (backupMessageQueue.length) {
const message = messageQueue.pop(); backupSocket?.send(
socket.send(JSON.stringify(message)); JSON.stringify(backupMessageQueue.shift())
);
} }
}); });
backupSocket.addEventListener("message", ev => {
socket.addEventListener("message", ev => { for (const listener of messageListeners) {
for (const listener of onMessageListeners) {
listener(JSON.parse(ev.data)); listener(JSON.parse(ev.data));
} }
}); });
backupSocket.addEventListener("close", ev => {
socket.addEventListener("close", ev => { // If not a normal closure, set error message
if (ev.code !== 1000) { if (ev.code !== 1000) {
portObject.error = { portObject.error = { message: ev.reason };
message: ev.reason
};
} }
for (const listener of onDisconnectListeners) { for (const listener of disconnectListeners) {
listener(portObject); listener(portObject);
} }
}); });
@@ -157,12 +141,12 @@ function connectNative(application: string): Port {
}); });
port.onMessage.addListener((message: Message) => { port.onMessage.addListener((message: Message) => {
if (!isNativeHostStatusKnown) { if (!isNativeHostReady) {
isNativeHostStatusKnown = true; isNativeHostReady = true;
messageQueue = []; backupMessageQueue = [];
} }
for (const listener of onMessageListeners) { for (const listener of messageListeners) {
listener(message); listener(message);
} }
}); });
@@ -170,30 +154,27 @@ function connectNative(application: string): Port {
return portObject; return portObject;
} }
async function sendNativeMessage(application: string, message: Message) { /**
* `browser.runtime.sendNativeMessage()` wrapper.
*/
export async function sendNativeMessage(application: string, message: Message) {
try { try {
return await browser.runtime.sendNativeMessage(application, message); return await browser.runtime.sendNativeMessage(application, message);
} catch { } catch {
const { const bridgeBackupEnabled = await options.get("bridgeBackupEnabled");
bridgeBackupEnabled,
bridgeBackupHost,
bridgeBackupPort,
bridgeBackupPassword
} = await options.getAll();
if (!bridgeBackupEnabled) { if (!bridgeBackupEnabled) {
throw logger.error( throw logger.error(
"Bridge connection failed and backup not enabled." "Bridge connection failed and backup not enabled."
); );
} }
const url = new URL(`http://${bridgeBackupHost}:${bridgeBackupPort}`); const backupServerUrl = await getBackupServerUrl();
if (bridgeBackupPassword) {
url.searchParams.append("password", bridgeBackupPassword);
}
const res = await fetch(url.href); const backupServerHttpUrl = new URL(backupServerUrl);
if (res.status === 401) { backupServerHttpUrl.protocol = "http";
// Send HTTP request to check authentication
if ((await fetch(backupServerHttpUrl)).status === 401) {
logger.error( logger.error(
"Bridge daemon connection failed due to authentication error." "Bridge daemon connection failed due to authentication error."
); );
@@ -201,29 +182,20 @@ async function sendNativeMessage(application: string, message: Message) {
throw 401; throw 401;
} }
url.protocol = "ws";
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
const ws = new WebSocket(url.href); const backupSocket = new WebSocket(backupServerUrl);
ws.addEventListener("open", () => { backupSocket.addEventListener("open", () => {
ws.send(JSON.stringify(message)); backupSocket.send(JSON.stringify(message));
}); });
backupSocket.addEventListener("message", ev => {
ws.addEventListener("message", ev => { backupSocket.close();
ws.close();
resolve(JSON.parse(ev.data)); resolve(JSON.parse(ev.data));
}); });
backupSocket.addEventListener("error", () => {
ws.addEventListener("error", () => {
logger.error("Bridge daemon connection error."); logger.error("Bridge daemon connection error.");
reject(); reject();
}); });
}); });
} }
} }
export default {
connectNative,
sendNativeMessage
};