391 lines
9.6 KiB
JavaScript
391 lines
9.6 KiB
JavaScript
import HotPocketExtension from '../common';
|
|
|
|
const POST_AUTH_PATH = '/integrations/extension/post-authenticate/';
|
|
const RPC_PATH = '/rpc/';
|
|
|
|
let authSessionToken = null;
|
|
let rpcURL = null;
|
|
|
|
const updateRpcURL = () => {
|
|
rpcURL = null;
|
|
if (HotPocketExtension.base_url !== null) {
|
|
rpcURL = (new URL(RPC_PATH, HotPocketExtension.base_url)).toString();
|
|
}
|
|
|
|
HotPocketExtension.LOGGER.debug(
|
|
'HotPocketExtension.background.updateRpcURL()',
|
|
HotPocketExtension.base_url,
|
|
rpcURL,
|
|
);
|
|
};
|
|
|
|
const makeJSONRPCCall = (method, params) => {
|
|
return {
|
|
'jsonrpc': '2.0',
|
|
'id': (new Date().toISOString()),
|
|
'method': method,
|
|
'params': params,
|
|
};
|
|
};
|
|
|
|
const executeJSONRPCCall = async (url, call, {accessToken}) => {
|
|
HotPocketExtension.LOGGER.debug(
|
|
'HotPocketExtension.background.executeJSONRPCCall()', url, call, accessToken,
|
|
);
|
|
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
if (accessToken) {
|
|
headers['Authorization'] = `Bearer ${accessToken}`;
|
|
}
|
|
|
|
let result = null;
|
|
let error = null;
|
|
|
|
const effectiveURL = new URL(url);
|
|
effectiveURL.searchParams.append('method', call.method);
|
|
|
|
try {
|
|
const response = await fetch(
|
|
effectiveURL.toString(),
|
|
{
|
|
body: JSON.stringify(call),
|
|
credentials: 'include',
|
|
headers: headers,
|
|
method: 'POST',
|
|
},
|
|
);
|
|
HotPocketExtension.LOGGER.debug(
|
|
'HotPocketExtension.background.executeJSONRPCCall()', response,
|
|
);
|
|
|
|
if (response.status !== 200) {
|
|
error = {
|
|
code: -32000,
|
|
message: 'Fetch error',
|
|
data: response,
|
|
};
|
|
} else {
|
|
const callResult = await response.json();
|
|
if (callResult.error) {
|
|
HotPocketExtension.LOGGER.error(
|
|
'HotPocketExtension.background.executeJSONRPCCall(): RPC error',
|
|
callResult.error.code,
|
|
callResult.error.message,
|
|
callResult.error.data,
|
|
);
|
|
|
|
error = callResult.error;
|
|
} else {
|
|
result = callResult.result;
|
|
}
|
|
}
|
|
} catch (exception) {
|
|
HotPocketExtension.LOGGER.error(
|
|
'HotPocketExtension.background.executeJSONRPCCall(): Fetch error', exception,
|
|
);
|
|
error = {
|
|
code: -32000,
|
|
message: 'Fetch error',
|
|
data: exception,
|
|
};
|
|
}
|
|
|
|
return [result, error];
|
|
};
|
|
|
|
const getAccessTokenMeta = () => {
|
|
return {
|
|
platform: navigator.platform,
|
|
version: HotPocketExtension.version,
|
|
};
|
|
};
|
|
|
|
const doSave = async (accessToken, tab) => {
|
|
const call = makeJSONRPCCall('saves.create', [tab.url]);
|
|
const [result, error] = await executeJSONRPCCall(rpcURL, call, {accessToken});
|
|
HotPocketExtension.LOGGER.debug(
|
|
'HotPocketExtension.background.doSave():', result, error,
|
|
);
|
|
|
|
if (error !== null) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
const doCreateAndStoreAccessToken = async (authKey) => {
|
|
const accessTokenCall = makeJSONRPCCall(
|
|
'accounts.access_tokens.create',
|
|
[
|
|
authKey,
|
|
getAccessTokenMeta(),
|
|
],
|
|
);
|
|
|
|
const [accessToken, error] = await executeJSONRPCCall(
|
|
rpcURL, accessTokenCall, {accessToken: null},
|
|
);
|
|
|
|
if (error === null) {
|
|
await HotPocketExtension.api.storage.local.set({
|
|
accessToken: accessToken,
|
|
});
|
|
}
|
|
|
|
return [accessToken, error];
|
|
};
|
|
|
|
const doHandleAuthFlow = (authTab) => {
|
|
HotPocketExtension.LOGGER.debug(
|
|
'HotPocketExtension.background.doHandleAuthFlow()', authTab,
|
|
);
|
|
let currentAuthTabId = authTab.id;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const onTabsUpdated = (tabId, changeInfo, updatedTab) => {
|
|
const changedURL = changeInfo.url;
|
|
|
|
HotPocketExtension.LOGGER.debug(
|
|
'HotPocketExtension.background.doHandleAuthFlow.onTabsUpdated()',
|
|
updatedTab,
|
|
changedURL,
|
|
(changedURL && changedURL.includes(POST_AUTH_PATH)),
|
|
changeInfo,
|
|
);
|
|
|
|
const expectedSessionTabQuery = `?authSessionToken=${authSessionToken}`;
|
|
if (tabId !== currentAuthTabId && changedURL.includes(expectedSessionTabQuery)) {
|
|
// When redirecting from the preauth page to the HotPocket instance,
|
|
// Safari "replaces" the auth tab with a new one. This nasty hack will
|
|
// allow the extension to keep track of it.
|
|
// I hate computers.
|
|
currentAuthTabId = tabId;
|
|
}
|
|
|
|
if (tabId === currentAuthTabId) {
|
|
if (changedURL && changedURL.includes(POST_AUTH_PATH)) {
|
|
const parsedChangedURL = new URL(changedURL);
|
|
const authKey = parsedChangedURL.searchParams.get('auth_key');
|
|
|
|
doCreateAndStoreAccessToken(authKey).
|
|
then((result) => {
|
|
HotPocketExtension.LOGGER.debug(
|
|
'doHandleAuthFlow.onTabsUpdated.doGetAndStoreAccessToken.then()', result,
|
|
);
|
|
const [accessToken, error] = result;
|
|
|
|
if (error !== null) {
|
|
reject(error);
|
|
} else {
|
|
resolve(accessToken);
|
|
}
|
|
}).
|
|
catch((error) => {
|
|
reject(error);
|
|
}).
|
|
finally(() => {
|
|
authSessionToken = null;
|
|
HotPocketExtension.api.tabs.onUpdated.removeListener(onTabsUpdated);
|
|
HotPocketExtension.api.tabs.remove(currentAuthTabId);
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
HotPocketExtension.api.tabs.onUpdated.addListener(onTabsUpdated);
|
|
});
|
|
};
|
|
|
|
const doCheckAuth = async (accessToken) => {
|
|
if (accessToken === null) {
|
|
return null;
|
|
}
|
|
|
|
const call = makeJSONRPCCall(
|
|
'accounts.auth.check_access_token',
|
|
[accessToken, getAccessTokenMeta()],
|
|
);
|
|
|
|
const [result, error] = await executeJSONRPCCall(rpcURL, call, {
|
|
accessToken,
|
|
});
|
|
|
|
if (error !== null) {
|
|
if (error.data instanceof Error) {
|
|
throw error;
|
|
}
|
|
|
|
if (error.data.status === 403) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if (result === false) {
|
|
return null;
|
|
}
|
|
|
|
return accessToken;
|
|
};
|
|
|
|
const doSetupRPC = async () => {
|
|
let storageResult = await HotPocketExtension.api.storage.local.get(
|
|
['accessToken', 'baseURL'],
|
|
);
|
|
|
|
let accessToken = null;
|
|
if (storageResult.baseURL) {
|
|
HotPocketExtension.base_url = storageResult.baseURL;
|
|
updateRpcURL();
|
|
|
|
accessToken = await doCheckAuth(
|
|
storageResult.accessToken || null,
|
|
);
|
|
}
|
|
|
|
if (accessToken === null) {
|
|
authSessionToken = crypto.randomUUID();
|
|
|
|
const authTab = await HotPocketExtension.api.tabs.create({
|
|
url: HotPocketExtension.api.runtime.getURL(
|
|
`preauth.html?authSessionToken=${authSessionToken}`,
|
|
),
|
|
});
|
|
|
|
accessToken = await doHandleAuthFlow(authTab);
|
|
}
|
|
|
|
return accessToken;
|
|
};
|
|
|
|
const doSendTabMessage = (tab, message) => {
|
|
HotPocketExtension.api.tabs.sendMessage(tab.id, message).
|
|
then((result) => {
|
|
HotPocketExtension.LOGGER.debug(
|
|
'HotPocketExtension.background.doSendTabMessage(): message sent',
|
|
message,
|
|
result,
|
|
);
|
|
}).
|
|
catch((error) => {
|
|
HotPocketExtension.LOGGER.error(
|
|
'HotPocketExtension.background.doSendTabMessage(): could not send message',
|
|
error,
|
|
);
|
|
});
|
|
};
|
|
|
|
const doUpdateBaseURL = (nextBaseURL) => {
|
|
HotPocketExtension.base_url = nextBaseURL;
|
|
updateRpcURL();
|
|
|
|
HotPocketExtension.api.storage.local.
|
|
set({
|
|
baseURL: nextBaseURL,
|
|
}).
|
|
then(() => {
|
|
HotPocketExtension.LOGGER.debug(
|
|
'HotPocketExtension.background.doUpdateBaseURL()', 'Base URL saved',
|
|
);
|
|
}).
|
|
catch((error) => {
|
|
HotPocketExtension.LOGGER.error(
|
|
'HotPocketExtension.background.doUpdateBaseURL()', error,
|
|
);
|
|
});
|
|
};
|
|
|
|
const onTabCreated = (tab) => {
|
|
HotPocketExtension.LOGGER.debug(
|
|
'HotPocketExtension.background.onTabCreated()', tab,
|
|
);
|
|
HotPocketExtension.api.action.enable(tab.id).catch((error) => {
|
|
HotPocketExtension.LOGGER.error(
|
|
'HotPocketExtension.background.onTabCreated()', tab.id, error,
|
|
);
|
|
});
|
|
};
|
|
|
|
const onBrowserActionClicked = async (tab) => {
|
|
HotPocketExtension.LOGGER.debug(
|
|
'HotPocketExtension.background.onBrowserActionClicked()', tab.url,
|
|
);
|
|
|
|
if (!tab.url) {
|
|
return;
|
|
}
|
|
|
|
let result = false;
|
|
let error = null;
|
|
|
|
try {
|
|
let accessToken = await doSetupRPC();
|
|
|
|
result = await doSave(accessToken, tab);
|
|
HotPocketExtension.LOGGER.debug(
|
|
'HotPocketExtension.background.onBrowserActionClicked()', result,
|
|
);
|
|
} catch (exception) {
|
|
HotPocketExtension.LOGGER.error(
|
|
'Unhandled exception when handling action click',
|
|
exception,
|
|
);
|
|
error = exception;
|
|
}
|
|
|
|
const message = {
|
|
type: 'HotPocket:Extension:save',
|
|
result: result,
|
|
error: error,
|
|
};
|
|
|
|
doSendTabMessage(tab, message);
|
|
};
|
|
|
|
const onMessage = (message, sender, sendResponse) => {
|
|
HotPocketExtension.LOGGER.debug(
|
|
'HotPocketExtension.background.onMessage()', message, sender, sendResponse,
|
|
);
|
|
|
|
let response = {ok: true};
|
|
try {
|
|
if (message.type === 'HotPocket:Extension:ping') {
|
|
doSendTabMessage(sender.tab, {
|
|
type: 'HotPocket:Extension:pong',
|
|
});
|
|
} else if (message.type === 'HotPocket:Extension:setBaseURL') {
|
|
doUpdateBaseURL(message.result);
|
|
}
|
|
} catch (exception) {
|
|
HotPocketExtension.LOGGER.error(
|
|
'HotPocketExtension.background.onMessage(): Unhandled exception when handling content message',
|
|
message,
|
|
exception,
|
|
);
|
|
response.ok = false;
|
|
}
|
|
|
|
sendResponse(response);
|
|
|
|
return true;
|
|
};
|
|
|
|
export default ({...configuration}) => {
|
|
HotPocketExtension.configure(configuration, {
|
|
background: true,
|
|
});
|
|
|
|
updateRpcURL();
|
|
|
|
HotPocketExtension.api.tabs.onCreated.addListener(onTabCreated);
|
|
|
|
HotPocketExtension.api.action.onClicked.addListener(onBrowserActionClicked);
|
|
|
|
HotPocketExtension.api.runtime.onMessage.addListener(onMessage);
|
|
|
|
HotPocketExtension.LOGGER.info(`HotPocket v${HotPocketExtension.version} by BTHLabs`);
|
|
};
|