From 0146a726bdcea4388d9e0694d64fc4f34592f51e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Sun, 27 Nov 2022 09:20:27 +0000 Subject: [PATCH] poc (#1) Reviewed-on: https://gitea.dwysokinski.me/twhelp/sessions-ext/pulls/1 --- .gitignore | 2 +- .prettierrc.js | 3 + package.json | 17 +++-- src/background.ts | 141 ++++++++++++++++++++++++++++++++++++++++- src/content.ts | 69 ++++++++++++++++++++ src/crypto.ts | 85 +++++++++++++++++++++++++ src/icon.png | Bin 1165 -> 5892 bytes src/manifest.json | 31 +++++---- src/message.ts | 16 +++++ src/options-storage.ts | 10 +++ src/options.html | 13 ---- src/popup.html | 34 ++++++++++ src/popup.ts | 7 ++ tsconfig.json | 11 ++++ yarn.lock | 41 ++++++++++++ 15 files changed, 446 insertions(+), 34 deletions(-) create mode 100644 .prettierrc.js create mode 100644 src/content.ts create mode 100644 src/crypto.ts create mode 100644 src/message.ts create mode 100644 src/options-storage.ts delete mode 100644 src/options.html create mode 100644 src/popup.html create mode 100644 src/popup.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 73d03b2..f877b91 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -distribution +dist .idea node_modules .parcel-cache diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..e340799 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + singleQuote: true, +}; diff --git a/package.json b/package.json index 0402eb0..9b2bcbb 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "sessions-ext", - "version": "0.0.1", + "version": "0.1.0", "description": "", "scripts": { - "build": "parcel build src/manifest.json --no-content-hash --no-source-maps --dist-dir distribution --no-cache --detailed-report 0", - "watch": "parcel watch src/manifest.json --dist-dir distribution --no-cache --no-hmr", + "build": "parcel build src/manifest.json --no-content-hash --no-source-maps --dist-dir dist --detailed-report 0", + "watch": "parcel watch src/manifest.json --dist-dir dist --no-cache --no-hmr", "run:chromium": "web-ext run -t chromium" }, "author": { @@ -17,14 +17,21 @@ "@parcel/config-webextension": "^2.8.0", "@types/chrome": "^0.0.200", "parcel": "^2.8.0", + "prettier": "^2.7.1", "typescript": "^4.8.4" }, + "browserslist": [ + "since 2017-06" + ], "webExt": { - "sourceDir": "distribution", + "sourceDir": "dist", "run": { "startUrl": [ - "https://pl181.plemiona.pl/game.php?screen=info_player" + "https://www.tribalwars.net" ] } + }, + "dependencies": { + "webext-options-sync": "^4.0.0" } } diff --git a/src/background.ts b/src/background.ts index b611819..14a8525 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1 +1,140 @@ -chrome.cookies.onChanged.addListener(console.log); +import { LoginMessage, LoginResponse, Message, MessageType } from './message'; +import { optionsStorage } from './options-storage'; +import { decrypt, encrypt } from './crypto'; + +const COOKIE_NAME = 'sid'; +const HTTP_STATUS_OK = 200; +const API_KEY_HEADER = 'X-Api-Key'; + +chrome.runtime.onMessage.addListener( + async (message: Message, sender, sendResponse) => { + try { + switch (message.type) { + case MessageType.LOGIN: + await handleLogin(message); + } + } catch (err: any) { + sendResponse({ error: err.message } as LoginResponse); + } + } +); + +const handleLogin = async (message: LoginMessage) => { + const sid = await chrome.cookies.get({ + name: COOKIE_NAME, + url: message.url, + }); + + if (sid) { + const success = await tryOpenOverview(message.url); + if (success) { + await createOrUpdateCookie(message.server, await encryptCookie(sid)); + await openOverview(message.url); + return; + } + } + + const sidFromApi = await getSid(message.server); + if (sidFromApi.length > 0) { + await setSid(message.url, sidFromApi); + if (await tryOpenOverview(message.url)) { + await openOverview(message.url); + return; + } + } + + await fetch(message.loginUrl, { + method: 'GET', + credentials: 'include', + }); + + const newSid = await chrome.cookies.get({ + name: COOKIE_NAME, + url: message.url, + }); + if (!newSid) { + return; + } + + await createOrUpdateCookie(message.server, await encryptCookie(newSid)); + await openOverview(message.url); +}; + +const tryOpenOverview = async (base: string) => { + try { + const resp = await fetch(buildUrlToOverview(base), { + method: 'GET', + credentials: 'include', + redirect: 'error', + }); + return resp.status == HTTP_STATUS_OK; + } catch (e) { + return false; + } +}; + +const openOverview = async (base: string) => { + await chrome.tabs.update({ + url: buildUrlToOverview(base).toString(), + }); +}; + +const buildUrlToOverview = (base: string) => { + const url = new URL(base); + url.pathname = '/game.php'; + url.searchParams.set('screen', 'overview_villages'); + return url; +}; + +const createOrUpdateCookie = async (server: string, sid: string) => { + const opts = await optionsStorage.getAll(); + + const url = new URL(opts.apiUrl); + url.pathname = `/api/v1/user/sessions/${server}`; + + await fetch(url, { + method: 'PUT', + body: sid, + headers: { + [API_KEY_HEADER]: opts.apiKey, + 'Content-Type': 'text/plain', + }, + }); +}; + +const getSid = async (server: string): Promise => { + const opts = await optionsStorage.getAll(); + + const url = new URL(opts.apiUrl); + url.pathname = `/api/v1/user/sessions/${server}`; + + const resp = await fetch(url, { + method: 'GET', + headers: { + [API_KEY_HEADER]: opts.apiKey, + }, + }); + + const respBody = await resp.json(); + if (!respBody.data?.sid) { + return ''; + } + + return respBody.data.sid; +}; + +const encryptCookie = async (cookie: chrome.cookies.Cookie) => { + const opts = await optionsStorage.getAll(); + return await encrypt(cookie.value, opts.encryptionPassword); +}; + +const setSid = async (url: string, sid: string) => { + const opts = await optionsStorage.getAll(); + await chrome.cookies.set({ + url, + name: COOKIE_NAME, + value: await decrypt(sid, opts.encryptionPassword), + httpOnly: true, + secure: true, + }); +}; diff --git a/src/content.ts b/src/content.ts new file mode 100644 index 0000000..558df93 --- /dev/null +++ b/src/content.ts @@ -0,0 +1,69 @@ +import { LoginMessage, LoginResponse, MessageType } from './message'; + +let isLoggingIn = false; + +const renderUI = () => { + document + .querySelectorAll('.worlds-container:first-of-type .world-select') + .forEach((el) => { + const cloned = el.cloneNode(true); + if (!(cloned instanceof HTMLAnchorElement)) { + return; + } + + cloned.addEventListener('click', handleClick); + + const span = cloned.childNodes.item(1); + if (!span) { + return; + } + span.textContent += ' (SS)'; + + el.parentNode?.insertBefore(cloned, el.nextSibling); + }); +}; + +const handleClick = async (e: MouseEvent) => { + if (!(e.currentTarget instanceof HTMLAnchorElement)) { + return; + } + + e.preventDefault(); + + if (isLoggingIn) { + return; + } + + isLoggingIn = true; + + const url = new URL(e.currentTarget.href); + const server = extractServerFromURL(url); + chrome.runtime + .sendMessage({ + type: MessageType.LOGIN, + server, + loginUrl: url.toString(), + url: url.protocol + '//' + url.host.replace('www', server), + } as LoginMessage) + .then((resp: LoginResponse) => { + isLoggingIn = false; + if (!resp?.error) { + return; + } + console.error(resp.error); + }); +}; + +const extractServerFromURL = (url: URL): string => { + const split = url.pathname.split('/'); + if (split.length <= 0) { + throw new Error(`invalid path: ${url.pathname}`); + } + return split[split.length - 1]; +}; + +const init = () => { + renderUI(); +}; + +init(); diff --git a/src/crypto.ts b/src/crypto.ts new file mode 100644 index 0000000..289de93 --- /dev/null +++ b/src/crypto.ts @@ -0,0 +1,85 @@ +const ITERATIONS = 100000; +const SALT_LENGTH = 16; +const IV_LENGTH = 12; + +const enc = new TextEncoder(); +const dec = new TextDecoder(); + +export const encrypt = async (data: string, password: string) => { + const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH)); + const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH)); + const passwordKey = await getPasswordKey(password); + const aesKey = await deriveKey(passwordKey, salt, ['encrypt']); + const encryptedContent = await crypto.subtle.encrypt( + { + name: 'AES-GCM', + iv: iv, + }, + aesKey, + enc.encode(data) + ); + + const encryptedContentArr = new Uint8Array(encryptedContent); + const buf = new Uint8Array( + salt.byteLength + iv.byteLength + encryptedContentArr.byteLength + ); + buf.set(salt, 0); + buf.set(iv, salt.byteLength); + buf.set(encryptedContentArr, salt.byteLength + iv.byteLength); + return bufToBase64(buf); +}; + +export const decrypt = async (encryptedData: string, password: string) => { + const encryptedDataBuff = base64ToBuf(encryptedData); + const salt = encryptedDataBuff.slice(0, SALT_LENGTH); + const iv = encryptedDataBuff.slice(SALT_LENGTH, SALT_LENGTH + IV_LENGTH); + const data = encryptedDataBuff.slice(SALT_LENGTH + IV_LENGTH); + const passwordKey = await getPasswordKey(password); + const aesKey = await deriveKey(passwordKey, salt, ['decrypt']); + const decryptedContent = await crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv: iv, + }, + aesKey, + data + ); + return dec.decode(decryptedContent); +}; + +const getPasswordKey = (password: string) => { + return crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, [ + 'deriveKey', + ]); +}; + +const deriveKey = ( + passwordKey: CryptoKey, + salt: Uint8Array, + keyUsages: KeyUsage[] +) => { + return crypto.subtle.deriveKey( + { + name: 'PBKDF2', + salt: salt, + iterations: ITERATIONS, + hash: 'SHA-256', + }, + passwordKey, + { name: 'AES-GCM', length: 256 }, + false, + keyUsages + ); +}; + +const bufToBase64 = (buf: Uint8Array) => { + let s = ''; + buf.forEach((c) => { + s += String.fromCharCode(c); + }); + return btoa(s); +}; + +const base64ToBuf = (b64: string) => { + return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)); +}; diff --git a/src/icon.png b/src/icon.png index a4905069f9a21264dfda3d2064565c720a5650ef..85812eabb741b32a36b50bfde020d4d943ce1844 100644 GIT binary patch literal 5892 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 LNkvXXu0mjfnXW5z diff --git a/src/manifest.json b/src/manifest.json index c7c4a4c..29fec4f 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,26 +1,29 @@ { - "name": "Awesome Extension", - "version": "0.0.0", - "description": "An awesome new browser extension", + "name": "Sessions", + "version": "0.1.0", + "description": "A browser extension aiming to simplify coplaying", "homepage_url": "https://tribalwarshelp.com/", "manifest_version": 3, "minimum_chrome_version": "100", "icons": { "128": "icon.png" }, - "permissions": [ - "storage", - "cookies" - ], - "host_permissions": [ - "https://*.plemiona.pl/*" - ], - "options_ui": { - "browser_style": true, - "page": "options.html" + "action": { + "default_title": "Sessions", + "default_popup": "popup.html" }, + "permissions": ["storage", "cookies", "activeTab", "tabs"], + "host_permissions": ["https://*.tribalwars.net/*", "https://*.plemiona.pl/*"], + "content_scripts": [ + { + "matches": ["https://www.tribalwars.net/*", "https://www.plemiona.pl/*"], + "js": ["content.ts"], + "run_at": "document_end" + } + ], "background": { "service_worker": "background.ts", "type": "module" - } + }, + "author": "Dawid WysokiƄski" } diff --git a/src/message.ts b/src/message.ts new file mode 100644 index 0000000..b48140f --- /dev/null +++ b/src/message.ts @@ -0,0 +1,16 @@ +export enum MessageType { + LOGIN, +} + +export type LoginMessage = { + type: MessageType.LOGIN; + url: string; + loginUrl: string; + server: string; +}; + +export type Message = LoginMessage; + +export type LoginResponse = { + error?: string; +}; diff --git a/src/options-storage.ts b/src/options-storage.ts new file mode 100644 index 0000000..7f9705b --- /dev/null +++ b/src/options-storage.ts @@ -0,0 +1,10 @@ +import OptionsSync from 'webext-options-sync'; + +export const optionsStorage = new OptionsSync({ + defaults: { + apiUrl: 'https://sessions.tribalwarshelp.com', + apiKey: '', + encryptionPassword: 'password', + }, + migrations: [OptionsSync.migrations.removeUnused], +}); diff --git a/src/options.html b/src/options.html deleted file mode 100644 index 17b2a8d..0000000 --- a/src/options.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Sessions - - -

Test

- - diff --git a/src/popup.html b/src/popup.html new file mode 100644 index 0000000..08e7a4d --- /dev/null +++ b/src/popup.html @@ -0,0 +1,34 @@ + + + + + + + Sessions + + +
+

Options

+
+
+ + + +
+
+
+ + + diff --git a/src/popup.ts b/src/popup.ts new file mode 100644 index 0000000..93a9bfd --- /dev/null +++ b/src/popup.ts @@ -0,0 +1,7 @@ +import { optionsStorage } from './options-storage'; + +const init = async () => { + await optionsStorage.syncForm('#options form'); +}; + +init(); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e1339e0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2017", + "strict": true, + "moduleResolution": "Node", + "strictNullChecks": true, + "strictFunctionTypes": true, + "skipLibCheck": true, + "noImplicitAny": true + } +} diff --git a/yarn.lock b/yarn.lock index 46bcccf..2af8bc4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -960,6 +960,11 @@ detect-libc@^1.0.3: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== +dom-form-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-form-serializer/-/dom-form-serializer-2.0.0.tgz#b1550efefa08ebca15547ee1b4c791bf7b45c748" + integrity sha512-HMrrc7gJIBj6sWmnJcO9DLZj8AsdFP60+pZSu0vMJxZhEP3GPfsNE9X1GC95nXZ0SZbS8FYDb/NHW/NArSmu0Q== + dom-serializer@^1.0.1: version "1.4.1" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" @@ -1185,6 +1190,11 @@ lmdb@2.5.2: "@lmdb/lmdb-linux-x64" "2.5.2" "@lmdb/lmdb-win32-x64" "2.5.2" +lz-string@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" + integrity sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ== + mdn-data@2.0.14: version "2.0.14" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" @@ -1334,6 +1344,11 @@ posthtml@^0.16.4, posthtml@^0.16.5: posthtml-parser "^0.11.0" posthtml-render "^3.0.0" +prettier@^2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64" + integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== + react-error-overlay@6.0.9: version "6.0.9" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" @@ -1424,6 +1439,11 @@ terser@^5.2.0: commander "^2.20.0" source-map-support "~0.5.20" +throttle-debounce@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-5.0.0.tgz#a17a4039e82a2ed38a5e7268e4132d6960d41933" + integrity sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg== + timsort@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" @@ -1467,6 +1487,27 @@ weak-lru-cache@^1.2.2: resolved "https://registry.yarnpkg.com/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz#fdbb6741f36bae9540d12f480ce8254060dccd19" integrity sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw== +webext-detect-page@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/webext-detect-page/-/webext-detect-page-4.0.1.tgz#124a6802c872fa0bc12549d99b2dd53d18af2ba7" + integrity sha512-Y9Skw6/Uj0dGwOIidc1XqZ3neEbmuuT4BlkL/J4JHAo6fVznHIZq6/MWDsPGOA/jnNowiSXtHHh4S/TOxbl6bQ== + +webext-options-sync@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/webext-options-sync/-/webext-options-sync-4.0.0.tgz#d706aa6f1330c07fb061da541477aa68b8538593" + integrity sha512-00umVaF/jpd3cWyT/9OZJf1CM5Z3AtiIKT/4Fzuhwfe3pg2z84cGz/RwaIRiN60zz7A64ieEG1z9dKT8/cEICQ== + dependencies: + dom-form-serializer "^2.0.0" + lz-string "^1.4.4" + throttle-debounce "^5.0.0" + webext-detect-page "^4.0.1" + webext-polyfill-kinda "^0.10.0" + +webext-polyfill-kinda@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/webext-polyfill-kinda/-/webext-polyfill-kinda-0.10.0.tgz#5eb154c581edae827f2832c090811d14dd074ea2" + integrity sha512-Yz5WTwig5byFfMXgagtfaJkVU+RrnVqtL1hmvA+GIbpRaGKU1DIrFYHMUUFkeyFqxRSuhbOdLKzteXxCd6VNzA== + xxhash-wasm@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/xxhash-wasm/-/xxhash-wasm-0.4.2.tgz#752398c131a4dd407b5132ba62ad372029be6f79"