From 66d3c581fa953b340c2848bbae297848074a46cb Mon Sep 17 00:00:00 2001 From: Tanel Metsar Date: Tue, 7 Jan 2025 07:52:13 +0000 Subject: [PATCH 01/17] Add debug mode DevTools Signed-off-by: Tanel Metsar --- README.md | 5 - package-lock.json | 4 +- package.json | 2 +- rollup.config.mjs | 7 +- scripts/build.mjs | 9 +- .../services/NativeAppService.ts | 24 ++- .../actions/TokenSigning/getCertificate.ts | 31 ++- src/background/actions/TokenSigning/sign.ts | 30 ++- src/background/actions/TokenSigning/status.ts | 22 +- src/background/actions/authenticate.ts | 24 ++- .../actions/getSigningCertificate.ts | 20 +- src/background/actions/sign.ts | 19 +- src/background/actions/status.ts | 27 ++- src/background/background.ts | 35 ++- src/background/services/NativeAppService.ts | 54 +++-- src/config.ts | 25 ++- src/content/content.ts | 54 +++-- src/models/Browser.ts | 13 ++ src/models/Browser/Devtools.ts | 56 +++++ src/models/Browser/ExtensionStorage | 70 ++++++ src/models/Browser/Runtime.ts | 49 +++-- src/shared/Logger.ts | 200 ++++++++++++++++++ src/shared/actionErrorHandler.ts | 2 +- src/shared/configManager.ts | 43 ++++ src/shared/devToolsBridge.ts | 81 +++++++ src/shared/tokenSigningResponse.ts | 2 +- static/chrome/manifest.json | 7 + static/firefox/manifest.json | 4 + static/firefox/manifest_v3.json | 4 + static/safari/manifest.json | 4 + static/safari/manifest_v3.json | 4 + static/views/devtools/devtools.html | 13 ++ static/views/devtools/devtools.js | 28 +++ .../devtools/panels/devtools-webeid.html | 92 ++++++++ .../views/devtools/panels/devtools-webeid.js | 39 ++++ static/views/devtools/panels/webeid-events.js | 64 ++++++ static/views/devtools/panels/webeid-log.js | 86 ++++++++ .../views/devtools/panels/webeid-settings.js | 104 +++++++++ .../devtools/style/empty-element-messages.css | 7 + static/views/devtools/style/main.css | 132 ++++++++++++ static/views/devtools/style/page-events.css | 115 ++++++++++ static/views/devtools/style/page-log.css | 65 ++++++ static/views/devtools/style/page-settings.css | 36 ++++ static/views/devtools/style/reset.css | 11 + static/views/devtools/style/theme.css | 65 ++++++ static/views/options.html | 85 ++++++++ static/views/options.js | 34 +++ 47 files changed, 1802 insertions(+), 105 deletions(-) create mode 100644 src/models/Browser/Devtools.ts create mode 100644 src/models/Browser/ExtensionStorage create mode 100644 src/shared/Logger.ts create mode 100644 src/shared/configManager.ts create mode 100644 src/shared/devToolsBridge.ts create mode 100644 static/views/devtools/devtools.html create mode 100644 static/views/devtools/devtools.js create mode 100644 static/views/devtools/panels/devtools-webeid.html create mode 100644 static/views/devtools/panels/devtools-webeid.js create mode 100644 static/views/devtools/panels/webeid-events.js create mode 100644 static/views/devtools/panels/webeid-log.js create mode 100644 static/views/devtools/panels/webeid-settings.js create mode 100644 static/views/devtools/style/empty-element-messages.css create mode 100644 static/views/devtools/style/main.css create mode 100644 static/views/devtools/style/page-events.css create mode 100644 static/views/devtools/style/page-log.css create mode 100644 static/views/devtools/style/page-settings.css create mode 100644 static/views/devtools/style/reset.css create mode 100644 static/views/devtools/style/theme.css create mode 100644 static/views/options.html create mode 100644 static/views/options.js diff --git a/README.md b/README.md index 6b6fb05..688ed10 100644 --- a/README.md +++ b/README.md @@ -48,11 +48,6 @@ The Web eID extension for Safari is built as a [Safari web extension](https://de TOKEN_SIGNING_BACKWARDS_COMPATIBILITY=true npm run clean build package ``` - During development, for additional logging, set the `DEBUG` environment variable to `true`. - ```bash - DEBUG=true npm run clean build package - ``` - 5. Load in Firefox as a Temporary Extension 1. Open `about:debugging#/runtime/this-firefox` 2. Click "Load temporary Add-on..." and open `/web-eid-webextension/dist/manifest.json` diff --git a/package-lock.json b/package-lock.json index 78a610e..cbb8c11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "web-eid-webextension", - "version": "2.4.1", + "version": "2.4.2-rc1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "web-eid-webextension", - "version": "2.4.1", + "version": "2.4.2-rc1", "license": "MIT", "devDependencies": { "@rollup/plugin-alias": "^5.1.1", diff --git a/package.json b/package.json index 07c8d40..5a8bb1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "web-eid-webextension", - "version": "2.4.1", + "version": "2.4.2-rc1", "description": "", "main": "src/index.js", "scripts": { diff --git a/rollup.config.mjs b/rollup.config.mjs index 893ddc4..4173daf 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -8,13 +8,10 @@ import license from "rollup-plugin-license"; import polyfill from "rollup-plugin-polyfill"; import resolve from "@rollup/plugin-node-resolve"; -const projectRootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url))); - // List of browsers to build for. const browsers = ["chrome", "firefox", "safari"]; const processEnvConf = { - DEBUG: process.env.DEBUG, TOKEN_SIGNING_BACKWARDS_COMPATIBILITY: process.env.TOKEN_SIGNING_BACKWARDS_COMPATIBILITY, } @@ -22,7 +19,7 @@ const pluginsConf = (environment) => [ alias({ entries: [{ find: "@web-eid.js", - replacement: path.resolve(projectRootDir, "dist/lib/web-eid.js/src"), + replacement: path.resolve(import.meta.dirname, "dist/lib/web-eid.js/src"), }], }), resolve({ rootDir: "./dist" }), @@ -30,7 +27,7 @@ const pluginsConf = (environment) => [ license({ banner: { content: { - file: path.join(projectRootDir, "LICENSE"), + file: path.join(import.meta.dirname, "LICENSE"), encoding: "utf-8", }, }, diff --git a/scripts/build.mjs b/scripts/build.mjs index d2953b5..b964304 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -111,10 +111,17 @@ const targets = { await cp("./static/icons", "./dist/safari"); rem( - "Copying static consent pages to Firefox dist directory" + "Copying static pages to Firefox dist directory" ); await cp("./static/_locales", "./dist/firefox/_locales"); await cp("./static/views", "./dist/firefox/views"); + await cp("./node_modules/webextension-polyfill/dist", "./dist/firefox/views"); + + rem( + "Copying static pages to Chrome dist directory" + ); + await cp("./static/views", "./dist/chrome/views"); + await cp("./node_modules/webextension-polyfill/dist", "./dist/chrome/views"); rem( "Setting up the Firefox manifest" diff --git a/src/background-safari/services/NativeAppService.ts b/src/background-safari/services/NativeAppService.ts index 1f5adf5..022dda8 100644 --- a/src/background-safari/services/NativeAppService.ts +++ b/src/background-safari/services/NativeAppService.ts @@ -27,7 +27,11 @@ import libraryConfig from "@web-eid.js/config"; import Mutex from "../../shared/Mutex"; import calculateJsonSize from "../../shared/utils/calculateJsonSize"; -import config from "../../config"; +import { config } from "../../shared/configManager"; + +import Logger from "../../shared/Logger"; + +const logger = new Logger("NativeAppService.ts"); type NativeAppPendingRequest = | { resolve?: (value: PromiseLike) => void; reject?: (reason?: any) => void } @@ -47,7 +51,13 @@ export default class NativeAppService { private pending: NativeAppPendingRequest = null; + constructor(tabId?: number) { + logger.tabId = tabId; + } + async connect(): Promise<{ version: string }> { + logger.log("Connecting to the native application " + config.NATIVE_APP_NAME); + this.state = NativeAppState.CONNECTING; try { @@ -71,6 +81,8 @@ export default class NativeAppService { if (message.version) { this.state = NativeAppState.CONNECTED; + logger.info("Connected to the native application", message); + return message; } @@ -91,7 +103,8 @@ export default class NativeAppService { } close(error?: any): void { - config.DEBUG && console.log("Disconnecting from native app"); + logger.info("Closing connection to native application"); + this.state = NativeAppState.DISCONNECTED; this.pending?.reject?.(error); @@ -111,6 +124,8 @@ export default class NativeAppService { setTimeout(() => { reject(throwAfterTimeout); }, timeout ); const onResponse = (message: any): void => { + logger.log("Response from native application", message); + this.pending = null; if (message.error) { @@ -120,13 +135,14 @@ export default class NativeAppService { } }; - config.DEBUG && console.log("Sending message to native app", JSON.stringify(message)); - const messageSize = calculateJsonSize(message); + logger.info("Calculated message size", messageSize); + if (messageSize > config.NATIVE_MESSAGE_MAX_BYTES) { throw new Error(`native application message exceeded ${config.NATIVE_MESSAGE_MAX_BYTES} bytes`); } + logger.log("Sending message to native application", message); browser.runtime.sendNativeMessage("application.id", message, onResponse); }); diff --git a/src/background/actions/TokenSigning/getCertificate.ts b/src/background/actions/TokenSigning/getCertificate.ts index c350512..c2173bb 100644 --- a/src/background/actions/TokenSigning/getCertificate.ts +++ b/src/background/actions/TokenSigning/getCertificate.ts @@ -30,27 +30,42 @@ import { } from "../../../models/TokenSigning/TokenSigningResponse"; import ByteArray from "../../../shared/ByteArray"; +import { MessageSender } from "../../../models/Browser/Runtime"; import NativeAppService from "../../services/NativeAppService"; -import config from "../../../config"; +import { config } from "../../../shared/configManager"; import errorToResponse from "./errorToResponse"; import threeLetterLanguageCodes from "./threeLetterLanguageCodes"; import tokenSigningResponse from "../../../shared/tokenSigningResponse"; +import Logger from "../../../shared/Logger"; + +const logger = new Logger("TokenSigning/getCertificate.ts"); + export default async function getCertificate( + sender: MessageSender, nonce: string, sourceUrl: string, lang?: string, filter: "AUTH" | "SIGN" = "SIGN", ): Promise { + logger.tabId = sender.tab?.id; + + logger.log("Certificate requested"); + if (lang && Object.keys(threeLetterLanguageCodes).includes(lang)) { lang = threeLetterLanguageCodes[lang]; + logger.log("Language code converted to three-letter code", lang); } - const nativeAppService = new NativeAppService(); + const nativeAppService = new NativeAppService(sender.tab?.id); + + logger.info("Checking 'filter', should be 'SIGN'"); if (filter !== "SIGN") { const { message, name, stack } = new Error("Web-eID only allows signing with a signing certificate"); + logger.error({ message, name, stack }); + return tokenSigningResponse("not_allowed", nonce, { message, name, @@ -59,9 +74,7 @@ export default async function getCertificate( } try { - const nativeAppStatus = await nativeAppService.connect(); - - config.DEBUG && console.log("Get certificate: connected to native", nativeAppStatus); + await nativeAppService.connect(); const message: NativeGetSigningCertificateRequest = { command: "get-signing-certificate", @@ -80,14 +93,20 @@ export default async function getCertificate( ); if (!response?.certificate) { + logger.info("Certificate request failed. Expected 'certificate' was not found in the response"); + return tokenSigningResponse("no_certificates", nonce); } else { + logger.info("Returning success response"); + return tokenSigningResponse("ok", nonce, { cert: new ByteArray().fromBase64(response.certificate).toHex(), }); } } catch (error) { - console.error(error); + logger.info("Certificate request failed"); + logger.error(error); + return errorToResponse(nonce, error); } finally { nativeAppService.close(); diff --git a/src/background/actions/TokenSigning/sign.ts b/src/background/actions/TokenSigning/sign.ts index 713d030..7c5c3f8 100644 --- a/src/background/actions/TokenSigning/sign.ts +++ b/src/background/actions/TokenSigning/sign.ts @@ -30,12 +30,17 @@ import { } from "../../../models/TokenSigning/TokenSigningResponse"; import ByteArray from "../../../shared/ByteArray"; +import { MessageSender } from "../../../models/Browser/Runtime"; import NativeAppService from "../../services/NativeAppService"; -import config from "../../../config"; +import { config } from "../../../shared/configManager"; import errorToResponse from "./errorToResponse"; import threeLetterLanguageCodes from "./threeLetterLanguageCodes"; import tokenSigningResponse from "../../../shared/tokenSigningResponse"; +import Logger from "../../../shared/Logger"; + +const logger = new Logger("TokenSigning/sign.ts"); + const digestCommandToHashFunction = { "sha224": "SHA-224", @@ -60,6 +65,7 @@ const hashFunctionToLength = { } as Record; export default async function sign( + sender: MessageSender, nonce: string, sourceUrl: string, certificate: string, @@ -67,17 +73,21 @@ export default async function sign( algorithm: string, lang?: string, ): Promise { + logger.tabId = sender.tab?.id; + + logger.log("Signing requested"); + if (lang && Object.keys(threeLetterLanguageCodes).includes(lang)) { lang = threeLetterLanguageCodes[lang]; + logger.info("Language code converted to three-letter code", lang); } - const nativeAppService = new NativeAppService(); + const nativeAppService = new NativeAppService(sender.tab?.id); try { const warnings: Array = []; - const nativeAppStatus = await nativeAppService.connect(); - config.DEBUG && console.log("Sign: connected to native", nativeAppStatus); + await nativeAppService.connect(); let hashFunction = ( Object.keys(digestCommandToHashFunction).includes(algorithm) @@ -85,12 +95,16 @@ export default async function sign( : algorithm ); + logger.debug("Selected hashing function", hashFunction); + const expectedHashByteLength = ( Object.keys(hashFunctionToLength).includes(hashFunction) ? hashFunctionToLength[hashFunction] : undefined ); + logger.debug("Hash byte length", expectedHashByteLength); + const hashByteArray = new ByteArray().fromHex(hash); if (hashByteArray.length !== expectedHashByteLength) { @@ -113,6 +127,8 @@ export default async function sign( } } + warnings.forEach((message) => logger.warn(message)); + const message: NativeSignRequest = { command: "sign", @@ -134,8 +150,10 @@ export default async function sign( ); if (!response?.signature) { + logger.info("Signing failed. Expected 'signature' was not found in the response"); return tokenSigningResponse("technical_error", nonce); } else { + logger.info("Returning success response"); return tokenSigningResponse("ok", nonce, { signature: new ByteArray().fromBase64(response.signature).toHex(), @@ -143,7 +161,9 @@ export default async function sign( }); } } catch (error) { - console.error(error); + logger.info("Signing failed"); + logger.error(error); + return errorToResponse(nonce, error); } finally { nativeAppService.close(); diff --git a/src/background/actions/TokenSigning/status.ts b/src/background/actions/TokenSigning/status.ts index fe03b09..212dd2a 100644 --- a/src/background/actions/TokenSigning/status.ts +++ b/src/background/actions/TokenSigning/status.ts @@ -27,16 +27,26 @@ import { TokenSigningStatusResponse, } from "../../../models/TokenSigning/TokenSigningResponse"; +import { MessageSender } from "../../../models/Browser/Runtime"; import NativeAppService from "../../services/NativeAppService"; -import config from "../../../config"; +import { config } from "../../../shared/configManager"; import errorToResponse from "./errorToResponse"; import tokenSigningResponse from "../../../shared/tokenSigningResponse"; +import Logger from "../../../shared/Logger"; + +const logger = new Logger("TokenSigning/status.ts"); + export default async function status( + sender: MessageSender, nonce: string, ): Promise { - const nativeAppService = new NativeAppService(); + logger.tabId = sender.tab?.id; + + const nativeAppService = new NativeAppService(sender.tab?.id); + + logger.log("Status requested"); try { const nativeAppStatus = await nativeAppService.connect(); @@ -48,15 +58,21 @@ export default async function status( throw new Error("missing native application version"); } + logger.info("Closing native app by sending the 'quit' command"); + await nativeAppService.send( { command: "quit", arguments: {} }, config.NATIVE_GRACEFUL_DISCONNECT_TIMEOUT, new UnknownError("native application failed to close gracefully"), ); + logger.info("Returning success response"); + return tokenSigningResponse("ok", nonce, { version }); } catch (error) { - console.error(error); + logger.info("Status check failed"); + logger.error(error); + return errorToResponse(nonce, error); } finally { nativeAppService.close(); diff --git a/src/background/actions/authenticate.ts b/src/background/actions/authenticate.ts index c7e2e22..0136761 100644 --- a/src/background/actions/authenticate.ts +++ b/src/background/actions/authenticate.ts @@ -20,19 +20,22 @@ * SOFTWARE. */ +import { ExtensionAuthenticateResponse, ExtensionFailureResponse } from "@web-eid.js/models/message/ExtensionResponse"; import Action from "@web-eid.js/models/Action"; import { NativeAuthenticateRequest } from "@web-eid.js/models/message/NativeRequest"; import { NativeAuthenticateResponse } from "@web-eid.js/models/message/NativeResponse"; import UserTimeoutError from "@web-eid.js/errors/UserTimeoutError"; -import { ExtensionAuthenticateResponse, ExtensionFailureResponse } from "@web-eid.js/models/message/ExtensionResponse"; import { MessageSender } from "../../models/Browser/Runtime"; import NativeAppService from "../services/NativeAppService"; import UnknownError from "@web-eid.js/errors/UnknownError"; import actionErrorHandler from "../../shared/actionErrorHandler"; -import config from "../../config"; import { getSenderUrl } from "../../shared/utils/sender"; +import Logger from "../../shared/Logger"; + +const logger = new Logger("authenticate.ts"); + export default async function authenticate( challengeNonce: string, sender: MessageSender, @@ -40,15 +43,17 @@ export default async function authenticate( userInteractionTimeout: number, lang?: string, ): Promise { + logger.tabId = sender.tab?.id; + + logger.log("Authentication requested"); + let nativeAppService: NativeAppService | undefined; let nativeAppStatus: { version: string } | undefined; try { - nativeAppService = new NativeAppService(); + nativeAppService = new NativeAppService(sender.tab?.id); nativeAppStatus = await nativeAppService.connect(); - config.DEBUG && console.log("Authenticate: connected to native", nativeAppStatus); - const message: NativeAuthenticateRequest = { command: "authenticate", @@ -67,8 +72,6 @@ export default async function authenticate( new UserTimeoutError(), ); - config.DEBUG && console.log("Authenticate: authentication token received"); - const isResponseValid = ( response?.unverifiedCertificate && response?.algorithm && @@ -78,12 +81,17 @@ export default async function authenticate( ); if (isResponseValid) { + logger.info("Returning success response"); + return { action: Action.AUTHENTICATE_SUCCESS, ...response }; } else { + logger.info("Authentication response is invalid"); + throw new UnknownError("unexpected response from native application"); } } catch (error) { - console.error("Authenticate:", error); + logger.info("Authentication failed"); + logger.error(error); return actionErrorHandler(Action.AUTHENTICATE_FAILURE, error, libraryVersion, nativeAppStatus?.version); } finally { diff --git a/src/background/actions/getSigningCertificate.ts b/src/background/actions/getSigningCertificate.ts index e3620ac..4de4c71 100644 --- a/src/background/actions/getSigningCertificate.ts +++ b/src/background/actions/getSigningCertificate.ts @@ -34,24 +34,29 @@ import { import { MessageSender } from "../../models/Browser/Runtime"; import NativeAppService from "../services/NativeAppService"; import actionErrorHandler from "../../shared/actionErrorHandler"; -import config from "../../config"; import { getSenderUrl } from "../../shared/utils/sender"; +import Logger from "../../shared/Logger"; + +const logger = new Logger("getSigningCertificate.ts"); + export default async function getSigningCertificate( sender: MessageSender, libraryVersion: string, userInteractionTimeout: number, lang?: string, ): Promise { + logger.tabId = sender.tab?.id; + + logger.log("Certificate requested"); + let nativeAppService: NativeAppService | undefined; let nativeAppStatus: { version: string } | undefined; try { - nativeAppService = new NativeAppService(); + nativeAppService = new NativeAppService(sender.tab?.id); nativeAppStatus = await nativeAppService.connect(); - config.DEBUG && console.log("getSigningCertificate: connected to native", nativeAppStatus); - const message: NativeGetSigningCertificateRequest = { command: "get-signing-certificate", @@ -74,12 +79,17 @@ export default async function getSigningCertificate( ); if (isResponseValid) { + logger.info("Returning success response"); + return { action: Action.GET_SIGNING_CERTIFICATE_SUCCESS, ...response }; } else { + logger.info("Certificate response is invalid"); + throw new UnknownError("unexpected response from native application"); } } catch (error: any) { - console.error("GetSigningCertificate:", error); + logger.info("Certificate request failed"); + logger.error(error); return actionErrorHandler(Action.GET_SIGNING_CERTIFICATE_FAILURE, error, libraryVersion, nativeAppStatus?.version); } finally { diff --git a/src/background/actions/sign.ts b/src/background/actions/sign.ts index 4ad980d..8583e6c 100644 --- a/src/background/actions/sign.ts +++ b/src/background/actions/sign.ts @@ -34,9 +34,11 @@ import UserTimeoutError from "@web-eid.js/errors/UserTimeoutError"; import { MessageSender } from "../../models/Browser/Runtime"; import NativeAppService from "../services/NativeAppService"; import actionErrorHandler from "../../shared/actionErrorHandler"; -import config from "../../config"; import { getSenderUrl } from "../../shared/utils/sender"; +import Logger from "../../shared/Logger"; + +const logger = new Logger("sign.ts"); export default async function sign( certificate: string, @@ -47,15 +49,17 @@ export default async function sign( userInteractionTimeout: number, lang?: string, ): Promise { + logger.tabId = sender.tab?.id; + + logger.log("Signing requested"); + let nativeAppService: NativeAppService | undefined; let nativeAppStatus: { version: string } | undefined; try { - nativeAppService = new NativeAppService(); + nativeAppService = new NativeAppService(sender.tab?.id); nativeAppStatus = await nativeAppService.connect(); - config.DEBUG && console.log("Sign: connected to native", nativeAppStatus); - const message: NativeSignRequest = { command: "sign", @@ -84,12 +88,17 @@ export default async function sign( ); if (isResponseValid) { + logger.info("Returning success response"); + return { action: Action.SIGN_SUCCESS, ...response }; } else { + logger.info("Signing response is invalid"); + throw new UnknownError("unexpected response from native application"); } } catch (error) { - console.error("Sign:", error); + logger.info("Signing failed"); + logger.error(error); return actionErrorHandler(Action.SIGN_FAILURE, error, libraryVersion, nativeAppStatus?.version); } finally { diff --git a/src/background/actions/status.ts b/src/background/actions/status.ts index f66b147..5c384b1 100644 --- a/src/background/actions/status.ts +++ b/src/background/actions/status.ts @@ -31,16 +31,24 @@ import UnknownError from "@web-eid.js/errors/UnknownError"; import VersionMismatchError from "@web-eid.js/errors/VersionMismatchError"; import { serializeError } from "@web-eid.js/utils/errorSerializer"; +import { MessageSender } from "../../models/Browser/Runtime"; import NativeAppService from "../services/NativeAppService"; import checkCompatibility from "../../shared/utils/checkCompatibility"; -import config from "../../config"; +import { config } from "../../shared/configManager"; + +import Logger from "../../shared/Logger"; + +const logger = new Logger("status.ts"); + +export default async function status(sender: MessageSender, libraryVersion: string): Promise { + logger.tabId = sender.tab?.id; + + logger.log("Status requested"); -export default async function status(libraryVersion: string): Promise { const extensionVersion = config.VERSION; - const nativeAppService = new NativeAppService(); + const nativeAppService = new NativeAppService(sender.tab?.id); try { - const status = await nativeAppService.connect(); const nativeApp = ( @@ -49,6 +57,8 @@ export default async function status(libraryVersion: string): Promise { switch (message.action) { case Action.AUTHENTICATE: @@ -66,6 +68,7 @@ async function onAction(message: ExtensionRequest, sender: MessageSender): Promi case Action.STATUS: return await status( + sender, message.libraryVersion, ); } @@ -77,12 +80,14 @@ async function onTokenSigningAction(message: TokenSigningMessage, sender: Messag switch (message.type) { case "VERSION": { return await TokenSigningAction.status( + sender, message.nonce, ); } case "CERT": { return await TokenSigningAction.getCertificate( + sender, message.nonce, sender.url, message.lang, @@ -92,6 +97,7 @@ async function onTokenSigningAction(message: TokenSigningMessage, sender: Messag case "SIGN": { return await TokenSigningAction.sign( + sender, message.nonce, sender.url, message.cert, @@ -104,10 +110,35 @@ async function onTokenSigningAction(message: TokenSigningMessage, sender: Messag } browser.runtime.onMessage.addListener((message, sender, sendResponse) => { + const logger = new Logger("background.ts"); + + logger.tabId = sender.tab?.id; + if ((message as ExtensionRequest).action) { - onAction(message, sender).then(sendResponse); + logger.devToolsEvent("request", "Extension (content)", "Extension (background)", message); + + onAction(message, sender) + .then((response) => { + logger.devToolsEvent("response", "Extension (content)", "Extension (background)", response); + + return response; + }) + .then(sendResponse); + + } else if (message.devtools) { + logger.devToolsProxy(message, sender); + sendResponse(); } else if ((message as TokenSigningMessage).type) { - onTokenSigningAction(message, sender).then(sendResponse); + logger.devToolsEvent("request", "Extension (content)", "Extension (background)", message); + + onTokenSigningAction(message, sender) + .then((response) => { + logger.devToolsEvent("response", "Extension (content)", "Extension (background)", response); + + return response; + }) + .then(sendResponse); } + return true; }); diff --git a/src/background/services/NativeAppService.ts b/src/background/services/NativeAppService.ts index fd83a15..878d4f7 100644 --- a/src/background/services/NativeAppService.ts +++ b/src/background/services/NativeAppService.ts @@ -27,7 +27,11 @@ import libraryConfig from "@web-eid.js/config"; import { NativeRequest } from "@web-eid.js/models/message/NativeRequest"; import { Port } from "../../models/Browser/Runtime"; import calculateJsonSize from "../../shared/utils/calculateJsonSize"; -import config from "../../config"; +import { config } from "../../shared/configManager"; + +import Logger from "../../shared/Logger"; + +const logger = new Logger("NativeAppService.ts"); export enum NativeAppState { UNINITIALIZED, @@ -41,7 +45,13 @@ export default class NativeAppService { private port: Port | null = null; + constructor(tabId?: number) { + logger.tabId = tabId; + } + async connect(): Promise<{ version: string }> { + logger.log("Connecting to the native application " + config.NATIVE_APP_NAME); + this.state = NativeAppState.CONNECTING; this.port = browser.runtime.connectNative(config.NATIVE_APP_NAME); @@ -58,6 +68,8 @@ export default class NativeAppService { if (message.version) { this.state = NativeAppState.CONNECTED; + logger.info("Connected to the native application"); + return message; } @@ -72,7 +84,7 @@ export default class NativeAppService { } } catch (error) { if (this.port.error) { - console.error(this.port.error); + logger.error(this.port.error); } if (error instanceof Error) { @@ -86,7 +98,7 @@ export default class NativeAppService { } disconnectListener(): void { - config.DEBUG && console.log("Native app disconnected."); + logger.log("Native app disconnected."); // Accessing lastError when it exists stops chrome from throwing it unnecessarily. chrome?.runtime?.lastError; @@ -95,36 +107,54 @@ export default class NativeAppService { } close(): void { - if (this.state == NativeAppState.DISCONNECTED) return; + logger.log("Closing connection to native application"); + + if (this.state == NativeAppState.DISCONNECTED) { + logger.info("Connection already closed"); + + return; + } this.state = NativeAppState.DISCONNECTED; this.port?.disconnect(); - - config.DEBUG && console.log("Native app port closed by extension."); } async send(message: NativeRequest, timeout: number, throwAfterTimeout: Error): Promise { switch (this.state) { case NativeAppState.CONNECTED: { - config.DEBUG && console.log("Sending message to native app", JSON.stringify(message)); - const messageSize = calculateJsonSize(message); + logger.debug("Calculated message size", messageSize); + if (messageSize > config.NATIVE_MESSAGE_MAX_BYTES) { throw new Error(`native application message exceeded ${config.NATIVE_MESSAGE_MAX_BYTES} bytes`); } + if (config.ALLOW_HTTP_LOCALHOST && message.arguments?.origin) { + const url = new URL(message.arguments.origin); + + if (url.protocol === "http:" && url.hostname === "localhost") { + url.protocol = "https:"; + + message.arguments.origin = url.origin; + } + } + + logger.log("Sending message to native application"); + logger.devToolsEvent("request", "Extension (background)", "Native app", message); + this.port?.postMessage(message); try { const response = await this.nextMessage(timeout, throwAfterTimeout); + logger.log("Response from native application"); + logger.devToolsEvent("response", "Extension (background)", "Native app", response); + this.close(); return response; } catch (error) { - console.error(error); - this.close(); throw error; @@ -173,9 +203,7 @@ export default class NativeAppService { const onDisconnectListener = (): void => { cleanup?.(); - reject(new NativeUnavailableError( - "a message from native application was expected, but native application closed connection" - )); + reject(new NativeUnavailableError("connection to the native app was closed")); }; cleanup = (): void => { diff --git a/src/config.ts b/src/config.ts index 6284362..6913459 100644 --- a/src/config.ts +++ b/src/config.ts @@ -32,10 +32,29 @@ export default Object.freeze({ */ NATIVE_GRACEFUL_DISCONNECT_TIMEOUT: 2000, // 2 seconds - // Default: false + /** + * Web eID is able to provide Chrome Token Signing backwards compatibility. + * When enabled, in addition to the Web eID library, the Web eID browser extension will process hwcrypto.js events. + * + * In this mode, Web-eID will need to inject the Token Signing page script to websites. + * + * This is a temporary solution. Web eID library should be used instead of hwcrypto.js. + * + * Disabled by default. + * + * @see https://github.com/open-eid/chrome-token-signing + * @see https://github.com/hwcrypto/hwcrypto.js/wiki + */ TOKEN_SIGNING_BACKWARDS_COMPATIBILITY: process.env.TOKEN_SIGNING_BACKWARDS_COMPATIBILITY?.toUpperCase() === "TRUE", TOKEN_SIGNING_USER_INTERACTION_TIMEOUT: 1000 * 60 * 5, // 5 minutes - // Default: false - DEBUG: process.env.DEBUG?.toUpperCase() === "TRUE", + /** + * Should the Web eID extension allow http://localhost. + * + * The extension checks if the browser context is secure and the native application checks for the HTTPS protocol. + * Localhost is considered to be secure context, even when HTTPS is not used. + * + * When this option is enabled, the extension reports localhost URLs to the native application with HTTPS. + */ + ALLOW_HTTP_LOCALHOST: false, }); diff --git a/src/content/content.ts b/src/content/content.ts index 5ef03e3..0da8f9a 100644 --- a/src/content/content.ts +++ b/src/content/content.ts @@ -28,6 +28,10 @@ import config from "../config"; import injectPageScript from "./TokenSigning/injectPageScript"; import tokenSigningResponse from "../shared/tokenSigningResponse"; +import Logger from "../shared/Logger"; + +const logger = new Logger("content.ts", { isContentScript: true }); + function isWebeidEvent(event: MessageEvent): boolean { return ( event.source === window && @@ -35,6 +39,19 @@ function isWebeidEvent(event: MessageEvent): boolean { ); } +function isWebeidResponseEvent(event: MessageEvent): boolean { + return ( + event.source === window && + event.data?.action?.startsWith?.("web-eid:") && + ( + event.data.action == Action.WARNING || + event.data.action.endsWith("-success") || + event.data.action.endsWith("-failure") || + event.data.action.endsWith("-ack") + ) + ); +} + function isTokenSigningEvent(event: MessageEvent): boolean { return ( event.source === window && @@ -48,13 +65,21 @@ async function send(message: object): Promise { return response; } +async function ack( + action: Action.AUTHENTICATE_ACK | Action.GET_SIGNING_CERTIFICATE_ACK | Action.SIGN_ACK | Action.STATUS_ACK, + origin: string +): Promise { + const message = { action }; + + await logger.devToolsEvent("response", "Website", "Extension (content)", message); + window.postMessage(message, origin); +} + window.addEventListener("message", async (event) => { if (isWebeidEvent(event)) { - // Warning messages should be ignored. - // When there are deprecation warnings, these messages would be sent by the content script and handled by the Web-eID library. - if (event.data.action === Action.WARNING) return; + if (isWebeidResponseEvent(event)) return; - config.DEBUG && console.log("Web-eID event: ", JSON.stringify(event)); + logger.devToolsEvent("request", "Website", "Extension (content)", event.data); if (!window.isSecureContext) { const response = { @@ -62,55 +87,60 @@ window.addEventListener("message", async (event) => { error: new ContextInsecureError(), }; - window.postMessage(response, event.origin); + logger.devToolsEvent("response", "Website", "Extension (content)", response); + window.postMessage(response, event.origin); } else { let response; switch (event.data.action) { case Action.STATUS: { - window.postMessage({ action: Action.STATUS_ACK }, event.origin); + await ack(Action.STATUS_ACK, event.origin); response = await send(event.data); break; } case Action.AUTHENTICATE: { - window.postMessage({ action: Action.AUTHENTICATE_ACK }, event.origin); + await ack(Action.AUTHENTICATE_ACK, event.origin); response = await send(event.data); break; } case Action.SIGN: { - window.postMessage({ action: Action.SIGN_ACK }, event.origin); + await ack(Action.SIGN_ACK, event.origin); response = await send(event.data); break; } case Action.GET_SIGNING_CERTIFICATE: { - window.postMessage({ action: Action.GET_SIGNING_CERTIFICATE_ACK }, event.origin); + await ack(Action.GET_SIGNING_CERTIFICATE_ACK, event.origin); response = await send(event.data); break; } } if (response) { + logger.devToolsEvent("response", "Website", "Extension (content)", response); + window.postMessage(response, event.origin); } } } else if (config.TOKEN_SIGNING_BACKWARDS_COMPATIBILITY && isTokenSigningEvent(event)) { - config.DEBUG && console.log("TokenSigning event:", JSON.stringify(event)); + logger.devToolsEvent("request", "Website", "Extension (content)", event.data); if (!window.isSecureContext) { - console.error(new ContextInsecureError()); + logger.error(new ContextInsecureError()); const nonce = event.data.nonce; const response = tokenSigningResponse("technical_error", nonce); + logger.devToolsEvent("response", "Website", "Extension (content)", response); + window.postMessage(response, event.origin); } else { const response = await send(event.data) as { warnings?: [], [key: string]: any } | void; - response?.warnings?.forEach((warning) => console.warn(warning)); + logger.devToolsEvent("response", "Website", "Extension (content)", response); window.postMessage(response, event.origin); } diff --git a/src/models/Browser.ts b/src/models/Browser.ts index 0f72d6f..6f7e75c 100644 --- a/src/models/Browser.ts +++ b/src/models/Browser.ts @@ -20,6 +20,7 @@ * SOFTWARE. */ +import Devtools from "./Browser/Devtools"; import Permissions from "./Browser/Permissions"; import Runtime from "./Browser/Runtime"; import Tabs from "./Browser/Tabs"; @@ -57,4 +58,16 @@ export default interface Browser { * @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs */ tabs: Tabs; + + /** + * Enables extensions to interact with the browser's Developer Tools. + * + * @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/devtools + */ + devtools: Devtools; + + /** + * + */ + storage: Storage; } diff --git a/src/models/Browser/Devtools.ts b/src/models/Browser/Devtools.ts new file mode 100644 index 0000000..4485682 --- /dev/null +++ b/src/models/Browser/Devtools.ts @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020-2024 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +export default interface Devtools { + + /** + * Interact with the window that Developer tools are attached to (inspected window). + * This includes obtaining the tab ID for the inspected page, evaluate the code + * in the context of the inspected window, reload the page, or obtain the list of resources within the page. + */ + inspectedWindow: Window; + + /** + * Obtain information about network requests associated with the window that + * the Developer Tools are attached to (the inspected window). + */ + network: any; + + /** + * Create User Interface panels that will be displayed inside User Agent Developer Tools. + */ + panels: { + create: (title: string, iconPath: string, pagePath: string) => Promise; + }; +} + +export interface ExtensionPanel { + onShown: { + addListener: (listener: () => void) => void; + removeListener: (listener: () => void) => void; + }; + + onHidden: { + addListener: (listener: () => void) => void; + removeListener: (listener: () => void) => void; + }; +} diff --git a/src/models/Browser/ExtensionStorage b/src/models/Browser/ExtensionStorage new file mode 100644 index 0000000..6fae284 --- /dev/null +++ b/src/models/Browser/ExtensionStorage @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2020-2024 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +export default interface ExtensionStorage { + + /** + * Represents the local storage area. Items in local storage are local to the machine the extension was installed on. + */ + local: StorageArea; + + /** + * Represents the managed storage area. Items in managed storage are set by the domain administrator and + * are read-only for the extension. Trying to modify this namespace results in an error. + */ + managed: StorageArea; + + /** + * Represents the session storage area. Items in session storage are stored in memory and are not persisted to disk. + */ + session: StorageArea; + + /** + * Represents the sync storage area. Items in sync storage are synced by the browser, + * and are available across all instances of that browser that the user is logged into, across different devices. + */ + sync: StorageArea; +} + +export interface StorageArea { + /** + * Retrieves one or more items from the storage area. + */ + get: (keys: null | string | object | Array) => Promise> + + /** + * Gets the amount of storage space (in bytes) used for one or more items in the storage area. + */ + getBytesInUse: (keys: null | string | Array) => Promise; + /** + * Stores one or more items in the storage area. If the item exists, its value is updated. + */ + set: (data: Record) => Promise; + /** + * Removes one or more items from the storage area. + */ + remove: (keys: string | Array) => Promise; + /** + * Removes all items from the storage area. + */ + clear: () => Promise; +} \ No newline at end of file diff --git a/src/models/Browser/Runtime.ts b/src/models/Browser/Runtime.ts index 9047a9b..1673d96 100644 --- a/src/models/Browser/Runtime.ts +++ b/src/models/Browser/Runtime.ts @@ -21,19 +21,6 @@ */ export default interface Runtime { - /** - * Fired when the extension is first installed, when the extension is updated to a new version, and when the browser is - * updated to a new version. - * - * Note that runtime.onInstalled is not the same as management.onInstalled. The runtime.onInstalled event is fired only - * for your extension. The browser.management.onInstalled event is fired for any extensions. - * - * @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onInstalled - */ - onInstalled: { - addListener: (callback: OnInstalledCallback) => void; - }; - /** * A string representing the extension ID. * @@ -42,10 +29,6 @@ export default interface Runtime { */ id: string; - onMessage: { - addListener: (callback: OnMessageCallback) => void; - }; - /** * Sends a single message from an extension to a native application. * @@ -120,6 +103,38 @@ export default interface Runtime { * Get the complete manifest.json file, deserialized from JSON to an object. */ getManifest: () => any; + + /** + * Fired when a connection is made with either an extension process or a content script. + * + * @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onConnect + */ + onConnect: { + addListener: (listener: (port: Port) => void) => void; + removeListener: (listener: () => void) => void; + }; + + /** + * Fired when the extension is first installed, when the extension is updated to a new version, + * and when the browser is updated to a new version. + * + * Note that runtime.onInstalled is not the same as management.onInstalled. The runtime.onInstalled event is + * fired only for your extension. The browser.management.onInstalled event is fired for any extensions. + * + * @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onInstalled + */ + onInstalled: { + addListener: (callback: OnInstalledCallback) => void; + }; + + /** + * Use this event to listen for messages from another part of your extension. + * + * @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onMessage + */ + onMessage: { + addListener: (callback: OnMessageCallback) => void; + }; } export interface Port { diff --git a/src/shared/Logger.ts b/src/shared/Logger.ts new file mode 100644 index 0000000..4bf3e9e --- /dev/null +++ b/src/shared/Logger.ts @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2020-2024 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { serializeError } from "@web-eid.js/utils/errorSerializer"; + +import { MessageSender } from "../models/Browser/Runtime"; +import devToolsBridge from "./devToolsBridge"; + +type Layer = "Website" | "Extension (content)" | "Extension (background)" | "Native app"; + +interface DevToolsLogMessage { + devtools: "log"; + source: string; + type: "log" | "info" | "warn" | "error"; + message: string; + time: string; +} + +interface DevToolsEventMessage { + devtools: "event"; + type: "request" | "response"; + layer1: Layer; + layer2: Layer; + data: any; + time: string; +} + +/* +let devtoolPorts: Array = []; + +browser.runtime.onConnect.addListener((port) => { + if (port.name === "webeid-devtools") { + devtoolPorts.push(port); + } + + port.onDisconnect.addListener(() => { + devtoolPorts = devtoolPorts.filter((devtoolsPort) => devtoolsPort != port); + }); +}); +*/ + +export default class Logger { + private module: string; + private isContentScript = false; + + public tabId?: number; + + constructor(module: string, options?: { isContentScript: boolean }) { + this.module = module; + this.isContentScript = options?.isContentScript ?? this.isContentScript; + } + + async log(...args: Array): Promise { + await this.devToolsLog("log", args); + console.log(...args); + } + + async info(...args: Array): Promise { + await this.devToolsLog("info", args); + console.info(...args); + } + + async warn(...args: Array): Promise { + await this.devToolsLog("warn", args); + console.warn(...args); + } + + async error(...args: Array): Promise { + await this.devToolsLog("error", args); + console.error(...args); + } + + async debug(...args: Array): Promise { + await this.devToolsLog("debug", args); + console.debug(...args); + } + + async isDevtoolsAvailable(): Promise { + return (await browser?.permissions?.contains({ "permissions": ["devtools"] })) ?? false; + } + + async devToolsLog(type: "event" | "log" | "info" | "warn" | "error" | "debug", rawMessage: Array) { + if (this.isContentScript || await this.isDevtoolsAvailable()) { + const time = this.getCurrentTime(); + const source = this.module; + + const message = rawMessage.map((obj) => { + if (obj instanceof Error || obj.stack != null) { + return serializeError(obj); + } else { + return obj; + } + }); + + const devToolsLogMessage = { + devtools: "log", + + source, + type, + message, + time, + }; + + if (this.isContentScript) { + browser.runtime.sendMessage(devToolsLogMessage); + } else { + devToolsBridge.send({ tabId: this.tabId, ...devToolsLogMessage }); + } + } + } + + async devToolsEvent(type: "request" | "response", layer1: Layer, layer2: Layer, data: any) { + if (this.isContentScript || await this.isDevtoolsAvailable()) { + const time = this.getCurrentTime(); + + const devToolsEventMessage: DevToolsEventMessage = { + devtools: "event", + + type, + data, + layer1, + layer2, + time, + }; + + if (this.isContentScript) { + browser.runtime.sendMessage(devToolsEventMessage); + } else { + devToolsBridge.send({ tabId: this.tabId, ...devToolsEventMessage }); + } + } + } + + devToolsProxy( + proxyMessage: DevToolsLogMessage | DevToolsEventMessage, + sender: MessageSender, + ) { + if (proxyMessage.devtools == "log") { + const { devtools, source, type, message, time } = proxyMessage; + + devToolsBridge.send({ + tabId: sender.tab?.id, + + devtools, + source, + type, + message, + time, + }); + } else if (proxyMessage.devtools == "event") { + const { devtools, type, data, layer1, layer2, time } = proxyMessage; + + devToolsBridge.send({ + tabId: sender.tab?.id, + + devtools, + type, + data, + layer1, + layer2, + time + }); + } + } + + getCurrentTime() { + const now = new Date(); + const [HH, mm, ss, SSS] = ( + [ + now.getHours(), + now.getMinutes(), + now.getSeconds(), + now.getMilliseconds(), + ] + .map((num) => num.toString()) + .map((str, i) => str.padStart(i == 3 ? 3 : 2, "0")) + ); + + return `${HH}:${mm}:${ss}.${SSS}`; + } +} diff --git a/src/shared/actionErrorHandler.ts b/src/shared/actionErrorHandler.ts index c9c2ff0..6268d71 100644 --- a/src/shared/actionErrorHandler.ts +++ b/src/shared/actionErrorHandler.ts @@ -27,7 +27,7 @@ import VersionMismatchError from "@web-eid.js/errors/VersionMismatchError"; import { serializeError } from "@web-eid.js/utils/errorSerializer"; import checkCompatibility from "./utils/checkCompatibility"; -import config from "../config"; +import { config } from "../shared/configManager"; export default function actionErrorHandler( action: diff --git a/src/shared/configManager.ts b/src/shared/configManager.ts new file mode 100644 index 0000000..bf3e8a3 --- /dev/null +++ b/src/shared/configManager.ts @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020-2024 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import defaultConfig from "../config"; + +type Config = { + -readonly [key in keyof typeof defaultConfig]: typeof defaultConfig[key]; +}; + +const config = JSON.parse(JSON.stringify(defaultConfig)) as Config; + +function setConfigOverride(key: K, value: typeof defaultConfig[K]) { + if (value === null) { + value = defaultConfig[key]; + } + + config[key] = value; +} + +export { + config, + defaultConfig, + setConfigOverride, +}; diff --git a/src/shared/devToolsBridge.ts b/src/shared/devToolsBridge.ts new file mode 100644 index 0000000..419c423 --- /dev/null +++ b/src/shared/devToolsBridge.ts @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2020-2024 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { config, defaultConfig, setConfigOverride } from "./configManager"; +import { Port } from "../models/Browser/Runtime"; + +class DevToolsBridge extends EventTarget { + + devToolPorts: Array = []; + + constructor() { + super(); + + browser.runtime.onConnect.addListener((port: Port) => { + if (port.name === "webeid-devtools") { + this.devToolPorts.push(port); + } + + port.onMessage.addListener((message) => { + if (message.devtools === "setting-set") { + setConfigOverride(message.key, message.value); + + this.send({ devtools: "settings", config, defaultConfig }, { ignore: port }); + } + }); + + port.onDisconnect.addListener(() => { + this.devToolPorts = this.devToolPorts.filter((connectedPort) => connectedPort !== port); + }); + + port.postMessage({ devtools: "settings", config, defaultConfig }); + }); + } + + send(message: object, options?: { ignore: Port }) { + this.devToolPorts + .filter((port) => !options?.ignore || port !== options.ignore) + .forEach((port) => port.postMessage(message)); + } + + async isDevToolsEnabled() { + const isDevToolsOptional = Boolean(browser.runtime.getManifest().optional_permissions?.includes("devtools")); + const hasDevToolsPermission = await browser.permissions.contains({ permissions: ["devtools"] }); + + if (isDevToolsOptional && hasDevToolsPermission) { + return true; + } + + const isStorageOptional = Boolean(browser.runtime.getManifest().optional_permissions?.includes("storage")); + const hasStoragePermission = await browser.permissions.contains({ permissions: ["storage"] }); + + if (isStorageOptional && hasStoragePermission) { + const { devtoolsEnabled } = await browser.storage.local.get(["devtoolsEnabled"]); + + return Boolean(devtoolsEnabled); + } + + return false; + } +} + +export default new DevToolsBridge(); diff --git a/src/shared/tokenSigningResponse.ts b/src/shared/tokenSigningResponse.ts index 2f7cbca..fa80901 100644 --- a/src/shared/tokenSigningResponse.ts +++ b/src/shared/tokenSigningResponse.ts @@ -25,7 +25,7 @@ import { TokenSigningResult, } from "../models/TokenSigning/TokenSigningResponse"; -import config from "../config"; +import { config } from "../shared/configManager"; /** * Helper function to compose a token signing response message diff --git a/static/chrome/manifest.json b/static/chrome/manifest.json index b6031cc..f94d6c8 100644 --- a/static/chrome/manifest.json +++ b/static/chrome/manifest.json @@ -27,9 +27,16 @@ "background": { "service_worker": "background.js" }, + "options_ui": { + "page": "views/options.html" + }, + "devtools_page": "views/devtools/devtools.html", "permissions": [ "nativeMessaging" ], + "optional_permissions": [ + "storage" + ], "host_permissions": [ "*://*/*" ] diff --git a/static/firefox/manifest.json b/static/firefox/manifest.json index 3521f0a..04d634c 100644 --- a/static/firefox/manifest.json +++ b/static/firefox/manifest.json @@ -31,8 +31,12 @@ "background.js" ] }, + "devtools_page": "views/devtools/devtools.html", "permissions": [ "*://*/*", "nativeMessaging" + ], + "optional_permissions": [ + "devtools" ] } diff --git a/static/firefox/manifest_v3.json b/static/firefox/manifest_v3.json index db11c1e..c3772cb 100644 --- a/static/firefox/manifest_v3.json +++ b/static/firefox/manifest_v3.json @@ -35,9 +35,13 @@ "background.js" ] }, + "devtools_page": "views/devtools/devtools.html", "permissions": [ "nativeMessaging" ], + "optional_permissions": [ + "devtools" + ], "host_permissions": [ "*://*/*" ] diff --git a/static/safari/manifest.json b/static/safari/manifest.json index 553e459..ebb74ba 100644 --- a/static/safari/manifest.json +++ b/static/safari/manifest.json @@ -22,8 +22,12 @@ "background.js" ] }, + "devtools_page": "views/devtools/devtools.html", "permissions": [ "*://*/*", "nativeMessaging" + ], + "optional_permissions": [ + "devtools" ] } diff --git a/static/safari/manifest_v3.json b/static/safari/manifest_v3.json index 4fb37fb..53763df 100644 --- a/static/safari/manifest_v3.json +++ b/static/safari/manifest_v3.json @@ -29,9 +29,13 @@ "background.js" ] }, + "devtools_page": "views/devtools/devtools.html", "permissions": [ "nativeMessaging" ], + "optional_permissions": [ + "devtools" + ], "host_permissions": [ "*://*/*" ] diff --git a/static/views/devtools/devtools.html b/static/views/devtools/devtools.html new file mode 100644 index 0000000..b82bfa0 --- /dev/null +++ b/static/views/devtools/devtools.html @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/views/devtools/devtools.js b/static/views/devtools/devtools.js new file mode 100644 index 0000000..64e147b --- /dev/null +++ b/static/views/devtools/devtools.js @@ -0,0 +1,28 @@ +async function isDevToolsEnabled() { + const isDevToolsOptional = Boolean(browser.runtime.getManifest().optional_permissions?.includes("devtools")); + + if (isDevToolsOptional) { + return true; + } + + const isStorageOptional = Boolean(browser.runtime.getManifest().optional_permissions?.includes("storage")); + const hasStoragePermission = await browser.permissions.contains({ permissions: ["storage"] }); + + if (isStorageOptional && hasStoragePermission) { + const { devtoolsEnabled } = await browser.storage.local.get(["devtoolsEnabled"]); + + return Boolean(devtoolsEnabled); + } + + return false; +} + +(async () => { + if (await isDevToolsEnabled()) { + browser.devtools.panels.create( + "Web eID", + "/icons/web-eid-icon-128.png", + "/views/devtools/panels/devtools-webeid.html", + ); + } +})(); diff --git a/static/views/devtools/panels/devtools-webeid.html b/static/views/devtools/panels/devtools-webeid.html new file mode 100644 index 0000000..06c7562 --- /dev/null +++ b/static/views/devtools/panels/devtools-webeid.html @@ -0,0 +1,92 @@ + + + + + + Web-eID Privacy + + + + +
+ + +
+
+
+
+
+

+    
+
+
+
+ +
+
+ Log levels: + + + + + + +
+
+
+
+
+
The settings you change here apply to all pages and persist even after closing DevTools.
+ + +
+ +
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/static/views/devtools/panels/devtools-webeid.js b/static/views/devtools/panels/devtools-webeid.js new file mode 100644 index 0000000..1f9adce --- /dev/null +++ b/static/views/devtools/panels/devtools-webeid.js @@ -0,0 +1,39 @@ +import events from "./webeid-events.js"; +import log from "./webeid-log.js"; +import settings from "./webeid-settings.js"; + + +const backgroundConnection = chrome.runtime.connect({ + name: "webeid-devtools", +}); + +backgroundConnection.onMessage.addListener((message) => { + if (!message.tabId || message.tabId === chrome.devtools.inspectedWindow.tabId) { + if (message.devtools === "log") { + log.append(message); + } else if (message.devtools === "event") { + events.append(message); + + const direction = ( + message.type === "request" + ? "—►" + : message.type === "response" + ? "◄—" + : "?" + ); + + log.append({ + time: message.time, + type: "event", + source: `${message.layer1} ${direction} ${message.layer2}`, + message: [message.data], + }); + } else if (message.devtools === "settings") { + const { config, defaultConfig } = message; + + settings.render(config, defaultConfig, backgroundConnection); + + document.querySelector('header .version').textContent = defaultConfig.VERSION; + } + } +}); diff --git a/static/views/devtools/panels/webeid-events.js b/static/views/devtools/panels/webeid-events.js new file mode 100644 index 0000000..37cbc46 --- /dev/null +++ b/static/views/devtools/panels/webeid-events.js @@ -0,0 +1,64 @@ +const ui = { + eventTemplate: document.querySelector("#event-template"), + eventList: document.querySelector("section[data-page='events'] #event-list"), + eventDetails: document.querySelector("section[data-page='events'] #event-details"), + clearButton: document.querySelector("section[data-page='events'] #clear-events"), +}; + +function createEventElement(layer1, layer2, type, time, name, data) { + const root = ui.eventTemplate.content.cloneNode(true); + + const el = { + event: root.querySelector(".event"), + input: root.querySelector("input"), + label: root.querySelector("label"), + layer1: root.querySelector(".layer-1"), + layer2: root.querySelector(".layer-2"), + direction: root.querySelector(".direction"), + time: root.querySelector(".time"), + name: root.querySelector(".name"), + }; + + el.label.dataset.indent = ({ + "Extension (content)": 1, + "Extension (background)": 2, + "Native app": 3, + })[layer1] ?? 0; + + el.input.id = "event-" + Math.random().toString(36).substring(2); + el.label.htmlFor = el.input.id; + + el.layer1.textContent = layer1; + el.layer2.textContent = layer2; + el.time.textContent = time; + el.name.textContent = name; + + el.direction.textContent = ( + type === "request" + ? "—►" + : type === "response" + ? "◄—" + : "" + ); + + if (type === "response") { + el.label.dataset.responseType = name.split(/-|:/g).at(-1); + } + + el.direction.dataset.type = type; + + el.input.addEventListener("change", () => { + ui.eventDetails.textContent = JSON.stringify(data, null, " "); + }); + + return el.event; +} + +export default { + append({ layer1, layer2, type, time, data }) { + const name = data.action ?? data.command ?? data.type ?? data.result ?? type; + const eventElement = createEventElement(layer1, layer2, type, time, name, data); + + ui.eventList.appendChild(eventElement); + }, +}; diff --git a/static/views/devtools/panels/webeid-log.js b/static/views/devtools/panels/webeid-log.js new file mode 100644 index 0000000..ada779c --- /dev/null +++ b/static/views/devtools/panels/webeid-log.js @@ -0,0 +1,86 @@ +const ui = { + logEntryTemplate: document.querySelector("#log-entry-template"), + logContainer: document.querySelector("section[data-page='log'] #log-messages"), + clearButton: document.querySelector("section[data-page='log'] #clear-log"), +}; + +function createLogEntryElement(type, time, message, source) { + const root = ui.logEntryTemplate.content.cloneNode(true); + + const el = { + entry: root.querySelector(".entry"), + time: root.querySelector(".time"), + message: root.querySelector(".message"), + source: root.querySelector(".source" ), + }; + + el.entry.classList.add(`type-${type}`); + + el.time.textContent = time; + el.message.textContent = message; + el.source.textContent = source; + + return el.entry; +} + +function stringifyError(error) { + const { + fileName, + lineNumber, + columnNumber, + message, + name, + code, + stack, + } = error; + + const prettyStack = ( + stack + .trim() + .split("\n") + .map((part) => "\t" + part) + .join("\n") + ); + + return `${name}: ${message}\n${prettyStack}`; +} + +function stringifyMessageArray(message) { + return ( + message + .map((obj) => { + if (typeof obj == "undefined") { + return "(undefined)"; + } else if (obj == null) { + return "(null)"; + } else if (obj instanceof Error || obj.stack != null) { + return stringifyError(obj); + } else if (obj instanceof Object) { + return JSON.stringify(obj, null, " "); + } else { + return String(obj); + } + }) + .join(", ") + ); +} + +ui.clearButton.addEventListener("click", () => { + [...ui.logContainer.children].forEach((child) => child.remove()); +}); + +export default { + append({ time, type, message, source }) { + const { clientHeight, scrollHeight, scrollTop } = ui.logContainer; + + const isScrolledToBottom = (scrollHeight - scrollTop) === clientHeight; + const messageString = stringifyMessageArray(message); + const logEntryElement = createLogEntryElement(type, time, messageString, source); + + ui.logContainer.appendChild(logEntryElement); + + if (isScrolledToBottom) { + ui.logContainer.scrollTo(0, ui.logContainer.scrollHeight); + } + }, +}; diff --git a/static/views/devtools/panels/webeid-settings.js b/static/views/devtools/panels/webeid-settings.js new file mode 100644 index 0000000..a70484a --- /dev/null +++ b/static/views/devtools/panels/webeid-settings.js @@ -0,0 +1,104 @@ +const excludedSettings = [ + "NATIVE_APP_NAME", + "TOKEN_SIGNING_BACKWARDS_COMPATIBILITY", +]; + +const ui = { + settingTemplate: document.querySelector("#setting-template"), + settingsTable: document.querySelector("section[data-page='settings'] #settings-list") +} + +export default { + render(config, defaultConfig, backgroundConnection) { + Object.assign(this, { config, defaultConfig, backgroundConnection }); + + const configRows = ( + Object + .entries(config) + .filter(([key]) => this.isVisible(key)) + .map(([key, value]) => this.createSettingRow(key, value)) + ); + + ui.settingsTable.replaceChildren(...configRows); + }, + + isVisible(settingKey) { + return !excludedSettings.includes(settingKey); + }, + + createSettingRow(key, value) { + const row = ui.settingTemplate.content.cloneNode(true); + + const el = { + key: row.querySelector("th"), + input: row.querySelector("input.value"), + reset: row.querySelector("input.reset"), + }; + + el.key.innerText = key; + + this.updateInput(el.input, value); + this.updateReset(el, key); + + const changeEvent = ( + el.input.type === "checkbox" + ? "change" + : "input" + ); + + el.input.addEventListener(changeEvent, () => { + const value = ({ + "text": el.input.value, + "number": el.input.valueAsNumber, + "checkbox": el.input.checked + })[el.input.type]; + + this.backgroundConnection.postMessage({ devtools: 'setting-set', key, value }); + + this.config[key] = value; + this.updateReset(el, key); + }); + + el.reset.addEventListener("click", () => { + this.config[key] = this.defaultConfig[key]; + el.reset.disabled = true; + + this.backgroundConnection.postMessage({ + devtools: 'setting-set', + key, + value: null, + }); + + this.updateInput(el.input, this.config[key]); + }); + + return row; + }, + + updateInput(input, value) { + switch (typeof value) { + case "number": { + input.type = "number"; + input.value = value; + break; + } + + case "boolean": { + input.type = "checkbox"; + input.checked = value; + break; + } + + default: + input.type = "text"; + input.value = value; + break; + } + }, + + updateReset(el, key) { + el.reset.disabled = ( + this.config[key] === this.defaultConfig[key] + ); + } +} diff --git a/static/views/devtools/style/empty-element-messages.css b/static/views/devtools/style/empty-element-messages.css new file mode 100644 index 0000000..4b44ac1 --- /dev/null +++ b/static/views/devtools/style/empty-element-messages.css @@ -0,0 +1,7 @@ +section[data-page="events"] #event-details:empty::before { + content: 'Select an event to inspect.'; +} + +section[data-page="events"] #event-list:empty::before { + content: 'The Web eID library has not sent any messages yet.'; +} diff --git a/static/views/devtools/style/main.css b/static/views/devtools/style/main.css new file mode 100644 index 0000000..e8220e2 --- /dev/null +++ b/static/views/devtools/style/main.css @@ -0,0 +1,132 @@ +@import url(theme.css); +@import url(reset.css); +@import url(page-events.css); +@import url(page-log.css); +@import url(page-settings.css); +@import url(empty-element-messages.css); + +body { + display: flex; + flex-direction: column; + height: 100vh; + overflow: hidden; + + font-family: var(--font-family); + color: var(--color-text-primary); + background: var(--color-background-primary); +} + +input[type="button"] { + margin: 0; + padding: var(--spacing-s); + font-size: var(--font-size); + font-family: var(--font-family); + + border: 1px solid var(--color-button-border); + color: var(--color-text-primary); + background-color: var(--color-background-primary); + + &:hover { + background-color: var(--color-background-secondary); + } + + &:active { + background-color: var(--color-background-tertiary); + } +} + +input[type="text"], +input[type="number"] { + margin: 0; + padding: 0 var(--spacing-s); + border-radius: 2px; + + border: 1px solid var(--color-input-border); + color: var(--color-text-primary); + background: var(--color-background-primary); +} + +header { + display: flex; + justify-content: space-between; + + gap: var(--spacing-m); + padding: 0 var(--spacing-m); + + border: 1px solid var(--color-border); + background-color: var(--color-background-secondary); + + nav { + font-size: var(--font-size); + + flex: 0 0 auto; + display: flex; + + input { + display: none; + } + + label { + line-height: 2rem; + padding: 0 var(--spacing-m); + + background-color: var(--color-background-secondary); + border-bottom: 0.125rem solid var(--color-tab-inactive); + transition: border-bottom-color var(--transition-duration) ease; + } + + input:checked+label { + border-bottom: 2px solid var(--color-tab-active); + } + } + + .version { + display: flex; + align-items: center; + + font-family: var(--font-family-version); + font-size: var(--font-size-version); + color: var(--color-version); + } +} + + +body:not(:has([data-nav="events"]:checked)) [data-page="events"] { display: none; } +body:not(:has([data-nav="log"]:checked)) [data-page="log"] { display: none; } +body:not(:has([data-nav="settings"]:checked)) [data-page="settings"] { display: none; } + +main { + position: relative; + flex: 1 1 100%; + overflow: hidden; + + font-size: var(--font-size); + + section { + height: 100%; + + border: 1px solid var(--color-border); + } +} + +.toolbar { + display:flex; + padding: var(--spacing-s) var(--spacing-m); + + border: 1px solid var(--color-border); + background-color: var(--color-background-tertiary); + + .group { + display: flex; + align-items: center; + gap: var(--spacing-s); + border-right: 1px solid var(--color-button-border); + padding-right: var(--spacing-m); + margin-right: var(--spacing-m); + + label { + display: flex; + align-items: center; + } + } +} diff --git a/static/views/devtools/style/page-events.css b/static/views/devtools/style/page-events.css new file mode 100644 index 0000000..aebbc50 --- /dev/null +++ b/static/views/devtools/style/page-events.css @@ -0,0 +1,115 @@ +section[data-page="events"] { + display: flex; + gap: 0.125rem; + + #event-list { + display: flex; + flex-direction: column; + flex: 0 0 40%; + + border: 1px solid var(--color-border); + + overflow-x: hidden; + overflow-y: auto; + + input { + display: none; + } + + input:checked+label { + background-color: var(--color-event-background-active); + box-shadow: inset 0 0 0 1px var(--color-event-border-active); + } + + label { + display: flex; + flex-direction: column; + padding: 0 var(--spacing-m); + border-bottom: 1px solid var(--color-border); + border-left: 0 solid var(--color-background-tertiary); + overflow: hidden; + + &[data-indent="1"] { border-left-width: var(--spacing-m); } + &[data-indent="2"] { border-left-width: calc(var(--spacing-m) * 2); } + &[data-indent="3"] { border-left-width: calc(var(--spacing-m) * 3); } + + &[data-response-type="ack"] { background-color: var(--color-event-ack); } + &[data-response-type="success"] { background-color: var(--color-event-success); } + &[data-response-type="failure"] { background-color: var(--color-event-failure); } + &[data-response-type="warning"] { background-color: var(--color-event-warning); } + + > div { + display: flex; + align-items: center; + width: 100%; + height: 2rem; + + &:first-child { + justify-content: flex-start; + + color: color-mix(in srgb, currentcolor 75%, transparent); + border-bottom: 1px dotted var(--color-border); + + .layer-1, .layer-2 { + overflow: hidden; + text-overflow: ellipsis; + } + + .layer-2 { + text-align: right; + } + + .direction { + display: flex; + flex: 0 0 2rem; + justify-content: center; + + &[data-type="request"] { + margin-right: auto; + } + + &[data-type="response"] { + margin-left: auto; + } + } + } + + &:last-child { + justify-content: space-between; + } + } + + .time { + height: 1.5rem; + display: flex; + align-items: center; + padding: 0 var(--spacing-s); + border-radius: 0.125rem; + + font-family: var(--font-family-timestamp); + font-size: var(--font-size-timestamp); + background-color: var(--color-background-tertiary); + } + } + } + + #event-details { + flex: 1 1 auto; + overflow: auto; + border: 1px solid var(--color-border); + + &:not(:empty) { + padding: var(--spacing-m); + } + } + + #event-list:empty::before, + #event-details:empty::before { + display: block; + padding: var(--spacing-m); + + color: var(--color-text-note); + font-family: var(--font-family); + font-size: var(--font-size); + } +} diff --git a/static/views/devtools/style/page-log.css b/static/views/devtools/style/page-log.css new file mode 100644 index 0000000..a6e8a33 --- /dev/null +++ b/static/views/devtools/style/page-log.css @@ -0,0 +1,65 @@ +section[data-page="log"] { + display: flex; + flex-direction: column; + overflow: hidden; + + #log-messages { + display: flex; + flex-direction: column; + overflow: auto; + + .entry { + display: flex; + padding: var(--spacing-m); + border: 1px dotted var(--color-border); + + border-left: var(--spacing-m) solid color-mix(in srgb, var(--color-log-level-default) 50%, transparent); + + &.type-info { + border-left-color: color-mix(in srgb, var(--color-log-level-info) 50%, transparent); + background-color: color-mix(in srgb, var(--color-log-level-info) 5%, transparent); + } + + &.type-warn { + border-left-color: color-mix(in srgb, var(--color-log-level-warn) 50%, transparent); + background-color: color-mix(in srgb, var(--color-log-level-warn) 5%, transparent); + } + + &.type-error { + border-left-color: color-mix(in srgb, var(--color-log-level-error) 50%, transparent); + background-color: color-mix(in srgb, var(--color-log-level-error) 5%, transparent); + } + + &.type-event { + border-left-color: color-mix(in srgb, var(--color-log-level-event) 50%, transparent); + background-color: color-mix(in srgb, var(--color-log-level-event) 5%, transparent); + } + + .time { + flex: 0 0 6rem; + color: var(--color-log-timestamp); + } + + .message { + flex: 1 1 100%; + white-space: pre-wrap; + } + + .source { + flex: 0 0 auto; + margin-left: var(--spacing-l); + + color: var(--color-text-note); + } + } + } + + +} + +body:not(:has([data-logfilter="log"]:checked)) #log-messages .entry.type-log { display: none; } +body:not(:has([data-logfilter="warn"]:checked)) #log-messages .entry.type-warn { display: none; } +body:not(:has([data-logfilter="error"]:checked)) #log-messages .entry.type-error { display: none; } +body:not(:has([data-logfilter="info"]:checked)) #log-messages .entry.type-info { display: none; } +body:not(:has([data-logfilter="debug"]:checked)) #log-messages .entry.type-debug { display: none; } +body:not(:has([data-logfilter="event"]:checked)) #log-messages .entry.type-event { display: none; } diff --git a/static/views/devtools/style/page-settings.css b/static/views/devtools/style/page-settings.css new file mode 100644 index 0000000..a4f2407 --- /dev/null +++ b/static/views/devtools/style/page-settings.css @@ -0,0 +1,36 @@ +section[data-page="settings"] { + overflow: auto; + + .warning { + padding: var(--spacing-s); + background-color: color-mix(in srgb, var(--color-log-level-warn) 5%, transparent); + } + + table { + width: 100%; + + tr:first-child { + td, th { + border-top: 1px solid var(--color-border); + } + } + + td, th { + border-bottom: 1px solid var(--color-border); + padding: var(--spacing-xs) var(--spacing-s); + } + + th { + width: 1px; + text-align: left; + } + + td:last-child { + text-align: right; + } + } + + input:invalid { + background-color: var(--color-input-invalid); + } +} \ No newline at end of file diff --git a/static/views/devtools/style/reset.css b/static/views/devtools/style/reset.css new file mode 100644 index 0000000..7e24161 --- /dev/null +++ b/static/views/devtools/style/reset.css @@ -0,0 +1,11 @@ +* { + box-sizing: border-box; +} + +label { + user-select: none; +} + +body, pre { + margin: 0; +} \ No newline at end of file diff --git a/static/views/devtools/style/theme.css b/static/views/devtools/style/theme.css new file mode 100644 index 0000000..9507ba3 --- /dev/null +++ b/static/views/devtools/style/theme.css @@ -0,0 +1,65 @@ +:root { + --spacing-xs: 0.125rem; + --spacing-s: 0.25rem; + --spacing-m: 0.5rem; + --spacing-l: 1rem; + + --color-text-primary: #181818; + --color-text-note: #31313199; + --color-background-primary: #ffffff; + --color-background-secondary: #f3f3f3; + --color-background-tertiary: #eaeaea; + --color-border: #E8E8E8; + + --color-tab-background: var(--color-background-secondary); + --color-tab-background-hover: var(--color-background-secondary); + --color-tab-inactive: transparent; + --color-tab-active: #1155CC; + + --color-button-border: #D3D3D3; + + --color-input-invalid: #FF000050; + --color-input-border: #31313199; + + --color-event-border-active: color-mix(in srgb, var(--color-tab-active) 50%, var(--color-background-primary)); + --color-event-background-active: color-mix(in srgb, var(--color-tab-active) 5%, var(--color-background-primary)); + + --event-highlight-strength: 5%; + --color-event-ack: color-mix(in srgb, teal var(--event-highlight-strength), var(--color-background-primary)); + --color-event-success: color-mix(in srgb, lime var(--event-highlight-strength), var(--color-background-primary)); + --color-event-failure: color-mix(in srgb, red var(--event-highlight-strength), var(--color-background-primary)); + --color-event-warning: color-mix(in srgb, orange var(--event-highlight-strength), var(--color-background-primary)); + + --color-log-timestamp: #666; + --color-log-level-default: black; + --color-log-level-info: blue; + --color-log-level-warn: orange; + --color-log-level-error: red; + --color-log-level-event: yellow; + + --font-family: sans-serif; + --font-size: 0.75rem; + + --font-family-timestamp: monospace; + --font-size-timestamp: 0.625rem; + + --font-family-version: monospace; + --font-size-version: 0.625rem; + --color-version: #666; + + --transition-duration: 0.2s; +} + +@media (prefers-color-scheme: dark) { + :root { + --event-highlight-strength: 10%; + --color-text-primary: #E7E7E7; + --color-text-note: #CECECE99; + --color-background-primary: #334; + --color-background-secondary: #445; + --color-background-tertiary: #556; + --color-border: #171717; + --color-button-border: #2C2C2C; + --color-version: #AAA; + } +} diff --git a/static/views/options.html b/static/views/options.html new file mode 100644 index 0000000..9cbf7ae --- /dev/null +++ b/static/views/options.html @@ -0,0 +1,85 @@ + + + + + + + + + + + +
+
+ + +
+
The extension needs storage permissions to persist the settings.
+
For the "Web eID" tab to appear in the browser's DevTools, you will need to close an existing DevTools and reopen it.
+
+ + + diff --git a/static/views/options.js b/static/views/options.js new file mode 100644 index 0000000..9491eab --- /dev/null +++ b/static/views/options.js @@ -0,0 +1,34 @@ +const ui = { + devtools: document.querySelector("#devtools"), + storageNotAllowed: document.querySelector(".warning.storage-not-allowed"), + openDevToolsAgain: document.querySelector(".warning.open-devtools-again"), + +}; + +(async () => { + if (await browser.permissions.contains({ permissions: ["storage"] })) { + const { devtoolsEnabled } = await browser.storage.local.get(["devtoolsEnabled"]); + + ui.devtools.checked = Boolean(devtoolsEnabled); + } +})(); + +ui.devtools.addEventListener("change", async () => { + const hasStoragePermission = await browser.permissions.request({ + permissions: ["storage"] + }); + + if (!hasStoragePermission) { + ui.storageNotAllowed.style.display = 'block'; + } else { + ui.storageNotAllowed.style.display = 'none'; + + browser.storage.local.set({ devtoolsEnabled: ui.devtools.checked }); + + ui.openDevToolsAgain.style.display = ( + ui.devtools.checked + ? 'block' + : 'none' + ); + } +}); From a8971433296476e749faef7333e752a6de16f38a Mon Sep 17 00:00:00 2001 From: Tanel Metsar Date: Tue, 28 Jan 2025 09:59:20 +0000 Subject: [PATCH 02/17] Update copyright year and add missing copyright comments Signed-off-by: Tanel Metsar --- LICENSE | 2 +- src/background-firefox/consent.ts | 2 +- .../services/NativeAppService.ts | 2 +- .../actions/TokenSigning/errorToResponse.ts | 2 +- .../actions/TokenSigning/getCertificate.ts | 2 +- src/background/actions/TokenSigning/index.ts | 2 +- src/background/actions/TokenSigning/sign.ts | 2 +- src/background/actions/TokenSigning/status.ts | 2 +- .../TokenSigning/threeLetterLanguageCodes.ts | 2 +- src/background/actions/authenticate.ts | 2 +- .../actions/getSigningCertificate.ts | 2 +- src/background/actions/sign.ts | 2 +- src/background/actions/status.ts | 2 +- src/background/background.ts | 2 +- src/background/services/NativeAppService.ts | 2 +- src/config.ts | 2 +- src/content/TokenSigning/injectPageScript.ts | 2 +- src/content/content.ts | 2 +- src/models/Browser.ts | 2 +- src/models/Browser/Devtools.ts | 2 +- src/models/Browser/ExtensionStorage | 2 +- src/models/Browser/Permissions.ts | 2 +- src/models/Browser/Runtime.ts | 2 +- src/models/Browser/Tabs.ts | 2 +- src/models/StatusOptions.ts | 2 +- .../TokenSigning/TokenSigningMessage.ts | 2 +- .../TokenSigning/TokenSigningPromise.ts | 2 +- .../TokenSigning/TokenSigningResponse.ts | 2 +- src/models/TokenSigning/TokenSigningType.ts | 2 +- src/resources/token-signing-page-script.ts | 2 +- src/shared/ByteArray.ts | 2 +- src/shared/HwcryptoPatcher.ts | 22 +++++++++++++++++++ src/shared/Logger.ts | 2 +- src/shared/Mutex.ts | 2 +- src/shared/TokenSigningPageScript.ts | 2 +- src/shared/actionErrorHandler.ts | 2 +- src/shared/configManager.ts | 2 +- src/shared/devToolsBridge.ts | 2 +- src/shared/tokenSigningResponse.ts | 2 +- src/shared/utils/__tests__/version-test.ts | 2 +- src/shared/utils/calculateJsonSize.ts | 2 +- src/shared/utils/checkCompatibility.ts | 2 +- src/shared/utils/semver.ts | 2 +- src/shared/utils/sender.ts | 2 +- static/views/devtools/devtools.js | 22 +++++++++++++++++++ .../views/devtools/panels/devtools-webeid.js | 22 +++++++++++++++++++ static/views/devtools/panels/webeid-events.js | 22 +++++++++++++++++++ static/views/devtools/panels/webeid-log.js | 22 +++++++++++++++++++ .../views/devtools/panels/webeid-settings.js | 22 +++++++++++++++++++ static/views/installed.js | 2 +- static/views/options.js | 22 +++++++++++++++++++ 51 files changed, 198 insertions(+), 44 deletions(-) diff --git a/LICENSE b/LICENSE index 03b6ac6..a7252de 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020-2024 Estonian Information System Authority +Copyright (c) 2020-2025 Estonian Information System Authority Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/background-firefox/consent.ts b/src/background-firefox/consent.ts index ecd09c1..8ea9327 100644 --- a/src/background-firefox/consent.ts +++ b/src/background-firefox/consent.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/background-safari/services/NativeAppService.ts b/src/background-safari/services/NativeAppService.ts index 022dda8..2cbf4dc 100644 --- a/src/background-safari/services/NativeAppService.ts +++ b/src/background-safari/services/NativeAppService.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/background/actions/TokenSigning/errorToResponse.ts b/src/background/actions/TokenSigning/errorToResponse.ts index fdf871b..03c9bb8 100644 --- a/src/background/actions/TokenSigning/errorToResponse.ts +++ b/src/background/actions/TokenSigning/errorToResponse.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/background/actions/TokenSigning/getCertificate.ts b/src/background/actions/TokenSigning/getCertificate.ts index c2173bb..1d04cb3 100644 --- a/src/background/actions/TokenSigning/getCertificate.ts +++ b/src/background/actions/TokenSigning/getCertificate.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/background/actions/TokenSigning/index.ts b/src/background/actions/TokenSigning/index.ts index 7f69020..8c2673b 100644 --- a/src/background/actions/TokenSigning/index.ts +++ b/src/background/actions/TokenSigning/index.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/background/actions/TokenSigning/sign.ts b/src/background/actions/TokenSigning/sign.ts index 7c5c3f8..783ff5a 100644 --- a/src/background/actions/TokenSigning/sign.ts +++ b/src/background/actions/TokenSigning/sign.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/background/actions/TokenSigning/status.ts b/src/background/actions/TokenSigning/status.ts index 212dd2a..4d1990e 100644 --- a/src/background/actions/TokenSigning/status.ts +++ b/src/background/actions/TokenSigning/status.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/background/actions/TokenSigning/threeLetterLanguageCodes.ts b/src/background/actions/TokenSigning/threeLetterLanguageCodes.ts index d3c67fb..ac7e073 100644 --- a/src/background/actions/TokenSigning/threeLetterLanguageCodes.ts +++ b/src/background/actions/TokenSigning/threeLetterLanguageCodes.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2024 Estonian Information System Authority + * Copyright (c) 2022-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/background/actions/authenticate.ts b/src/background/actions/authenticate.ts index 0136761..d57065f 100644 --- a/src/background/actions/authenticate.ts +++ b/src/background/actions/authenticate.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/background/actions/getSigningCertificate.ts b/src/background/actions/getSigningCertificate.ts index 4de4c71..5f8486a 100644 --- a/src/background/actions/getSigningCertificate.ts +++ b/src/background/actions/getSigningCertificate.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/background/actions/sign.ts b/src/background/actions/sign.ts index 8583e6c..e143bd8 100644 --- a/src/background/actions/sign.ts +++ b/src/background/actions/sign.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/background/actions/status.ts b/src/background/actions/status.ts index 5c384b1..389052b 100644 --- a/src/background/actions/status.ts +++ b/src/background/actions/status.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/background/background.ts b/src/background/background.ts index a29007b..965b115 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/background/services/NativeAppService.ts b/src/background/services/NativeAppService.ts index 878d4f7..2941b3b 100644 --- a/src/background/services/NativeAppService.ts +++ b/src/background/services/NativeAppService.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/config.ts b/src/config.ts index 6913459..592bbce 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/content/TokenSigning/injectPageScript.ts b/src/content/TokenSigning/injectPageScript.ts index 5c67698..fc2f30a 100644 --- a/src/content/TokenSigning/injectPageScript.ts +++ b/src/content/TokenSigning/injectPageScript.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/content/content.ts b/src/content/content.ts index 0da8f9a..64c40bf 100644 --- a/src/content/content.ts +++ b/src/content/content.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/models/Browser.ts b/src/models/Browser.ts index 6f7e75c..9f060d5 100644 --- a/src/models/Browser.ts +++ b/src/models/Browser.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/models/Browser/Devtools.ts b/src/models/Browser/Devtools.ts index 4485682..69d996d 100644 --- a/src/models/Browser/Devtools.ts +++ b/src/models/Browser/Devtools.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/models/Browser/ExtensionStorage b/src/models/Browser/ExtensionStorage index 6fae284..969cd28 100644 --- a/src/models/Browser/ExtensionStorage +++ b/src/models/Browser/ExtensionStorage @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/models/Browser/Permissions.ts b/src/models/Browser/Permissions.ts index a670876..f53c7f8 100644 --- a/src/models/Browser/Permissions.ts +++ b/src/models/Browser/Permissions.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/models/Browser/Runtime.ts b/src/models/Browser/Runtime.ts index 1673d96..1cd8ca8 100644 --- a/src/models/Browser/Runtime.ts +++ b/src/models/Browser/Runtime.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/models/Browser/Tabs.ts b/src/models/Browser/Tabs.ts index beab51d..685f632 100644 --- a/src/models/Browser/Tabs.ts +++ b/src/models/Browser/Tabs.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/models/StatusOptions.ts b/src/models/StatusOptions.ts index 0974a69..7350b40 100644 --- a/src/models/StatusOptions.ts +++ b/src/models/StatusOptions.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/models/TokenSigning/TokenSigningMessage.ts b/src/models/TokenSigning/TokenSigningMessage.ts index b25a734..9db08df 100644 --- a/src/models/TokenSigning/TokenSigningMessage.ts +++ b/src/models/TokenSigning/TokenSigningMessage.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/models/TokenSigning/TokenSigningPromise.ts b/src/models/TokenSigning/TokenSigningPromise.ts index 191d4d9..e250144 100644 --- a/src/models/TokenSigning/TokenSigningPromise.ts +++ b/src/models/TokenSigning/TokenSigningPromise.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/models/TokenSigning/TokenSigningResponse.ts b/src/models/TokenSigning/TokenSigningResponse.ts index 6aa6b22..859f058 100644 --- a/src/models/TokenSigning/TokenSigningResponse.ts +++ b/src/models/TokenSigning/TokenSigningResponse.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/models/TokenSigning/TokenSigningType.ts b/src/models/TokenSigning/TokenSigningType.ts index 775d0dc..4c361b0 100644 --- a/src/models/TokenSigning/TokenSigningType.ts +++ b/src/models/TokenSigning/TokenSigningType.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/resources/token-signing-page-script.ts b/src/resources/token-signing-page-script.ts index 66c2cbc..dc09b48 100644 --- a/src/resources/token-signing-page-script.ts +++ b/src/resources/token-signing-page-script.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/shared/ByteArray.ts b/src/shared/ByteArray.ts index c9b137d..b569dd8 100644 --- a/src/shared/ByteArray.ts +++ b/src/shared/ByteArray.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/shared/HwcryptoPatcher.ts b/src/shared/HwcryptoPatcher.ts index 7063241..61cab06 100644 --- a/src/shared/HwcryptoPatcher.ts +++ b/src/shared/HwcryptoPatcher.ts @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2022-2025 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + interface Hwcrypto { [key: string]: any; diff --git a/src/shared/Logger.ts b/src/shared/Logger.ts index 4bf3e9e..f0ca00b 100644 --- a/src/shared/Logger.ts +++ b/src/shared/Logger.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/shared/Mutex.ts b/src/shared/Mutex.ts index 4c4acf6..4d54dcb 100644 --- a/src/shared/Mutex.ts +++ b/src/shared/Mutex.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/shared/TokenSigningPageScript.ts b/src/shared/TokenSigningPageScript.ts index 6744767..66cfb18 100644 --- a/src/shared/TokenSigningPageScript.ts +++ b/src/shared/TokenSigningPageScript.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/shared/actionErrorHandler.ts b/src/shared/actionErrorHandler.ts index 6268d71..82086b1 100644 --- a/src/shared/actionErrorHandler.ts +++ b/src/shared/actionErrorHandler.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/shared/configManager.ts b/src/shared/configManager.ts index bf3e8a3..2e5e10a 100644 --- a/src/shared/configManager.ts +++ b/src/shared/configManager.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/shared/devToolsBridge.ts b/src/shared/devToolsBridge.ts index 419c423..e81784e 100644 --- a/src/shared/devToolsBridge.ts +++ b/src/shared/devToolsBridge.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/shared/tokenSigningResponse.ts b/src/shared/tokenSigningResponse.ts index fa80901..057eb19 100644 --- a/src/shared/tokenSigningResponse.ts +++ b/src/shared/tokenSigningResponse.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/shared/utils/__tests__/version-test.ts b/src/shared/utils/__tests__/version-test.ts index eca8bc5..c40bb5f 100644 --- a/src/shared/utils/__tests__/version-test.ts +++ b/src/shared/utils/__tests__/version-test.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2024 Estonian Information System Authority + * Copyright (c) 2022-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/shared/utils/calculateJsonSize.ts b/src/shared/utils/calculateJsonSize.ts index f1e849f..8ad0425 100644 --- a/src/shared/utils/calculateJsonSize.ts +++ b/src/shared/utils/calculateJsonSize.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/shared/utils/checkCompatibility.ts b/src/shared/utils/checkCompatibility.ts index fc215b8..61ebfd0 100644 --- a/src/shared/utils/checkCompatibility.ts +++ b/src/shared/utils/checkCompatibility.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/shared/utils/semver.ts b/src/shared/utils/semver.ts index d09cc1e..c7683fc 100644 --- a/src/shared/utils/semver.ts +++ b/src/shared/utils/semver.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/shared/utils/sender.ts b/src/shared/utils/sender.ts index 5c54c27..96efc95 100644 --- a/src/shared/utils/sender.ts +++ b/src/shared/utils/sender.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 Estonian Information System Authority + * Copyright (c) 2020-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/static/views/devtools/devtools.js b/static/views/devtools/devtools.js index 64e147b..e1212e9 100644 --- a/static/views/devtools/devtools.js +++ b/static/views/devtools/devtools.js @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2024-2025 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + async function isDevToolsEnabled() { const isDevToolsOptional = Boolean(browser.runtime.getManifest().optional_permissions?.includes("devtools")); diff --git a/static/views/devtools/panels/devtools-webeid.js b/static/views/devtools/panels/devtools-webeid.js index 1f9adce..f5dface 100644 --- a/static/views/devtools/panels/devtools-webeid.js +++ b/static/views/devtools/panels/devtools-webeid.js @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2024-2025 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + import events from "./webeid-events.js"; import log from "./webeid-log.js"; import settings from "./webeid-settings.js"; diff --git a/static/views/devtools/panels/webeid-events.js b/static/views/devtools/panels/webeid-events.js index 37cbc46..b0d2f58 100644 --- a/static/views/devtools/panels/webeid-events.js +++ b/static/views/devtools/panels/webeid-events.js @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2024-2025 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + const ui = { eventTemplate: document.querySelector("#event-template"), eventList: document.querySelector("section[data-page='events'] #event-list"), diff --git a/static/views/devtools/panels/webeid-log.js b/static/views/devtools/panels/webeid-log.js index ada779c..3e49061 100644 --- a/static/views/devtools/panels/webeid-log.js +++ b/static/views/devtools/panels/webeid-log.js @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2024-2025 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + const ui = { logEntryTemplate: document.querySelector("#log-entry-template"), logContainer: document.querySelector("section[data-page='log'] #log-messages"), diff --git a/static/views/devtools/panels/webeid-settings.js b/static/views/devtools/panels/webeid-settings.js index a70484a..bbe3d03 100644 --- a/static/views/devtools/panels/webeid-settings.js +++ b/static/views/devtools/panels/webeid-settings.js @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2024-2025 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + const excludedSettings = [ "NATIVE_APP_NAME", "TOKEN_SIGNING_BACKWARDS_COMPATIBILITY", diff --git a/static/views/installed.js b/static/views/installed.js index 0234335..2a43531 100644 --- a/static/views/installed.js +++ b/static/views/installed.js @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023-2024 Estonian Information System Authority + * Copyright (c) 2023-2025 Estonian Information System Authority * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/static/views/options.js b/static/views/options.js index 9491eab..14d5052 100644 --- a/static/views/options.js +++ b/static/views/options.js @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2024-2025 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + const ui = { devtools: document.querySelector("#devtools"), storageNotAllowed: document.querySelector(".warning.storage-not-allowed"), From 74fef3b73c2ac1b6a9c5ee5a91de318d3d2b95ca Mon Sep 17 00:00:00 2001 From: Tanel Metsar Date: Tue, 28 Jan 2025 10:30:38 +0000 Subject: [PATCH 03/17] Update README.md with DevTools instructions Signed-off-by: Tanel Metsar --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index 688ed10..25c5b44 100644 --- a/README.md +++ b/README.md @@ -57,3 +57,28 @@ The Web eID extension for Safari is built as a [Safari web extension](https://de Make sure the `NATIVE_APP_NAME` value in `src/config.ts` matches the one in the Web-eID native application manifest file. +### Developer tools + +The Web eID DevTools tab can be useful while integrating Web eID on a website. + +#### Features +- Event history between the website, extension and native application. +- Extension's internal log messages. +- Option to override extension settings, without needing to compile the extension yourself. +- Option to allow the http://localhost origin when authenticating and signing + +#### Enable or disable the Web eID developer tools tab + +**Firefox** +1. Open [Menu -> Settings -> Extensions and themes] + or navigate to the address `about:addons` +2. Open the Web eID extension details +3. Open the "Permissions" tab +4. Toggle "Extend developer tools to access your data in open tabs" + +**Chrome** +1. Open [Settings -> Extensions] + or navigate to the address `chrome://extensions/` +2. Open the Web eID extension details +3. Open "Extension options" +4. Toggle "Enable developer tools" From 0336e568e972945dd49a41984efb0483b30aac10 Mon Sep 17 00:00:00 2001 From: Tanel Metsar Date: Thu, 30 Jan 2025 09:31:10 +0200 Subject: [PATCH 04/17] Fix DevTools panel disconnecting in Chrome after 30 seconds of inactivity WE2-967 Includes minor cleanup for DevTools related files Signed-off-by: Tanel Metsar --- .../devtools/panels/devtools-webeid.html | 4 +++- .../views/devtools/panels/devtools-webeid.js | 20 +++++++++++++++++-- static/views/devtools/panels/webeid-log.js | 4 ---- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/static/views/devtools/panels/devtools-webeid.html b/static/views/devtools/panels/devtools-webeid.html index 06c7562..dbb970a 100644 --- a/static/views/devtools/panels/devtools-webeid.html +++ b/static/views/devtools/panels/devtools-webeid.html @@ -22,11 +22,13 @@
+

     
+
@@ -44,12 +46,12 @@
+
The settings you change here apply to all pages and persist even after closing DevTools.
-
diff --git a/static/views/devtools/panels/devtools-webeid.js b/static/views/devtools/panels/devtools-webeid.js index f5dface..f5fad7b 100644 --- a/static/views/devtools/panels/devtools-webeid.js +++ b/static/views/devtools/panels/devtools-webeid.js @@ -24,11 +24,27 @@ import events from "./webeid-events.js"; import log from "./webeid-log.js"; import settings from "./webeid-settings.js"; - const backgroundConnection = chrome.runtime.connect({ name: "webeid-devtools", }); +const keepAliveInterval = setInterval(() => { + backgroundConnection.postMessage({ devtools: "keep-alive" }); +}, 20000); + +backgroundConnection.onDisconnect.addListener(() => { + clearInterval(keepAliveInterval); + + log.append({ + source: "devtools-webeid.js", + type: "error", + time: new Date().toISOString().match(/T((.)*)Z/)[1], + message: [ + "Web eID DevTools panel disconnected from the extension. Please reopen the browser DevTools to continue." + ], + }); +}); + backgroundConnection.onMessage.addListener((message) => { if (!message.tabId || message.tabId === chrome.devtools.inspectedWindow.tabId) { if (message.devtools === "log") { @@ -58,4 +74,4 @@ backgroundConnection.onMessage.addListener((message) => { document.querySelector('header .version').textContent = defaultConfig.VERSION; } } -}); +}); \ No newline at end of file diff --git a/static/views/devtools/panels/webeid-log.js b/static/views/devtools/panels/webeid-log.js index 3e49061..9ddee91 100644 --- a/static/views/devtools/panels/webeid-log.js +++ b/static/views/devtools/panels/webeid-log.js @@ -47,12 +47,8 @@ function createLogEntryElement(type, time, message, source) { function stringifyError(error) { const { - fileName, - lineNumber, - columnNumber, message, name, - code, stack, } = error; From 531cb33586d369a319d07243faa98b129097daad Mon Sep 17 00:00:00 2001 From: Sven Mitt Date: Mon, 9 Jun 2025 08:01:53 +0200 Subject: [PATCH 05/17] standardize loopback address validation between frontend and backend WE2-967 Signed-off-by: Sven Mitt --- src/background/services/NativeAppService.ts | 3 +- src/shared/utils/isLoopbackAddress.ts | 33 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/shared/utils/isLoopbackAddress.ts diff --git a/src/background/services/NativeAppService.ts b/src/background/services/NativeAppService.ts index 2941b3b..3f4461b 100644 --- a/src/background/services/NativeAppService.ts +++ b/src/background/services/NativeAppService.ts @@ -30,6 +30,7 @@ import calculateJsonSize from "../../shared/utils/calculateJsonSize"; import { config } from "../../shared/configManager"; import Logger from "../../shared/Logger"; +import isLoopbackAddress from "../../shared/utils/isLoopbackAddress"; const logger = new Logger("NativeAppService.ts"); @@ -133,7 +134,7 @@ export default class NativeAppService { if (config.ALLOW_HTTP_LOCALHOST && message.arguments?.origin) { const url = new URL(message.arguments.origin); - if (url.protocol === "http:" && url.hostname === "localhost") { + if (url.protocol === "http:" && isLoopbackAddress(url.hostname)) { url.protocol = "https:"; message.arguments.origin = url.origin; diff --git a/src/shared/utils/isLoopbackAddress.ts b/src/shared/utils/isLoopbackAddress.ts new file mode 100644 index 0000000..f0dc7dc --- /dev/null +++ b/src/shared/utils/isLoopbackAddress.ts @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2020-2025 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** + * + * @param host hostname or ip address + * + * @returns true if is loopback address + */ +export default function isLoopbackAddress(host: string): boolean { + return host === "localhost" + || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host) + || /^\[?::1]?$/.test(host); +} From 563390e3820d37ad20558c4a04660fb7d80f0923 Mon Sep 17 00:00:00 2001 From: Sven Mitt Date: Mon, 9 Jun 2025 08:15:26 +0200 Subject: [PATCH 06/17] Persist and load configuration from localstorage as it is done with opening Web-eID tab WE2-967 Signed-off-by: Sven Mitt --- src/shared/configManager.ts | 11 ++++++- src/shared/devToolsBridge.ts | 56 +++++++++++++++++++++++++++++------- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/src/shared/configManager.ts b/src/shared/configManager.ts index 2e5e10a..680d70e 100644 --- a/src/shared/configManager.ts +++ b/src/shared/configManager.ts @@ -32,12 +32,21 @@ function setConfigOverride(key: K, value: if (value === null) { value = defaultConfig[key]; } - + config[key] = value; } +function setConfigFromStorage(key: K, value: any): void { + if (value == null || typeof value !== typeof defaultConfig[key]) { + config[key] = defaultConfig[key]; + } else { + config[key] = value; + } +} + export { config, defaultConfig, setConfigOverride, + setConfigFromStorage, }; diff --git a/src/shared/devToolsBridge.ts b/src/shared/devToolsBridge.ts index e81784e..cca1ef3 100644 --- a/src/shared/devToolsBridge.ts +++ b/src/shared/devToolsBridge.ts @@ -20,12 +20,14 @@ * SOFTWARE. */ -import { config, defaultConfig, setConfigOverride } from "./configManager"; +import {config, defaultConfig, setConfigFromStorage, setConfigOverride} from "./configManager"; import { Port } from "../models/Browser/Runtime"; +import Logger from "./Logger"; class DevToolsBridge extends EventTarget { devToolPorts: Array = []; + logger = new Logger("devToolsBridge.ts"); constructor() { super(); @@ -38,6 +40,7 @@ class DevToolsBridge extends EventTarget { port.onMessage.addListener((message) => { if (message.devtools === "setting-set") { setConfigOverride(message.key, message.value); + this.saveToStorage(message.key, message.value); this.send({ devtools: "settings", config, defaultConfig }, { ignore: port }); } @@ -47,7 +50,8 @@ class DevToolsBridge extends EventTarget { this.devToolPorts = this.devToolPorts.filter((connectedPort) => connectedPort !== port); }); - port.postMessage({ devtools: "settings", config, defaultConfig }); + this.loadConfigFromStorage() + .then(() => port.postMessage({ devtools: "settings", config, defaultConfig })); }); } @@ -60,22 +64,54 @@ class DevToolsBridge extends EventTarget { async isDevToolsEnabled() { const isDevToolsOptional = Boolean(browser.runtime.getManifest().optional_permissions?.includes("devtools")); const hasDevToolsPermission = await browser.permissions.contains({ permissions: ["devtools"] }); - + if (isDevToolsOptional && hasDevToolsPermission) { return true; } - - const isStorageOptional = Boolean(browser.runtime.getManifest().optional_permissions?.includes("storage")); - const hasStoragePermission = await browser.permissions.contains({ permissions: ["storage"] }); - - if (isStorageOptional && hasStoragePermission) { + + const isStorageEnabled = await this.isStorageEnabled(); + if (isStorageEnabled) { const { devtoolsEnabled } = await browser.storage.local.get(["devtoolsEnabled"]); - + return Boolean(devtoolsEnabled); } - + return false; } + + private async loadConfigFromStorage(): Promise { + const isStorageEnabled = await this.isStorageEnabled(); + if (isStorageEnabled) { + try { + const keys = Object.keys(config) as Array; + const results = await browser.storage.local.get(keys); + + for (const key of keys) { + setConfigFromStorage(key, results[key]); + } + } catch (error) { + await this.logger.error('Failed to load configuration from storage:', error); + } + } + } + + private async saveToStorage(key: K, value: typeof defaultConfig[K]) { + const isStorageEnabled = await this.isStorageEnabled(); + if (isStorageEnabled) { + if (value === null) { + browser.storage.local.remove([key]); + } else { + browser.storage.local.set({[key]: value}); + } + } + } + + private async isStorageEnabled() { + const isStorageOptional = Boolean(browser.runtime.getManifest().optional_permissions?.includes("storage")); + const hasStoragePermission = await browser.permissions.contains({permissions: ["storage"]}); + return isStorageOptional && hasStoragePermission; + } + } export default new DevToolsBridge(); From 9d21c8a13f196efb67d4de4b9cb1d1ae697e977d Mon Sep 17 00:00:00 2001 From: Sven Mitt Date: Mon, 9 Jun 2025 16:00:54 +0200 Subject: [PATCH 07/17] Add warning to log when http is used. Remove Logger as it causes error during loading WE2-967 Signed-off-by: Sven Mitt --- src/background/services/NativeAppService.ts | 1 + src/shared/devToolsBridge.ts | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/background/services/NativeAppService.ts b/src/background/services/NativeAppService.ts index 3f4461b..b3d2094 100644 --- a/src/background/services/NativeAppService.ts +++ b/src/background/services/NativeAppService.ts @@ -138,6 +138,7 @@ export default class NativeAppService { url.protocol = "https:"; message.arguments.origin = url.origin; + logger.warn("Setting ALLOW_HTTP_LOCALHOST enabled, replaced origin with " + message.arguments.origin); } } diff --git a/src/shared/devToolsBridge.ts b/src/shared/devToolsBridge.ts index cca1ef3..b2503d1 100644 --- a/src/shared/devToolsBridge.ts +++ b/src/shared/devToolsBridge.ts @@ -20,14 +20,12 @@ * SOFTWARE. */ -import {config, defaultConfig, setConfigFromStorage, setConfigOverride} from "./configManager"; +import { config, defaultConfig, setConfigFromStorage, setConfigOverride } from "./configManager"; import { Port } from "../models/Browser/Runtime"; -import Logger from "./Logger"; class DevToolsBridge extends EventTarget { devToolPorts: Array = []; - logger = new Logger("devToolsBridge.ts"); constructor() { super(); @@ -90,7 +88,7 @@ class DevToolsBridge extends EventTarget { setConfigFromStorage(key, results[key]); } } catch (error) { - await this.logger.error('Failed to load configuration from storage:', error); + console.error('Failed to load configuration from storage:', error); } } } From 85ff99c261511cffe1d2295de722f3999b4fec51 Mon Sep 17 00:00:00 2001 From: Sven Mitt Date: Mon, 16 Jun 2025 18:08:33 +0200 Subject: [PATCH 08/17] Add devtools scripts and views to Safari build WE2-967 Signed-off-by: Sven Mitt --- scripts/build.mjs | 10 ++++++++-- static/safari/manifest.json | 6 +++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/scripts/build.mjs b/scripts/build.mjs index b964304..e93f3be 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -115,13 +115,19 @@ const targets = { ); await cp("./static/_locales", "./dist/firefox/_locales"); await cp("./static/views", "./dist/firefox/views"); - await cp("./node_modules/webextension-polyfill/dist", "./dist/firefox/views"); + await cp("./node_modules/webextension-polyfill/dist/browser-polyfill.min.js", "./dist/firefox/views/browser-polyfill.min.js"); rem( "Copying static pages to Chrome dist directory" ); await cp("./static/views", "./dist/chrome/views"); - await cp("./node_modules/webextension-polyfill/dist", "./dist/chrome/views"); + await cp("./node_modules/webextension-polyfill/dist/browser-polyfill.min.js", "./dist/chrome/views/browser-polyfill.min.js"); + + rem( + "Copying static pages to Safari dist directory" + ); + await cp("./static/views", "./dist/safari/views"); + await cp("./node_modules/webextension-polyfill/dist/browser-polyfill.min.js", "./dist/safari/views/browser-polyfill.min.js"); rem( "Setting up the Firefox manifest" diff --git a/static/safari/manifest.json b/static/safari/manifest.json index ebb74ba..ae09fc1 100644 --- a/static/safari/manifest.json +++ b/static/safari/manifest.json @@ -22,12 +22,16 @@ "background.js" ] }, + "options_ui": { + "page": "views/options.html" + }, "devtools_page": "views/devtools/devtools.html", "permissions": [ "*://*/*", "nativeMessaging" ], "optional_permissions": [ - "devtools" + "devtools", + "storage" ] } From 035187df6220ee84bbf393d80ce886da71dbad4f Mon Sep 17 00:00:00 2001 From: Sven Mitt Date: Wed, 17 Sep 2025 10:39:50 +0300 Subject: [PATCH 09/17] Refactor persistence of config to localstorage WE2-967 Signed-off-by: Sven Mitt --- src/config.ts | 2 +- src/shared/__tests__/configManager-test.ts | 154 ++++++++++++++++++ src/shared/configManager.ts | 54 +++++- src/shared/devToolsBridge.ts | 49 +----- src/shared/utils/isBrowserStorageEnabled.ts | 32 ++++ src/shared/utils/isLoopbackAddress.ts | 18 +- .../views/devtools/panels/webeid-settings.js | 9 +- 7 files changed, 262 insertions(+), 56 deletions(-) create mode 100644 src/shared/__tests__/configManager-test.ts create mode 100644 src/shared/utils/isBrowserStorageEnabled.ts diff --git a/src/config.ts b/src/config.ts index 592bbce..b6ae9aa 100644 --- a/src/config.ts +++ b/src/config.ts @@ -56,5 +56,5 @@ export default Object.freeze({ * * When this option is enabled, the extension reports localhost URLs to the native application with HTTPS. */ - ALLOW_HTTP_LOCALHOST: false, + ALLOW_HTTP_LOCALHOST: false as boolean, }); diff --git a/src/shared/__tests__/configManager-test.ts b/src/shared/__tests__/configManager-test.ts new file mode 100644 index 0000000..bd484b9 --- /dev/null +++ b/src/shared/__tests__/configManager-test.ts @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2022-2025 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import {config, setConfigOverride} from "../configManager"; + +const mockStorageLocal = { + set: jest.fn(), + remove: jest.fn() +}; + +const mockBrowser = { + storage: { + local: mockStorageLocal + } +}; + +(global as any).browser = mockBrowser; + +const mockIsBrowserStorageEnabled = jest.fn(); + +jest.mock("../utils/isBrowserStorageEnabled", () => ({ + __esModule: true, + default: () => mockIsBrowserStorageEnabled() +})); + +describe("setConfigOverride", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("when storage is enabled", () => { + beforeEach(() => { + config.ALLOW_HTTP_LOCALHOST = false; + mockIsBrowserStorageEnabled.mockResolvedValue(true); + }); + + it("should set value in config and storage when value is not null", async () => { + expect(config.ALLOW_HTTP_LOCALHOST).toEqual(false); + + await setConfigOverride("ALLOW_HTTP_LOCALHOST", true); + + expect(config.ALLOW_HTTP_LOCALHOST).toEqual(true); + expect(mockStorageLocal.set).toHaveBeenCalledWith({ALLOW_HTTP_LOCALHOST: true}); + expect(mockStorageLocal.remove).not.toHaveBeenCalled(); + }); + + it("should set config value to default and remove key from storage when value is null", async () => { + expect(config.ALLOW_HTTP_LOCALHOST).toEqual(false); + + await setConfigOverride("ALLOW_HTTP_LOCALHOST", null); + + expect(config.ALLOW_HTTP_LOCALHOST).toEqual(false); + expect(mockStorageLocal.remove).toHaveBeenCalledWith(["ALLOW_HTTP_LOCALHOST"]); + expect(mockStorageLocal.set).not.toHaveBeenCalled(); + }); + + it("should set config value to default and remove key from storage when value is equal to default value", async () => { + expect(config.ALLOW_HTTP_LOCALHOST).toEqual(false); + + await setConfigOverride("ALLOW_HTTP_LOCALHOST", false); + + expect(config.ALLOW_HTTP_LOCALHOST).toEqual(false); + expect(mockStorageLocal.remove).toHaveBeenCalledWith(["ALLOW_HTTP_LOCALHOST"]); + expect(mockStorageLocal.set).not.toHaveBeenCalled(); + }); + + it("should not set non overridable config value", async () => { + expect(config.NATIVE_APP_NAME).toEqual("eu.webeid"); + + await setConfigOverride("NATIVE_APP_NAME", null); + + expect(config.NATIVE_APP_NAME).toEqual("eu.webeid"); + expect(mockStorageLocal.remove).not.toHaveBeenCalledWith(["NATIVE_APP_NAME"]); + expect(mockStorageLocal.set).not.toHaveBeenCalled(); + }); + + }); + + describe("when storage is disabled", () => { + beforeEach(() => { + config.ALLOW_HTTP_LOCALHOST = false; + mockIsBrowserStorageEnabled.mockResolvedValue(false); + }); + + it("should set value in config and not call storage methods when storage is disabled", async () => { + expect(config.ALLOW_HTTP_LOCALHOST).toEqual(false); + + await setConfigOverride("ALLOW_HTTP_LOCALHOST", true); + + expect(config.ALLOW_HTTP_LOCALHOST).toEqual(true); + expect(mockStorageLocal.set).not.toHaveBeenCalled(); + expect(mockStorageLocal.remove).not.toHaveBeenCalled(); + }); + + it("should set config value to default and not call storage methods when storage is disabled", async () => { + expect(config.ALLOW_HTTP_LOCALHOST).toEqual(false); + + await setConfigOverride("ALLOW_HTTP_LOCALHOST", null); + + expect(config.ALLOW_HTTP_LOCALHOST).toEqual(false); + expect(mockStorageLocal.set).not.toHaveBeenCalled(); + expect(mockStorageLocal.remove).not.toHaveBeenCalled(); + }); + }); + + describe("error handling", () => { + beforeEach(() => { + mockIsBrowserStorageEnabled.mockResolvedValue(true); + }); + + it("should handle storage.set errors", async () => { + const error = new Error("Storage set failed"); + mockStorageLocal.set.mockRejectedValue(error); + + await expect(setConfigOverride("ALLOW_HTTP_LOCALHOST", true)) + .rejects.toThrow("Storage set failed"); + }); + + it("should handle storage.remove errors", async () => { + const error = new Error("Storage remove failed"); + mockStorageLocal.remove.mockRejectedValue(error); + + await expect(setConfigOverride("ALLOW_HTTP_LOCALHOST", null)) + .rejects.toThrow("Storage remove failed"); + }); + + it("should handle isBrowserStorageEnabled errors", async () => { + const error = new Error("Storage check failed"); + mockIsBrowserStorageEnabled.mockRejectedValue(error); + + await expect(setConfigOverride("ALLOW_HTTP_LOCALHOST", true)) + .rejects.toThrow("Storage check failed"); + }); + }); +}); diff --git a/src/shared/configManager.ts b/src/shared/configManager.ts index 680d70e..cb184a3 100644 --- a/src/shared/configManager.ts +++ b/src/shared/configManager.ts @@ -21,6 +21,7 @@ */ import defaultConfig from "../config"; +import isBrowserStorageEnabled from "./utils/isBrowserStorageEnabled"; type Config = { -readonly [key in keyof typeof defaultConfig]: typeof defaultConfig[key]; @@ -28,25 +29,64 @@ type Config = { const config = JSON.parse(JSON.stringify(defaultConfig)) as Config; -function setConfigOverride(key: K, value: typeof defaultConfig[K]) { - if (value === null) { - value = defaultConfig[key]; +const overrideableConfigKeys: Array = [ + "NATIVE_MESSAGE_MAX_BYTES", + "NATIVE_GRACEFUL_DISCONNECT_TIMEOUT", + "TOKEN_SIGNING_USER_INTERACTION_TIMEOUT", + "ALLOW_HTTP_LOCALHOST" +]; + +async function setConfigOverride(key: K, value: typeof defaultConfig[K] | null) { + if (!overrideableConfigKeys.includes(key)) { + return; } + setConfigValueOrResetToDefaultOnNull(key, value); + await saveToStorageOrRemoveOnNull(key, value); +} - config[key] = value; +async function loadConfigFromStorage() { + const isStorageEnabled = await isBrowserStorageEnabled(); + if (isStorageEnabled) { + try { + const results = await browser.storage.local.get(overrideableConfigKeys); + + for (const key of overrideableConfigKeys) { + if (isValidConfigValue(key, results[key])) { + setConfigValueOrResetToDefaultOnNull(key, results[key]); + } + } + } catch (error) { + console.error("Failed to load configuration from storage:", error); + } + } } -function setConfigFromStorage(key: K, value: any): void { - if (value == null || typeof value !== typeof defaultConfig[key]) { +function isValidConfigValue(key: K, value: unknown): value is typeof defaultConfig[K] { + return typeof value === typeof defaultConfig[key]; +} + +function setConfigValueOrResetToDefaultOnNull(key: K, value: typeof defaultConfig[K] | null) { + if (value === null) { config[key] = defaultConfig[key]; } else { config[key] = value; } } +async function saveToStorageOrRemoveOnNull(key: K, value: typeof defaultConfig[K] | null) { + const isStorageEnabled = await isBrowserStorageEnabled(); + if (isStorageEnabled) { + if (value === null || value === defaultConfig[key]) { + await browser.storage.local.remove([key]); + } else { + await browser.storage.local.set({ [key]: value }); + } + } +} + export { config, defaultConfig, + loadConfigFromStorage, setConfigOverride, - setConfigFromStorage, }; diff --git a/src/shared/devToolsBridge.ts b/src/shared/devToolsBridge.ts index b2503d1..d200aa6 100644 --- a/src/shared/devToolsBridge.ts +++ b/src/shared/devToolsBridge.ts @@ -20,8 +20,9 @@ * SOFTWARE. */ -import { config, defaultConfig, setConfigFromStorage, setConfigOverride } from "./configManager"; +import { config, defaultConfig, loadConfigFromStorage, setConfigOverride } from "./configManager"; import { Port } from "../models/Browser/Runtime"; +import isBrowserStorageEnabled from "./utils/isBrowserStorageEnabled"; class DevToolsBridge extends EventTarget { @@ -30,15 +31,14 @@ class DevToolsBridge extends EventTarget { constructor() { super(); - browser.runtime.onConnect.addListener((port: Port) => { + browser.runtime.onConnect.addListener(async (port: Port) => { if (port.name === "webeid-devtools") { this.devToolPorts.push(port); } - port.onMessage.addListener((message) => { + port.onMessage.addListener(async (message) => { if (message.devtools === "setting-set") { - setConfigOverride(message.key, message.value); - this.saveToStorage(message.key, message.value); + await setConfigOverride(message.key, message.value); this.send({ devtools: "settings", config, defaultConfig }, { ignore: port }); } @@ -48,8 +48,8 @@ class DevToolsBridge extends EventTarget { this.devToolPorts = this.devToolPorts.filter((connectedPort) => connectedPort !== port); }); - this.loadConfigFromStorage() - .then(() => port.postMessage({ devtools: "settings", config, defaultConfig })); + await loadConfigFromStorage(); + port.postMessage({ devtools: "settings", config, defaultConfig }); }); } @@ -67,7 +67,7 @@ class DevToolsBridge extends EventTarget { return true; } - const isStorageEnabled = await this.isStorageEnabled(); + const isStorageEnabled = await isBrowserStorageEnabled(); if (isStorageEnabled) { const { devtoolsEnabled } = await browser.storage.local.get(["devtoolsEnabled"]); @@ -77,39 +77,6 @@ class DevToolsBridge extends EventTarget { return false; } - private async loadConfigFromStorage(): Promise { - const isStorageEnabled = await this.isStorageEnabled(); - if (isStorageEnabled) { - try { - const keys = Object.keys(config) as Array; - const results = await browser.storage.local.get(keys); - - for (const key of keys) { - setConfigFromStorage(key, results[key]); - } - } catch (error) { - console.error('Failed to load configuration from storage:', error); - } - } - } - - private async saveToStorage(key: K, value: typeof defaultConfig[K]) { - const isStorageEnabled = await this.isStorageEnabled(); - if (isStorageEnabled) { - if (value === null) { - browser.storage.local.remove([key]); - } else { - browser.storage.local.set({[key]: value}); - } - } - } - - private async isStorageEnabled() { - const isStorageOptional = Boolean(browser.runtime.getManifest().optional_permissions?.includes("storage")); - const hasStoragePermission = await browser.permissions.contains({permissions: ["storage"]}); - return isStorageOptional && hasStoragePermission; - } - } export default new DevToolsBridge(); diff --git a/src/shared/utils/isBrowserStorageEnabled.ts b/src/shared/utils/isBrowserStorageEnabled.ts new file mode 100644 index 0000000..a58f2b2 --- /dev/null +++ b/src/shared/utils/isBrowserStorageEnabled.ts @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020-2025 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** + * Function to check if saving to browser storage is allowed + * + * @returns true if manifest optional_permissions includes storage and storage permission is given by user + */ +export default async function isBrowserStorageEnabled() { + const isStorageOptional = Boolean(browser.runtime.getManifest().optional_permissions?.includes("storage")); + const hasStoragePermission = await browser.permissions.contains({ permissions: ["storage"] }); + return isStorageOptional && hasStoragePermission; +} diff --git a/src/shared/utils/isLoopbackAddress.ts b/src/shared/utils/isLoopbackAddress.ts index f0dc7dc..c5a7657 100644 --- a/src/shared/utils/isLoopbackAddress.ts +++ b/src/shared/utils/isLoopbackAddress.ts @@ -26,8 +26,20 @@ * * @returns true if is loopback address */ + +const ipv4LoopbackAddressPattern = /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; + +/** + * Checks whether host resolves to loopback address + * + * @param host One of IP-literal / IPv4address / reg-name aka hostname + * @returns `true` if host is loopback address + */ export default function isLoopbackAddress(host: string): boolean { - return host === "localhost" - || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host) - || /^\[?::1]?$/.test(host); + return ( + host === "localhost" || + ipv4LoopbackAddressPattern.test(host) || + host === "[::1]" || + host === "[0000:0000:0000:0000:0000:0000:0000:0001]" + ); } diff --git a/static/views/devtools/panels/webeid-settings.js b/static/views/devtools/panels/webeid-settings.js index bbe3d03..e773245 100644 --- a/static/views/devtools/panels/webeid-settings.js +++ b/static/views/devtools/panels/webeid-settings.js @@ -22,7 +22,8 @@ const excludedSettings = [ "NATIVE_APP_NAME", - "TOKEN_SIGNING_BACKWARDS_COMPATIBILITY", + "VERSION", + "TOKEN_SIGNING_BACKWARDS_COMPATIBILITY" ]; const ui = { @@ -33,7 +34,7 @@ const ui = { export default { render(config, defaultConfig, backgroundConnection) { Object.assign(this, { config, defaultConfig, backgroundConnection }); - + const configRows = ( Object .entries(config) @@ -104,13 +105,13 @@ export default { input.value = value; break; } - + case "boolean": { input.type = "checkbox"; input.checked = value; break; } - + default: input.type = "text"; input.value = value; From c96f5b9f3c2bfe567bcb5c991b4b8225e56c50f7 Mon Sep 17 00:00:00 2001 From: Sven Mitt Date: Mon, 22 Sep 2025 12:04:35 +0300 Subject: [PATCH 10/17] Replace exclude filter with include WE2-967 Signed-off-by: Sven Mitt --- static/views/devtools/panels/webeid-settings.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/static/views/devtools/panels/webeid-settings.js b/static/views/devtools/panels/webeid-settings.js index e773245..0149eb7 100644 --- a/static/views/devtools/panels/webeid-settings.js +++ b/static/views/devtools/panels/webeid-settings.js @@ -20,10 +20,11 @@ * SOFTWARE. */ -const excludedSettings = [ - "NATIVE_APP_NAME", - "VERSION", - "TOKEN_SIGNING_BACKWARDS_COMPATIBILITY" +const includedSettings = [ + "NATIVE_MESSAGE_MAX_BYTES", + "NATIVE_GRACEFUL_DISCONNECT_TIMEOUT", + "TOKEN_SIGNING_USER_INTERACTION_TIMEOUT", + "ALLOW_HTTP_LOCALHOST" ]; const ui = { @@ -46,7 +47,7 @@ export default { }, isVisible(settingKey) { - return !excludedSettings.includes(settingKey); + return includedSettings.includes(settingKey); }, createSettingRow(key, value) { From 1120e0a07d595b2ad9aef91f6425738cf92b8353 Mon Sep 17 00:00:00 2001 From: Sven Mitt Date: Mon, 22 Sep 2025 14:00:28 +0300 Subject: [PATCH 11/17] fix: Improve Reset button visibility when disabled WE2-967 Signed-off-by: Sven Mitt Co-authored-by: Sten Anderson --- static/views/devtools/style/main.css | 39 ++++++++++++++++----------- static/views/devtools/style/theme.css | 20 +++++++------- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/static/views/devtools/style/main.css b/static/views/devtools/style/main.css index e8220e2..b8517c0 100644 --- a/static/views/devtools/style/main.css +++ b/static/views/devtools/style/main.css @@ -26,13 +26,17 @@ input[type="button"] { color: var(--color-text-primary); background-color: var(--color-background-primary); - &:hover { + &:hover:not(:disabled) { background-color: var(--color-background-secondary); } - &:active { + &:active:not(:disabled) { background-color: var(--color-background-tertiary); } + + &:disabled { + color: var(--color-button-border); + } } input[type="text"], @@ -58,24 +62,24 @@ header { nav { font-size: var(--font-size); - + flex: 0 0 auto; display: flex; - + input { display: none; } - + label { line-height: 2rem; padding: 0 var(--spacing-m); - + background-color: var(--color-background-secondary); border-bottom: 0.125rem solid var(--color-tab-inactive); transition: border-bottom-color var(--transition-duration) ease; } - - input:checked+label { + + input:checked + label { border-bottom: 2px solid var(--color-tab-active); } } @@ -90,10 +94,15 @@ header { } } - -body:not(:has([data-nav="events"]:checked)) [data-page="events"] { display: none; } -body:not(:has([data-nav="log"]:checked)) [data-page="log"] { display: none; } -body:not(:has([data-nav="settings"]:checked)) [data-page="settings"] { display: none; } +body:not(:has([data-nav="events"]:checked)) [data-page="events"] { + display: none; +} +body:not(:has([data-nav="log"]:checked)) [data-page="log"] { + display: none; +} +body:not(:has([data-nav="settings"]:checked)) [data-page="settings"] { + display: none; +} main { position: relative; @@ -104,15 +113,15 @@ main { section { height: 100%; - + border: 1px solid var(--color-border); } } .toolbar { - display:flex; + display: flex; padding: var(--spacing-s) var(--spacing-m); - + border: 1px solid var(--color-border); background-color: var(--color-background-tertiary); diff --git a/static/views/devtools/style/theme.css b/static/views/devtools/style/theme.css index 9507ba3..7186311 100644 --- a/static/views/devtools/style/theme.css +++ b/static/views/devtools/style/theme.css @@ -9,16 +9,16 @@ --color-background-primary: #ffffff; --color-background-secondary: #f3f3f3; --color-background-tertiary: #eaeaea; - --color-border: #E8E8E8; - + --color-border: #e8e8e8; + --color-tab-background: var(--color-background-secondary); --color-tab-background-hover: var(--color-background-secondary); --color-tab-inactive: transparent; - --color-tab-active: #1155CC; - - --color-button-border: #D3D3D3; + --color-tab-active: #1155cc; + + --color-button-border: #d3d3d3; - --color-input-invalid: #FF000050; + --color-input-invalid: #ff000050; --color-input-border: #31313199; --color-event-border-active: color-mix(in srgb, var(--color-tab-active) 50%, var(--color-background-primary)); @@ -53,13 +53,13 @@ @media (prefers-color-scheme: dark) { :root { --event-highlight-strength: 10%; - --color-text-primary: #E7E7E7; - --color-text-note: #CECECE99; + --color-text-primary: #e7e7e7; + --color-text-note: #cecece99; --color-background-primary: #334; --color-background-secondary: #445; --color-background-tertiary: #556; --color-border: #171717; - --color-button-border: #2C2C2C; - --color-version: #AAA; + --color-button-border: #5a5a5a; + --color-version: #aaa; } } From ae867ae40265d14282e1f3de4ce5d4f95f0f6646 Mon Sep 17 00:00:00 2001 From: Sven Mitt Date: Thu, 2 Oct 2025 07:57:13 +0300 Subject: [PATCH 12/17] fix: Refactor constructor, remove unused code WE2-967 Signed-off-by: Sven Mitt --- src/shared/configManager.ts | 6 ++--- src/shared/devToolsBridge.ts | 46 +++++++++++++----------------------- 2 files changed, 19 insertions(+), 33 deletions(-) diff --git a/src/shared/configManager.ts b/src/shared/configManager.ts index cb184a3..e32d90e 100644 --- a/src/shared/configManager.ts +++ b/src/shared/configManager.ts @@ -48,11 +48,11 @@ async function loadConfigFromStorage() { const isStorageEnabled = await isBrowserStorageEnabled(); if (isStorageEnabled) { try { - const results = await browser.storage.local.get(overrideableConfigKeys); + const values = await browser.storage.local.get(overrideableConfigKeys); for (const key of overrideableConfigKeys) { - if (isValidConfigValue(key, results[key])) { - setConfigValueOrResetToDefaultOnNull(key, results[key]); + if (isValidConfigValue(key, values[key])) { + setConfigValueOrResetToDefaultOnNull(key, values[key]); } } } catch (error) { diff --git a/src/shared/devToolsBridge.ts b/src/shared/devToolsBridge.ts index d200aa6..d913e35 100644 --- a/src/shared/devToolsBridge.ts +++ b/src/shared/devToolsBridge.ts @@ -32,24 +32,7 @@ class DevToolsBridge extends EventTarget { super(); browser.runtime.onConnect.addListener(async (port: Port) => { - if (port.name === "webeid-devtools") { - this.devToolPorts.push(port); - } - - port.onMessage.addListener(async (message) => { - if (message.devtools === "setting-set") { - await setConfigOverride(message.key, message.value); - - this.send({ devtools: "settings", config, defaultConfig }, { ignore: port }); - } - }); - - port.onDisconnect.addListener(() => { - this.devToolPorts = this.devToolPorts.filter((connectedPort) => connectedPort !== port); - }); - - await loadConfigFromStorage(); - port.postMessage({ devtools: "settings", config, defaultConfig }); + await this.connectToPort(port); }); } @@ -59,22 +42,25 @@ class DevToolsBridge extends EventTarget { .forEach((port) => port.postMessage(message)); } - async isDevToolsEnabled() { - const isDevToolsOptional = Boolean(browser.runtime.getManifest().optional_permissions?.includes("devtools")); - const hasDevToolsPermission = await browser.permissions.contains({ permissions: ["devtools"] }); - - if (isDevToolsOptional && hasDevToolsPermission) { - return true; + private async connectToPort(port: Port) { + if (port.name === "webeid-devtools") { + this.devToolPorts.push(port); } - const isStorageEnabled = await isBrowserStorageEnabled(); - if (isStorageEnabled) { - const { devtoolsEnabled } = await browser.storage.local.get(["devtoolsEnabled"]); + port.onMessage.addListener((message) => { + if (message.devtools === "setting-set") { + setConfigOverride(message.key, message.value); - return Boolean(devtoolsEnabled); - } + this.send({ devtools: "settings", config, defaultConfig }, { ignore: port }); + } + }); + + port.onDisconnect.addListener(() => { + this.devToolPorts = this.devToolPorts.filter((connectedPort) => connectedPort !== port); + }); - return false; + await loadConfigFromStorage(); + port.postMessage({ devtools: "settings", config, defaultConfig }); } } From 43f9bdd61f6a586423ab72b7f4b38e4ed346f6b5 Mon Sep 17 00:00:00 2001 From: Sven Mitt Date: Thu, 2 Oct 2025 07:59:25 +0300 Subject: [PATCH 13/17] fix: Add polyfill WE2-967 Signed-off-by: Sven Mitt --- static/views/devtools/devtools.js | 4 ++-- static/views/devtools/panels/devtools-webeid.html | 7 ++++--- static/views/devtools/panels/devtools-webeid.js | 7 ++++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/static/views/devtools/devtools.js b/static/views/devtools/devtools.js index e1212e9..372c6a1 100644 --- a/static/views/devtools/devtools.js +++ b/static/views/devtools/devtools.js @@ -21,9 +21,9 @@ */ async function isDevToolsEnabled() { - const isDevToolsOptional = Boolean(browser.runtime.getManifest().optional_permissions?.includes("devtools")); + const isOptionalPermissionDevToolsTurnedOn = Boolean(browser.runtime.getManifest().optional_permissions?.includes("devtools")); - if (isDevToolsOptional) { + if (isOptionalPermissionDevToolsTurnedOn) { return true; } diff --git a/static/views/devtools/panels/devtools-webeid.html b/static/views/devtools/panels/devtools-webeid.html index dbb970a..3dea1dc 100644 --- a/static/views/devtools/panels/devtools-webeid.html +++ b/static/views/devtools/panels/devtools-webeid.html @@ -12,10 +12,10 @@ @@ -88,7 +88,8 @@ + - \ No newline at end of file + diff --git a/static/views/devtools/panels/devtools-webeid.js b/static/views/devtools/panels/devtools-webeid.js index f5fad7b..99ac1df 100644 --- a/static/views/devtools/panels/devtools-webeid.js +++ b/static/views/devtools/panels/devtools-webeid.js @@ -24,7 +24,7 @@ import events from "./webeid-events.js"; import log from "./webeid-log.js"; import settings from "./webeid-settings.js"; -const backgroundConnection = chrome.runtime.connect({ +const backgroundConnection = browser.runtime.connect({ name: "webeid-devtools", }); @@ -46,9 +46,10 @@ backgroundConnection.onDisconnect.addListener(() => { }); backgroundConnection.onMessage.addListener((message) => { - if (!message.tabId || message.tabId === chrome.devtools.inspectedWindow.tabId) { + if (!message.tabId || message.tabId === browser.devtools.inspectedWindow.tabId) { if (message.devtools === "log") { log.append(message); + } else if (message.devtools === "event") { events.append(message); @@ -74,4 +75,4 @@ backgroundConnection.onMessage.addListener((message) => { document.querySelector('header .version').textContent = defaultConfig.VERSION; } } -}); \ No newline at end of file +}); From eea8aea708df8341bceeb15290bc853b673aff6b Mon Sep 17 00:00:00 2001 From: Sven Mitt Date: Thu, 2 Oct 2025 08:00:13 +0300 Subject: [PATCH 14/17] fix: Make Safari temporary Extension to work WE2-967 Signed-off-by: Sven Mitt --- README.md | 5 +++++ static/views/devtools/panels/devtools-webeid.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 25c5b44..e1b36c2 100644 --- a/README.md +++ b/README.md @@ -82,3 +82,8 @@ The Web eID DevTools tab can be useful while integrating Web eID on a website. 2. Open the Web eID extension details 3. Open "Extension options" 4. Toggle "Enable developer tools" + +**Safari** +1. Open [Settings -> Extensions] +2. Open the Web eID Settings +3. Toggle "Enable developer tools" diff --git a/static/views/devtools/panels/devtools-webeid.js b/static/views/devtools/panels/devtools-webeid.js index 99ac1df..1d8d55e 100644 --- a/static/views/devtools/panels/devtools-webeid.js +++ b/static/views/devtools/panels/devtools-webeid.js @@ -46,7 +46,7 @@ backgroundConnection.onDisconnect.addListener(() => { }); backgroundConnection.onMessage.addListener((message) => { - if (!message.tabId || message.tabId === browser.devtools.inspectedWindow.tabId) { + if (!message.tabId || message.tabId === browser.devtools.inspectedWindow.tabId || browser.devtools.inspectedWindow.tabId === -1) { if (message.devtools === "log") { log.append(message); From 966883af912c32aba314c69640ae780e57576ca7 Mon Sep 17 00:00:00 2001 From: Sven Mitt Date: Thu, 2 Oct 2025 08:04:38 +0300 Subject: [PATCH 15/17] fix: "permissions": ["devtools"] causes error in Safari, make it similar to devtools.js WE2-967 Signed-off-by: Sven Mitt --- src/shared/Logger.ts | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/src/shared/Logger.ts b/src/shared/Logger.ts index f0ca00b..b889fa1 100644 --- a/src/shared/Logger.ts +++ b/src/shared/Logger.ts @@ -24,6 +24,7 @@ import { serializeError } from "@web-eid.js/utils/errorSerializer"; import { MessageSender } from "../models/Browser/Runtime"; import devToolsBridge from "./devToolsBridge"; +import isBrowserStorageEnabled from "./utils/isBrowserStorageEnabled"; type Layer = "Website" | "Extension (content)" | "Extension (background)" | "Native app"; @@ -44,20 +45,6 @@ interface DevToolsEventMessage { time: string; } -/* -let devtoolPorts: Array = []; - -browser.runtime.onConnect.addListener((port) => { - if (port.name === "webeid-devtools") { - devtoolPorts.push(port); - } - - port.onDisconnect.addListener(() => { - devtoolPorts = devtoolPorts.filter((devtoolsPort) => devtoolsPort != port); - }); -}); -*/ - export default class Logger { private module: string; private isContentScript = false; @@ -94,12 +81,26 @@ export default class Logger { console.debug(...args); } - async isDevtoolsAvailable(): Promise { - return (await browser?.permissions?.contains({ "permissions": ["devtools"] })) ?? false; + async isDevToolsEnabled(): Promise { + const isOptionalPermissionDevToolsTurnedOn = Boolean(browser.runtime.getManifest().optional_permissions?.includes("devtools")); + + if (isOptionalPermissionDevToolsTurnedOn) { + return true; + } + + const isStorageEnabled = await isBrowserStorageEnabled(); + + if (isStorageEnabled) { + const { devtoolsEnabled } = await browser.storage.local.get(["devtoolsEnabled"]); + + return Boolean(devtoolsEnabled); + } + + return false; } async devToolsLog(type: "event" | "log" | "info" | "warn" | "error" | "debug", rawMessage: Array) { - if (this.isContentScript || await this.isDevtoolsAvailable()) { + if (this.isContentScript || await this.isDevToolsEnabled()) { const time = this.getCurrentTime(); const source = this.module; @@ -129,7 +130,7 @@ export default class Logger { } async devToolsEvent(type: "request" | "response", layer1: Layer, layer2: Layer, data: any) { - if (this.isContentScript || await this.isDevtoolsAvailable()) { + if (this.isContentScript || await this.isDevToolsEnabled()) { const time = this.getCurrentTime(); const devToolsEventMessage: DevToolsEventMessage = { @@ -159,7 +160,7 @@ export default class Logger { devToolsBridge.send({ tabId: sender.tab?.id, - + devtools, source, type, @@ -171,13 +172,13 @@ export default class Logger { devToolsBridge.send({ tabId: sender.tab?.id, - + devtools, type, data, layer1, layer2, - time + time }); } } From 01c6154e3ab576892283f0f123e1a7de6b1c5ede Mon Sep 17 00:00:00 2001 From: Sven Mitt Date: Thu, 2 Oct 2025 08:06:16 +0300 Subject: [PATCH 16/17] fix: Firefox does not use options page, it uses internal toggle for turning optional_permissions -> devtools on WE2-967 Signed-off-by: Sven Mitt --- scripts/build.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/build.mjs b/scripts/build.mjs index e93f3be..088399b 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -115,6 +115,7 @@ const targets = { ); await cp("./static/_locales", "./dist/firefox/_locales"); await cp("./static/views", "./dist/firefox/views"); + await rm("./dist/firefox/views/options.*"); await cp("./node_modules/webextension-polyfill/dist/browser-polyfill.min.js", "./dist/firefox/views/browser-polyfill.min.js"); rem( From 1b37ae8dfc2f7b263c1899abf49da04444ebd25d Mon Sep 17 00:00:00 2001 From: Mart Somermaa Date: Thu, 13 Nov 2025 16:42:46 +0200 Subject: [PATCH 17/17] deps: update lib/web-eid.js Signed-off-by: Mart Somermaa --- lib/web-eid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web-eid.js b/lib/web-eid.js index 4138a66..3b46b55 160000 --- a/lib/web-eid.js +++ b/lib/web-eid.js @@ -1 +1 @@ -Subproject commit 4138a66e5ba0596bfe9a76d2b75aebe13c7a3216 +Subproject commit 3b46b55051a0e1c4d79b86d0f682e8f04f226baf