commit 46aea254d06622534a2275a4ca0d39d400ed62d1 Author: imnyang Date: Thu Mar 5 03:06:24 2026 +0900 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/README.md b/README.md new file mode 100644 index 0000000..8f2fc7a --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# Figma Windows UA Spoofer (Chrome + Firefox) + +`figma.com` / `*.figma.com`에서만 User-Agent를 Windows로 바꾸고, +페이지 JS의 `navigator.userAgent`, `navigator.platform`도 Windows 값으로 오버라이드합니다. + +## 빠른 사용 + +### Chrome / Edge + +1. `chrome://extensions` (Edge: `edge://extensions`) 접속 +2. 개발자 모드 ON +3. 압축해제된 확장 프로그램 로드 +4. 프로젝트 폴더 선택 + +### Firefox unsigned 빌드 + +```bash +./build-firefox.sh +``` + +- 출력: `dist/figma-windows-ua-firefox-unsigned.xpi` +- 개발/테스트용 unsigned 패키지 + +### Firefox 서명 빌드 (실사용) + +1. AMO(Add-ons Mozilla)에서 API Key/Secret 발급 +2. `.env`에 값 저장 (자동 로드) + +```dotenv +AMO_JWT_ISSUER="" +AMO_JWT_SECRET="" +``` + +3. Nix로 서명 실행 (추천) + +```bash +nix run .#sign-firefox +``` + +- 출력: `dist/signed/*.xpi` (서명 완료) + +## Firefox 설치 + +- 임시 로드: `about:debugging#/runtime/this-firefox` → 임시 부가 기능 로드 → `dist/firefox/manifest.json` +- 실제 배포/설치: `dist/signed/*.xpi` 사용 + +## 확인 + +`figma.com`에서 콘솔 실행: + +```js +navigator.userAgent +navigator.platform +navigator.userAgentData?.platform +``` + +Windows 계열 값이면 정상입니다. diff --git a/background.firefox.js b/background.firefox.js new file mode 100644 index 0000000..5a31bf3 --- /dev/null +++ b/background.firefox.js @@ -0,0 +1,29 @@ +const WINDOWS_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.7444.163 Safari/537.36'; + +browser.webRequest.onBeforeSendHeaders.addListener( + (details) => { + const headers = details.requestHeaders || []; + let found = false; + + for (const header of headers) { + if (header.name && header.name.toLowerCase() === 'user-agent') { + header.value = WINDOWS_UA; + found = true; + break; + } + } + + if (!found) { + headers.push({ name: 'User-Agent', value: WINDOWS_UA }); + } + + return { requestHeaders: headers }; + }, + { + urls: [ + 'https://figma.com/*', + 'https://*.figma.com/*' + ] + }, + ['blocking', 'requestHeaders'] +); diff --git a/build-firefox.sh b/build-firefox.sh new file mode 100755 index 0000000..2d648d1 --- /dev/null +++ b/build-firefox.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DIST_DIR="$ROOT_DIR/dist/firefox" +XPI_PATH="$ROOT_DIR/dist/figma-windows-ua-firefox-unsigned.xpi" + +rm -rf "$DIST_DIR" +mkdir -p "$DIST_DIR" + +rm -f "$ROOT_DIR/dist/figma-windows-ua-firefox.xpi" +rm -f "$ROOT_DIR/dist/figma-windows-ua-firefox.zip" + +cp "$ROOT_DIR/manifest.firefox.json" "$DIST_DIR/manifest.json" +cp "$ROOT_DIR/background.firefox.js" "$DIST_DIR/background.firefox.js" +cp "$ROOT_DIR/content.js" "$DIST_DIR/content.js" +cp "$ROOT_DIR/injected.js" "$DIST_DIR/injected.js" + +rm -f "$XPI_PATH" +( + cd "$DIST_DIR" + zip -r "$XPI_PATH" . +) + +echo "Firefox build created: $XPI_PATH" diff --git a/content.js b/content.js new file mode 100644 index 0000000..bd9c07f --- /dev/null +++ b/content.js @@ -0,0 +1,6 @@ +(() => { + const script = document.createElement('script'); + script.src = chrome.runtime.getURL('injected.js'); + script.onload = () => script.remove(); + (document.head || document.documentElement).appendChild(script); +})(); diff --git a/dist/figma-windows-ua-firefox-unsigned.xpi b/dist/figma-windows-ua-firefox-unsigned.xpi new file mode 100644 index 0000000..5295266 Binary files /dev/null and b/dist/figma-windows-ua-firefox-unsigned.xpi differ diff --git a/dist/firefox/.amo-upload-uuid b/dist/firefox/.amo-upload-uuid new file mode 100644 index 0000000..54f8282 --- /dev/null +++ b/dist/firefox/.amo-upload-uuid @@ -0,0 +1 @@ +{"uploadUuid":"b4b16a82abdb4ce5850b5d0deb76ef3b","channel":"unlisted","xpiCrcHash":"e5567d47cfb9f6133110396308a37ed42afc5be51f6b047ae4643d3f5f063591"} \ No newline at end of file diff --git a/dist/firefox/background.firefox.js b/dist/firefox/background.firefox.js new file mode 100644 index 0000000..5a31bf3 --- /dev/null +++ b/dist/firefox/background.firefox.js @@ -0,0 +1,29 @@ +const WINDOWS_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.7444.163 Safari/537.36'; + +browser.webRequest.onBeforeSendHeaders.addListener( + (details) => { + const headers = details.requestHeaders || []; + let found = false; + + for (const header of headers) { + if (header.name && header.name.toLowerCase() === 'user-agent') { + header.value = WINDOWS_UA; + found = true; + break; + } + } + + if (!found) { + headers.push({ name: 'User-Agent', value: WINDOWS_UA }); + } + + return { requestHeaders: headers }; + }, + { + urls: [ + 'https://figma.com/*', + 'https://*.figma.com/*' + ] + }, + ['blocking', 'requestHeaders'] +); diff --git a/dist/firefox/content.js b/dist/firefox/content.js new file mode 100644 index 0000000..bd9c07f --- /dev/null +++ b/dist/firefox/content.js @@ -0,0 +1,6 @@ +(() => { + const script = document.createElement('script'); + script.src = chrome.runtime.getURL('injected.js'); + script.onload = () => script.remove(); + (document.head || document.documentElement).appendChild(script); +})(); diff --git a/dist/firefox/injected.js b/dist/firefox/injected.js new file mode 100644 index 0000000..f6be54c --- /dev/null +++ b/dist/firefox/injected.js @@ -0,0 +1,62 @@ +(() => { + const windowsUA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.7444.163 Safari/537.36'; + const appVersion = '5.0 (Windows)'; + + const override = (obj, key, value) => { + try { + Object.defineProperty(obj, key, { + get: () => value, + configurable: true + }); + } catch (error) { + // no-op + } + }; + + override(Navigator.prototype, 'userAgent', windowsUA); + override(Navigator.prototype, 'appVersion', appVersion); + override(Navigator.prototype, 'platform', 'Win32'); + override(Navigator.prototype, 'oscpu', 'Windows NT 10.0; Win64; x64'); + + if (navigator.userAgentData) { + const uaData = { + brands: [ + { brand: 'Chromium', version: '142' }, + { brand: 'Google Chrome', version: '142' } + ], + mobile: false, + platform: 'Windows', + getHighEntropyValues: async (hints) => { + const result = { + architecture: 'x86', + bitness: '64', + mobile: false, + model: '', + platform: 'Windows', + platformVersion: '10.0.0', + uaFullVersion: '142.0.7444.163', + wow64: false + }; + + if (Array.isArray(hints)) { + return hints.reduce((acc, hint) => { + if (hint in result) acc[hint] = result[hint]; + return acc; + }, {}); + } + + return result; + }, + toJSON: () => ({ + brands: [ + { brand: 'Chromium', version: '142' }, + { brand: 'Google Chrome', version: '142' } + ], + mobile: false, + platform: 'Windows' + }) + }; + + override(Navigator.prototype, 'userAgentData', uaData); + } +})(); diff --git a/dist/firefox/manifest.json b/dist/firefox/manifest.json new file mode 100644 index 0000000..59bf5ae --- /dev/null +++ b/dist/firefox/manifest.json @@ -0,0 +1,38 @@ +{ + "manifest_version": 2, + "name": "Figma Windows UA Spoofer", + "description": "Spoof Windows User-Agent and platform on figma.com", + "version": "1.0.0", + "applications": { + "gecko": { + "id": "figma-windows-ua-spoofer@imnyang.local", + "strict_min_version": "109.0" + } + }, + "permissions": [ + "webRequest", + "webRequestBlocking", + "https://figma.com/*", + "https://*.figma.com/*" + ], + "background": { + "scripts": [ + "background.firefox.js" + ] + }, + "content_scripts": [ + { + "matches": [ + "https://figma.com/*", + "https://*.figma.com/*" + ], + "js": [ + "content.js" + ], + "run_at": "document_start" + } + ], + "web_accessible_resources": [ + "injected.js" + ] +} diff --git a/dist/signed/d168a70cb6a24fe4a478-1.0.0.xpi b/dist/signed/d168a70cb6a24fe4a478-1.0.0.xpi new file mode 100644 index 0000000..612aae7 Binary files /dev/null and b/dist/signed/d168a70cb6a24fe4a478-1.0.0.xpi differ diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..c993e94 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1772542754, + "narHash": "sha256-WGV2hy+VIeQsYXpsLjdr4GvHv5eECMISX1zKLTedhdg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "8c809a146a140c5c8806f13399592dbcb1bb5dc4", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..df25787 --- /dev/null +++ b/flake.nix @@ -0,0 +1,32 @@ +{ + description = "Figma Windows UA Spoofer - Firefox signing environment"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; }; + in + { + devShells.default = pkgs.mkShell { + packages = with pkgs; [ + bash + zip + web-ext + nodejs + ]; + }; + + apps.sign-firefox = { + type = "app"; + program = toString (pkgs.writeShellScript "sign-firefox" '' + set -euo pipefail + exec ${pkgs.bash}/bin/bash ./sign-firefox.sh + ''); + }; + }); +} \ No newline at end of file diff --git a/injected.js b/injected.js new file mode 100644 index 0000000..f6be54c --- /dev/null +++ b/injected.js @@ -0,0 +1,62 @@ +(() => { + const windowsUA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.7444.163 Safari/537.36'; + const appVersion = '5.0 (Windows)'; + + const override = (obj, key, value) => { + try { + Object.defineProperty(obj, key, { + get: () => value, + configurable: true + }); + } catch (error) { + // no-op + } + }; + + override(Navigator.prototype, 'userAgent', windowsUA); + override(Navigator.prototype, 'appVersion', appVersion); + override(Navigator.prototype, 'platform', 'Win32'); + override(Navigator.prototype, 'oscpu', 'Windows NT 10.0; Win64; x64'); + + if (navigator.userAgentData) { + const uaData = { + brands: [ + { brand: 'Chromium', version: '142' }, + { brand: 'Google Chrome', version: '142' } + ], + mobile: false, + platform: 'Windows', + getHighEntropyValues: async (hints) => { + const result = { + architecture: 'x86', + bitness: '64', + mobile: false, + model: '', + platform: 'Windows', + platformVersion: '10.0.0', + uaFullVersion: '142.0.7444.163', + wow64: false + }; + + if (Array.isArray(hints)) { + return hints.reduce((acc, hint) => { + if (hint in result) acc[hint] = result[hint]; + return acc; + }, {}); + } + + return result; + }, + toJSON: () => ({ + brands: [ + { brand: 'Chromium', version: '142' }, + { brand: 'Google Chrome', version: '142' } + ], + mobile: false, + platform: 'Windows' + }) + }; + + override(Navigator.prototype, 'userAgentData', uaData); + } +})(); diff --git a/manifest.firefox.json b/manifest.firefox.json new file mode 100644 index 0000000..59bf5ae --- /dev/null +++ b/manifest.firefox.json @@ -0,0 +1,38 @@ +{ + "manifest_version": 2, + "name": "Figma Windows UA Spoofer", + "description": "Spoof Windows User-Agent and platform on figma.com", + "version": "1.0.0", + "applications": { + "gecko": { + "id": "figma-windows-ua-spoofer@imnyang.local", + "strict_min_version": "109.0" + } + }, + "permissions": [ + "webRequest", + "webRequestBlocking", + "https://figma.com/*", + "https://*.figma.com/*" + ], + "background": { + "scripts": [ + "background.firefox.js" + ] + }, + "content_scripts": [ + { + "matches": [ + "https://figma.com/*", + "https://*.figma.com/*" + ], + "js": [ + "content.js" + ], + "run_at": "document_start" + } + ], + "web_accessible_resources": [ + "injected.js" + ] +} diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..60e96f0 --- /dev/null +++ b/manifest.json @@ -0,0 +1,45 @@ +{ + "manifest_version": 3, + "name": "Figma Windows UA Spoofer", + "description": "Spoof Windows User-Agent and platform on figma.com", + "version": "1.0.0", + "permissions": [ + "declarativeNetRequest" + ], + "host_permissions": [ + "https://figma.com/*", + "https://*.figma.com/*" + ], + "declarative_net_request": { + "rule_resources": [ + { + "id": "ruleset_1", + "enabled": true, + "path": "rules.json" + } + ] + }, + "content_scripts": [ + { + "matches": [ + "https://figma.com/*", + "https://*.figma.com/*" + ], + "js": [ + "content.js" + ], + "run_at": "document_start" + } + ], + "web_accessible_resources": [ + { + "resources": [ + "injected.js" + ], + "matches": [ + "https://figma.com/*", + "https://*.figma.com/*" + ] + } + ] +} diff --git a/rules.json b/rules.json new file mode 100644 index 0000000..9831ff3 --- /dev/null +++ b/rules.json @@ -0,0 +1,30 @@ +[ + { + "id": 1, + "priority": 1, + "action": { + "type": "modifyHeaders", + "requestHeaders": [ + { + "header": "User-Agent", + "operation": "set", + "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.7444.163 Safari/537.36" + } + ] + }, + "condition": { + "regexFilter": "^https:\\/\\/([a-zA-Z0-9-]+\\.)*figma\\.com\\/", + "resourceTypes": [ + "main_frame", + "sub_frame", + "xmlhttprequest", + "script", + "image", + "font", + "stylesheet", + "media", + "other" + ] + } + } +] diff --git a/sign-firefox.sh b/sign-firefox.sh new file mode 100755 index 0000000..d4b54c2 --- /dev/null +++ b/sign-firefox.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SOURCE_DIR="$ROOT_DIR/dist/firefox" +SIGNED_DIR="$ROOT_DIR/dist/signed" + +if [ -f "$ROOT_DIR/.env" ]; then + set -a + # shellcheck disable=SC1091 + . "$ROOT_DIR/.env" + set +a +fi + +if [ -z "${AMO_JWT_ISSUER:-}" ] || [ -z "${AMO_JWT_SECRET:-}" ]; then + echo "AMO_JWT_ISSUER / AMO_JWT_SECRET 값이 필요합니다." + echo "(.env 또는 환경변수에서 읽습니다)" + echo "예:" + echo " export AMO_JWT_ISSUER=\"\"" + echo " export AMO_JWT_SECRET=\"\"" + exit 1 +fi + +"$ROOT_DIR/build-firefox.sh" +mkdir -p "$SIGNED_DIR" + +if command -v web-ext >/dev/null 2>&1; then + web-ext sign \ + --source-dir "$SOURCE_DIR" \ + --artifacts-dir "$SIGNED_DIR" \ + --api-key "$AMO_JWT_ISSUER" \ + --api-secret "$AMO_JWT_SECRET" \ + --channel unlisted +elif command -v npx >/dev/null 2>&1; then + npx --yes web-ext sign \ + --source-dir "$SOURCE_DIR" \ + --artifacts-dir "$SIGNED_DIR" \ + --api-key "$AMO_JWT_ISSUER" \ + --api-secret "$AMO_JWT_SECRET" \ + --channel unlisted +else + echo "web-ext 또는 npx가 필요합니다." + echo "Nix 사용 시: nix develop -c ./sign-firefox.sh" + exit 1 +fi + +echo "Signed XPI generated under: $SIGNED_DIR"