From 57545ae80f60371597ac9a7ab98dfa275b27c18a Mon Sep 17 00:00:00 2001 From: go0p Date: Fri, 7 Feb 2025 11:36:25 +0800 Subject: [PATCH] add proxy switch demo --- .gitignore | 2 +- public/background.js | 5 +- public/content.js | 10 +- public/manifest.json | 27 +- public/proxy.js | 214 + public/proxy/options.css | 160 + public/proxy/options.html | 11 + public/proxy/options.js | 170 + public/proxy/proxy-auth.js | 98 + public/proxy/proxy-manager.js | 87 + public/proxy/proxy-settings.js | 41 + public/sidepanel.html | 10 + public/socket.js | 5 +- public/types/action.js | 5 + src/App.tsx | 8 +- src/components/EvalInTab.tsx | 1 + src/components/ProxySwitch/index.css | 68 + src/components/ProxySwitch/index.tsx | 288 + src/pages/OptionsPage/index.css | 100 + src/pages/OptionsPage/index.tsx | 385 + src/pages/options.tsx | 15 + src/styles/global.css | 37 + src/types/action.ts | 15 + src/types/chrome.ts | 19 + src/types/proxy.ts | 11 + tsconfig.json | 6 +- webpack.config.js | 27 +- yarn.lock | 10661 +++++++++++++++++++++++++ 28 files changed, 12458 insertions(+), 28 deletions(-) create mode 100644 public/proxy.js create mode 100644 public/proxy/options.css create mode 100644 public/proxy/options.html create mode 100644 public/proxy/options.js create mode 100644 public/proxy/proxy-auth.js create mode 100644 public/proxy/proxy-manager.js create mode 100644 public/proxy/proxy-settings.js create mode 100644 public/sidepanel.html create mode 100644 public/types/action.js create mode 100644 src/components/ProxySwitch/index.css create mode 100644 src/components/ProxySwitch/index.tsx create mode 100644 src/pages/OptionsPage/index.css create mode 100644 src/pages/OptionsPage/index.tsx create mode 100644 src/pages/options.tsx create mode 100644 src/styles/global.css create mode 100644 src/types/action.ts create mode 100644 src/types/chrome.ts create mode 100644 src/types/proxy.ts create mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore index d132fa3..456c430 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ build.pem # production /build - +/2.5.21_0 # misc .DS_Store .env.local diff --git a/public/background.js b/public/background.js index 4b4b3b8..1ed3c9a 100644 --- a/public/background.js +++ b/public/background.js @@ -1,10 +1,13 @@ import {ActionType, injectScriptAndSendMessage, WebSocketManager} from './socket.js'; - +import { setupProxyHandlers } from './proxy.js'; +import { ProxyActionType } from './types/action.js'; console.info("Chrome Extenstion Background is loaded") const websocketManager = new WebSocketManager(); +// 设置代理处理器 +setupProxyHandlers(); chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) { console.log("msg", msg) diff --git a/public/content.js b/public/content.js index fc46a8c..da362a1 100644 --- a/public/content.js +++ b/public/content.js @@ -3,6 +3,7 @@ return; } window.contentScriptInjected = true; + window.badgeCount = 0; // 检查并插入 CSS 样式 const styleId = 'injected-css-style'; if (!document.getElementById(styleId)) { @@ -38,18 +39,21 @@ script.remove(); }; (document.head || document.documentElement).appendChild(script); - window.addEventListener('message', function onMessage(event) { + window.addEventListener('message', async function onMessage(event) { if (event.source !== window || event.data.type !== 'FROM_INJECT_JS') { return; } window.removeEventListener('message', onMessage); - // Send the result to the background script - // chrome.runtime.sendMessage({ action: 'yakit_to_extension_page', result: event.data.result }); + window.badgeCount += 1; // 直接向向发送端返回结果 sendResponse({action: 'yakit_to_extension_page', result: event.data.result}); + // Send updated badge count to background script + await chrome.runtime.sendMessage({action: 'yakit_badge', data: window.badgeCount.toString()}); }); return true; } }); + + })() diff --git a/public/manifest.json b/public/manifest.json index bc6a976..918990e 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -3,36 +3,43 @@ "name": "Yakit Chrome Endpoint", "version": "1.0", "description": "A Endpoint for Yakit MITM or more", + "options_ui": { + "page": "proxy/options.html", + "open_in_tab": true + }, "action": { - "default_popup": "index.html", + "default_title": "Click to open panel", "default_icon": { "16": "/images/icon16.png", "48": "/images/icon48.png", "128": "/images/icon128.png" } }, + "side_panel": { + "default_path" : "index.html" + }, "background": { "service_worker": "background.js", "type": "module" }, "permissions": [ - "webNavigation", - "activeTab", - "scripting", - "tabs", "proxy", "storage", - "webRequest" + "sidePanel" ], "host_permissions": [ - "" + "*://*/*" ], "web_accessible_resources": [ { - "resources": ["inject.js"], - "matches": [""], - "use_dynamic_url": true + "resources": [ + "types/*.js", + "proxy/*.js", + "socket.js", + "proxy.js" + ], + "matches": [""] } ], "icons": { diff --git a/public/proxy.js b/public/proxy.js new file mode 100644 index 0000000..e82e069 --- /dev/null +++ b/public/proxy.js @@ -0,0 +1,214 @@ +import { ProxyManager } from './proxy/proxy-manager.js'; +import { ProxySettings } from './proxy/proxy-settings.js'; +import { ProxyAuth } from './proxy/proxy-auth.js'; +import { ProxyActionType } from './types/action.js'; + +// 记录代理日志 +async function logProxyRequest(details, proxyConfig, error = null) { + const log = { + id: Date.now().toString(), + timestamp: Date.now(), + url: details.url, + proxyId: proxyConfig.id, + proxyName: proxyConfig.name, + status: error ? 'error' : 'success', + errorMessage: error?.message + }; + + const result = await chrome.storage.local.get('proxyLogs'); + const logs = result.proxyLogs || []; + const updatedLogs = [log, ...logs].slice(0, 1000); + await chrome.storage.local.set({ proxyLogs: updatedLogs }); +} + +async function handleSetProxyConfig(config, sendResponse) { + try { + if (!config || !config.host || !config.port) { + sendResponse({ + success: false, + error: '无效的代理配置' + }); + return; + } + + const proxyConfig = { + mode: "fixed_servers", + rules: { + singleProxy: { + scheme: config.scheme || 'http', + host: config.host, + port: parseInt(config.port) + }, + bypassList: ["localhost", "127.0.0.1"] + } + }; + + await chrome.proxy.settings.set({ + value: proxyConfig, + scope: 'regular' + }); + + const settings = await chrome.proxy.settings.get({}); + const isSuccess = settings.value.mode === "fixed_servers" && + settings.value.rules.singleProxy.host === config.host && + settings.value.rules.singleProxy.port === parseInt(config.port); + + if (isSuccess) { + await chrome.storage.local.set({ + currentProxy: { + ...config, + timestamp: Date.now() + } + }); + + const result = await chrome.storage.local.get('proxyConfigs'); + if (result.proxyConfigs) { + const updatedConfigs = result.proxyConfigs.map(c => ({ + ...c, + enabled: c.id === config.id + })); + await chrome.storage.local.set({ proxyConfigs: updatedConfigs }); + } + + console.log('Proxy successfully set:', settings.value); + sendResponse({ success: true }); + } else { + console.error('Proxy settings verification failed'); + sendResponse({ + success: false, + error: '代理设置验证失败' + }); + } + } catch (error) { + console.error('Error setting proxy:', error); + sendResponse({ + success: false, + error: error.message || '设置代理时发生错误' + }); + } +} + +async function handleClearProxyConfig(sendResponse) { + try { + await chrome.proxy.settings.clear({ + scope: 'regular' + }); + + await chrome.proxy.settings.set({ + value: { mode: "system" }, + scope: 'regular' + }); + + // 只移除当前代理配置,保留代理列表 + await chrome.storage.local.remove([ + 'currentProxy', + 'proxyAuthHandlers' + ]); + + // 更新所有代理的启用状态 + const result = await chrome.storage.local.get('proxyConfigs'); + if (result.proxyConfigs) { + const updatedConfigs = result.proxyConfigs.map(config => ({ + ...config, + enabled: false + })); + await chrome.storage.local.set({ proxyConfigs: updatedConfigs }); + } + + const settings = await chrome.proxy.settings.get({}); + const isSuccess = settings.value.mode === "system"; + + if (isSuccess) { + console.log('Proxy successfully cleared'); + sendResponse({ success: true }); + } else { + console.error('Failed to clear proxy settings'); + sendResponse({ + success: false, + error: '无法清除代理设置' + }); + } + } catch (error) { + console.error('Error clearing proxy:', error); + sendResponse({ + success: false, + error: error.message || '清除代理时发生错误' + }); + } +} + +async function handleGetProxyStatus(sendResponse) { + try { + const settings = await chrome.proxy.settings.get({}); + const currentProxy = await chrome.storage.local.get('currentProxy'); + + const status = { + enabled: settings.value.mode === "fixed_servers", + config: currentProxy.currentProxy || null, + mode: settings.value.mode + }; + + console.log('Current proxy status:', status); + sendResponse({ + success: true, + data: status + }); + } catch (error) { + console.error('Error getting proxy status:', error); + sendResponse({ + success: false, + error: error.message || '获取代理状态时发生错误' + }); + } +} + +// 导出代理处理器设置函数 +export function setupProxyHandlers() { + // 设置代理错误处理和认证 + ProxyAuth.setupErrorHandler(); + ProxyAuth.setupAuthListener(); + + // 消息监听器 + chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + console.log("Proxy message:", msg); + + switch (msg.action) { + case ProxyActionType.SET_PROXY_CONFIG: + handleSetProxyConfig(msg.config, sendResponse); + return true; + + case ProxyActionType.CLEAR_PROXY_CONFIG: + handleClearProxyConfig(sendResponse); + return true; + + case ProxyActionType.GET_PROXY_STATUS: + handleGetProxyStatus(sendResponse); + return true; + } + }); + + // 在扩展启动时初始化 + chrome.runtime.onInstalled.addListener(async () => { + try { + // 确保默认配置存在 + await ProxySettings.setDefaultConfigs(); + + // 清除之前的代理设置 + await handleClearProxyConfig(() => {}); + + // 设置认证监听 + await ProxyAuth.setupAuthListener(); + + // 初始化存储 + const storage = await chrome.storage.local.get(['proxyConfigs', 'proxyLogs']); + if (!storage.proxyConfigs) { + await chrome.storage.local.set({ proxyConfigs: [] }); + } + if (!storage.proxyLogs) { + await chrome.storage.local.set({ proxyLogs: [] }); + } + } catch (error) { + console.error('Error during installation:', error); + } + }); +} \ No newline at end of file diff --git a/public/proxy/options.css b/public/proxy/options.css new file mode 100644 index 0000000..808ba2b --- /dev/null +++ b/public/proxy/options.css @@ -0,0 +1,160 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + margin: 0; + padding: 20px; + background-color: #f5f5f5; +} + +.container { + max-width: 800px; + margin: 0 auto; + background: white; + padding: 20px; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 20px; + border-bottom: 1px solid #eee; +} + +.header h1 { + margin: 0; + font-size: 24px; + color: #333; +} + +.proxy-item { + background: white; + border: 1px solid #e8e8e8; + border-radius: 4px; + margin-bottom: 16px; + padding: 16px; + transition: all 0.3s; +} + +.proxy-item:hover { + box-shadow: 0 2px 8px rgba(0,0,0,0.09); +} + +.proxy-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.proxy-name { + font-size: 14px; + padding: 4px 8px; + border: 1px solid #d9d9d9; + border-radius: 4px; + width: 200px; +} + +.proxy-name:focus { + border-color: #40a9ff; + outline: none; + box-shadow: 0 0 0 2px rgba(24,144,255,0.2); +} + +.proxy-type-select, +.proxy-scheme, +.proxy-host, +.proxy-port { + width: 100%; + height: 32px; + padding: 4px 11px; + border: 1px solid #d9d9d9; + border-radius: 4px; + transition: all 0.3s; +} + +.proxy-type-select:focus, +.proxy-scheme:focus, +.proxy-host:focus, +.proxy-port:focus { + border-color: #40a9ff; + outline: none; + box-shadow: 0 0 0 2px rgba(24,144,255,0.2); +} + +.proxy-actions { + display: flex; + gap: 8px; +} + +.proxy-content { + display: flex; + flex-direction: column; + gap: 16px; +} + +.form-group { + margin-bottom: 16px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + color: #666; +} + +.btn-primary { + background: #1890ff; + border: none; + color: white; + padding: 4px 15px; + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; +} + +.btn-primary:hover { + background: #40a9ff; +} + +.btn-secondary { + background: white; + border: 1px solid #d9d9d9; + color: rgba(0,0,0,0.85); + padding: 4px 15px; + border-radius: 4px; + cursor: pointer; + margin-left: 8px; +} + +.btn-secondary:hover { + border-color: #40a9ff; + color: #40a9ff; +} + +.header-actions { + display: flex; + align-items: center; +} + +.delete-btn { + padding: 4px 8px; + background: #ff4d4f; +} + +.delete-btn:hover { + background: #ff7875; +} + +.pac-script { + font-family: monospace; + min-height: 200px; +} + +.options-page { + min-height: 100vh; +} \ No newline at end of file diff --git a/public/proxy/options.html b/public/proxy/options.html new file mode 100644 index 0000000..c5d5bff --- /dev/null +++ b/public/proxy/options.html @@ -0,0 +1,11 @@ + + + + + 代理设置 + + +
+ + + \ No newline at end of file diff --git a/public/proxy/options.js b/public/proxy/options.js new file mode 100644 index 0000000..d9100c9 --- /dev/null +++ b/public/proxy/options.js @@ -0,0 +1,170 @@ +console.log('Options page script loaded'); + +import { ProxySettings } from './proxy-settings.js'; +import { ProxyManager } from './proxy-manager.js'; + +let currentConfigs = []; + +document.addEventListener('DOMContentLoaded', async () => { + try { + await initOptionsPage(); + setupEventListeners(); + } catch (error) { + console.error('Failed to initialize options page:', error); + showError(error.message); + } +}); + +async function initOptionsPage() { + const result = await ProxySettings.exportSettings(); + currentConfigs = result.settings || []; + renderProxyConfigs(); +} + +function renderProxyConfigs() { + const proxyList = document.getElementById('proxyList'); + const template = document.getElementById('proxyItemTemplate'); + + // 清除除了直接连接以外的所有配置 + const items = proxyList.querySelectorAll('.proxy-item:not([data-id="direct"])'); + items.forEach(item => item.remove()); + + // 渲染其他代理配置 + currentConfigs.forEach(config => { + if (config.id === 'direct') return; // 跳过直接连接 + + const clone = template.content.cloneNode(true); + const proxyItem = clone.querySelector('.proxy-item'); + + proxyItem.dataset.id = config.id; + proxyItem.querySelector('.proxy-name').value = config.name; + proxyItem.querySelector('.proxy-type-select').value = config.proxyType; + + updateProxyTypeSettings(proxyItem, config); + proxyList.appendChild(proxyItem); + }); +} + +function updateProxyTypeSettings(proxyItem, config) { + const serverSettings = proxyItem.querySelector('.proxy-server-settings'); + const pacSettings = proxyItem.querySelector('.pac-script-settings'); + + if (config.proxyType === 'fixed_server') { + serverSettings.style.display = 'block'; + pacSettings.style.display = 'none'; + + proxyItem.querySelector('.proxy-scheme').value = config.scheme || 'http'; + proxyItem.querySelector('.proxy-host').value = config.host || ''; + proxyItem.querySelector('.proxy-port').value = config.port || ''; + } + else if (config.proxyType === 'pac_script') { + serverSettings.style.display = 'none'; + pacSettings.style.display = 'block'; + + proxyItem.querySelector('.pac-script').value = config.pacScript || ''; + } + else { + serverSettings.style.display = 'none'; + pacSettings.style.display = 'none'; + } +} + +function setupEventListeners() { + // 添加代理按钮 + document.getElementById('addProxy').addEventListener('click', addNewProxy); + + // 代理列表变更事件 + document.getElementById('proxyList').addEventListener('change', handleProxyChange); + + // 删除代理按钮 + document.getElementById('proxyList').addEventListener('click', handleProxyDelete); +} + +async function addNewProxy() { + const newConfig = { + id: Date.now().toString(), + name: '新建代理', + proxyType: 'fixed_server', + scheme: 'http', + host: '127.0.0.1', + port: 8080 + }; + + currentConfigs = [...currentConfigs, newConfig]; + await ProxySettings.importSettings(currentConfigs); + renderProxyConfigs(); +} + +async function handleProxyChange(e) { + const proxyItem = e.target.closest('.proxy-item'); + if (!proxyItem) return; + + const configId = proxyItem.dataset.id; + const configIndex = currentConfigs.findIndex(c => c.id === configId); + if (configIndex === -1) return; + + const updatedConfig = { ...currentConfigs[configIndex] }; + + if (e.target.classList.contains('proxy-name')) { + updatedConfig.name = e.target.value; + } else if (e.target.classList.contains('proxy-type-select')) { + updatedConfig.proxyType = e.target.value; + // 如果切换到固定服务器模式,设置默认值 + if (updatedConfig.proxyType === 'fixed_server' && !updatedConfig.scheme) { + updatedConfig.scheme = 'http'; + updatedConfig.host = '127.0.0.1'; + updatedConfig.port = 8080; + } + } else if (e.target.classList.contains('proxy-scheme')) { + updatedConfig.scheme = e.target.value; + } else if (e.target.classList.contains('proxy-host')) { + updatedConfig.host = e.target.value; + } else if (e.target.classList.contains('proxy-port')) { + updatedConfig.port = parseInt(e.target.value) || ''; + } else if (e.target.classList.contains('pac-script')) { + updatedConfig.pacScript = e.target.value; + } + + currentConfigs[configIndex] = updatedConfig; + await ProxySettings.importSettings(currentConfigs); + + // 如果代理类型改变,需要更新UI + if (e.target.classList.contains('proxy-type-select')) { + updateProxyTypeSettings(proxyItem, updatedConfig); + } +} + +async function handleProxyDelete(e) { + const deleteBtn = e.target.closest('.delete-btn'); + if (!deleteBtn) return; + + const proxyItem = deleteBtn.closest('.proxy-item'); + const configId = proxyItem.dataset.id; + + currentConfigs = currentConfigs.filter(c => c.id !== configId); + await ProxySettings.importSettings(currentConfigs); + renderProxyConfigs(); +} + +function showError(message) { + const container = document.querySelector('.container'); + const errorDiv = document.createElement('div'); + errorDiv.className = 'error-message'; + errorDiv.style.cssText = ` + color: #ff4d4f; + padding: 8px 16px; + margin-bottom: 16px; + background-color: #fff2f0; + border: 1px solid #ffccc7; + border-radius: 4px; + `; + errorDiv.textContent = message; + container.insertBefore(errorDiv, container.firstChild); + + // 5秒后自动移除错误信息 + setTimeout(() => { + errorDiv.remove(); + }, 5000); +} + +// ... 其他代码保持不变 \ No newline at end of file diff --git a/public/proxy/proxy-auth.js b/public/proxy/proxy-auth.js new file mode 100644 index 0000000..a91d7fd --- /dev/null +++ b/public/proxy/proxy-auth.js @@ -0,0 +1,98 @@ +// 代理认证管理 +export class ProxyAuth { + static async setupAuthListener() { + try { + // 保存认证信息到 storage + const saveAuth = async (config) => { + await chrome.storage.local.set({ + proxyAuth: { + username: config.username, + password: config.password, + timestamp: Date.now() + } + }); + }; + + // 获取认证信息 + const getAuth = async () => { + const result = await chrome.storage.local.get('proxyAuth'); + return result.proxyAuth; + }; + + // 清除认证信息 + const clearAuth = async () => { + await chrome.storage.local.remove('proxyAuth'); + }; + + return { + saveAuth, + getAuth, + clearAuth + }; + } catch (error) { + console.error('Error setting up auth listener:', error); + return null; + } + } + + static setupErrorHandler() { + // 在 Manifest V3 中,我们不能使用 chrome.proxy.onProxyError + // 所以我们只记录错误到 storage + try { + const logError = async (error) => { + const errors = await chrome.storage.local.get('proxyErrors') || []; + errors.push({ + timestamp: Date.now(), + error: error.message || error + }); + await chrome.storage.local.set({ + proxyErrors: errors.slice(-100) // 只保留最近100条错误记录 + }); + }; + + return { logError }; + } catch (error) { + console.error('Error setting up error handler:', error); + return null; + } + } + + // 设置代理认证信息 + static async setProxyAuth(username, password) { + try { + await chrome.storage.local.set({ + proxyAuth: { + username, + password, + timestamp: Date.now() + } + }); + return true; + } catch (error) { + console.error('Error setting proxy auth:', error); + return false; + } + } + + // 获取代理认证信息 + static async getProxyAuth() { + try { + const result = await chrome.storage.local.get('proxyAuth'); + return result.proxyAuth || null; + } catch (error) { + console.error('Error getting proxy auth:', error); + return null; + } + } + + // 清除代理认证信息 + static async clearProxyAuth() { + try { + await chrome.storage.local.remove('proxyAuth'); + return true; + } catch (error) { + console.error('Error clearing proxy auth:', error); + return false; + } + } +} \ No newline at end of file diff --git a/public/proxy/proxy-manager.js b/public/proxy/proxy-manager.js new file mode 100644 index 0000000..f35cf24 --- /dev/null +++ b/public/proxy/proxy-manager.js @@ -0,0 +1,87 @@ +// 代理配置管理 +export class ProxyManager { + static async setProxy(config) { + try { + const proxyConfig = { + mode: "fixed_servers", + rules: { + singleProxy: { + scheme: config.scheme, + host: config.host, + port: config.port + }, + bypassList: ["localhost", "127.0.0.1"] + } + }; + + await chrome.proxy.settings.set({ + value: proxyConfig, + scope: 'regular' + }); + + // 保存当前配置 + await chrome.storage.local.set({ + currentProxy: { + ...config, + timestamp: Date.now() + } + }); + + return true; + } catch (error) { + console.error('Error setting proxy:', error); + return false; + } + } + + static async clearProxy() { + try { + await chrome.proxy.settings.clear({scope: 'regular'}); + await chrome.storage.local.remove('currentProxy'); + return true; + } catch (error) { + console.error('Error clearing proxy:', error); + return false; + } + } + + static async getProxyStatus() { + try { + const settings = await chrome.proxy.settings.get({}); + const currentProxy = await chrome.storage.local.get('currentProxy'); + return { + enabled: settings.value.mode === "fixed_servers", + config: currentProxy.currentProxy || null + }; + } catch (error) { + console.error('Error getting proxy status:', error); + return { + enabled: false, + config: null + }; + } + } + + static async deleteProxy(proxyId) { + try { + // 获取当前代理配置 + const result = await chrome.storage.local.get(['proxyConfigs', 'currentProxy']); + const configs = result.proxyConfigs || []; + const currentProxy = result.currentProxy; + + // 如果要删除的代理正在使用中,先清除代理设置 + if (currentProxy && currentProxy.id === proxyId) { + await clearProxyConfig(); + } + + // 从配置列表中删除代理 + const updatedConfigs = configs.filter(config => config.id !== proxyId); + await chrome.storage.local.set({ proxyConfigs: updatedConfigs }); + + return true; + } catch (error) { + console.error('Error deleting proxy:', error); + return false; + } + } +} \ No newline at end of file diff --git a/public/proxy/proxy-settings.js b/public/proxy/proxy-settings.js new file mode 100644 index 0000000..6c07b94 --- /dev/null +++ b/public/proxy/proxy-settings.js @@ -0,0 +1,41 @@ +// 代理配置存储和管理 +export const ProxySettings = { + async importSettings(settings) { + try { + if (Array.isArray(settings) && settings.every(s => s.proxyType)) { + await chrome.storage.local.set({proxyConfigs: settings}); + return {success: true}; + } + return {success: false, error: "Invalid settings format"}; + } catch (error) { + return {success: false, error: error.message}; + } + }, + + async exportSettings() { + try { + const {proxyConfigs} = await chrome.storage.local.get('proxyConfigs'); + return {success: true, settings: proxyConfigs || []}; + } catch (error) { + return {success: false, error: error.message}; + } + }, + + async setDefaultConfigs() { + const result = await chrome.storage.local.get('proxyConfigs'); + if (!result.proxyConfigs) { + const defaultConfigs = [{ + id: 'direct', + name: '直接连接', + proxyType: 'direct', + enabled: false + }]; + await chrome.storage.local.set({ proxyConfigs: defaultConfigs }); + } + // 确保 proxyLogs 存在 + const logsResult = await chrome.storage.local.get('proxyLogs'); + if (!logsResult.proxyLogs) { + await chrome.storage.local.set({ proxyLogs: [] }); + } + } +}; \ No newline at end of file diff --git a/public/sidepanel.html b/public/sidepanel.html new file mode 100644 index 0000000..ac4cc51 --- /dev/null +++ b/public/sidepanel.html @@ -0,0 +1,10 @@ + + + + My Sidepanel + + +

All sites sidepanel extension

+

This side panel is enabled on all sites

+ + \ No newline at end of file diff --git a/public/socket.js b/public/socket.js index c60795d..a80344a 100644 --- a/public/socket.js +++ b/public/socket.js @@ -8,6 +8,7 @@ export const ActionType = { CLEAR_PROXY: 'clear_proxy', INJECT_SCRIPT: 'yakit_inject_script', TO_EXTENSION_PAGE: "yakit_to_extension_page", + BADGE_COUNT: "yakit_badge", } export class WebSocketManager { @@ -22,7 +23,7 @@ export class WebSocketManager { this.socket.onopen = () => { chrome.runtime.sendMessage({action: ActionType.STATUS, connected: true, port: port}); - // this.startHeartbeat(); + this.startHeartbeat(); }; this.socket.onmessage = (event) => { @@ -66,7 +67,7 @@ export class WebSocketManager { } startHeartbeat() { - this.intervalId = setInterval(() => this.heartbeat(), 3000); + this.intervalId = setInterval(() => this.heartbeat(), 25000); } stopHeartbeat() { diff --git a/public/types/action.js b/public/types/action.js new file mode 100644 index 0000000..59f9dbf --- /dev/null +++ b/public/types/action.js @@ -0,0 +1,5 @@ +export const ProxyActionType = { + SET_PROXY_CONFIG: "SET_PROXY_CONFIG", + CLEAR_PROXY_CONFIG: "CLEAR_PROXY_CONFIG", + GET_PROXY_STATUS: "GET_PROXY_STATUS" +}; \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 58826e2..36d25d2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import {ConfigProvider} from "antd"; import {Contro} from "@components/Contro"; import {Proxifier} from "@components/Proxifier"; import {EvalInTab} from "@components/EvalInTab"; +import {ProxySwitch} from "@components/ProxySwitch"; function App() { return ( @@ -15,10 +16,11 @@ function App() { }} >
- - {/**/} + {/**/} + {/* */} + - + {/* */}
); diff --git a/src/components/EvalInTab.tsx b/src/components/EvalInTab.tsx index 3eb1a6c..e094e75 100644 --- a/src/components/EvalInTab.tsx +++ b/src/components/EvalInTab.tsx @@ -32,6 +32,7 @@ export const EvalInTab: React.FC = () => { value: {mode: "CONTENT_CALL_FUNCTION", fn_name: funcName, args: inputArgsData}, }); } catch (error) { + console.log("error:", error) } }; diff --git a/src/components/ProxySwitch/index.css b/src/components/ProxySwitch/index.css new file mode 100644 index 0000000..6b12f83 --- /dev/null +++ b/src/components/ProxySwitch/index.css @@ -0,0 +1,68 @@ +.proxy-switch { + padding: 16px; + background: #fff; + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.proxy-switch-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.proxy-switch .ant-select { + margin-right: 8px; +} + +.proxy-switch .ant-space { + gap: 8px; +} + +.proxy-switch-title { + display: flex; + align-items: center; +} + +.proxy-switch-title-text { + color: #31343F; + font-size: 14px; + font-weight: 600; + margin-right: 8px; +} + +.proxy-switch-content { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + background: #F8F8F8; + border-radius: 4px; +} + +.proxy-switch-info { + display: flex; + flex-direction: column; + gap: 4px; +} + +.proxy-switch-status { + color: #85899E; + font-size: 12px; +} + +.proxy-switch-proxy { + color: #31343F; + font-size: 13px; +} + +.proxy-switch-status-active { + color: var(--yakit-primary); +} + +.proxy-switch-actions { + display: flex; + align-items: center; + gap: 8px; +} \ No newline at end of file diff --git a/src/components/ProxySwitch/index.tsx b/src/components/ProxySwitch/index.tsx new file mode 100644 index 0000000..608df67 --- /dev/null +++ b/src/components/ProxySwitch/index.tsx @@ -0,0 +1,288 @@ +import React, {useEffect, useState} from "react"; +import {Select, Button, Tooltip, Modal, Form, Input, Radio, Space, message} from "antd"; +import {PlusOutlined, SettingOutlined} from "@ant-design/icons"; +import {ProxyConfig} from "@/types/proxy"; +import {StorageChanges} from "@/types/chrome"; +import "./index.css"; + +export const ProxySwitch: React.FC = () => { + const [currentMode, setCurrentMode] = useState("direct"); + const [proxyConfigs, setProxyConfigs] = useState([]); + const [isModalVisible, setIsModalVisible] = useState(false); + const [form] = Form.useForm(); + const [proxyHost, setProxyHost] = useState(''); + const [proxyPort, setProxyPort] = useState(''); + + useEffect(() => { + loadConfigs().catch(error => { + console.error("Failed to load configs:", error); + // 可以在这里添加错误提示 + }); + }, []); + + useEffect(() => { + if (proxyConfigs.length > 0) { + applyProxySettings(currentMode).catch(error => { + console.error("Failed to apply proxy settings:", error); + // 可以在这里添加错误提示 + }); + } + }, [proxyConfigs, currentMode]); + + useEffect(() => { + const handleStorageChange = (changes: StorageChanges) => { + if (changes.proxyConfigs) { + const newConfigs = changes.proxyConfigs.newValue; + setProxyConfigs(newConfigs); + const currentConfig = newConfigs.find((c: ProxyConfig) => c.id === currentMode); + if (!currentConfig || !currentConfig.enabled) { + setCurrentMode("direct"); + } + } + }; + chrome.storage.onChanged.addListener(handleStorageChange); + return () => chrome.storage.onChanged.removeListener(handleStorageChange); + }, [currentMode]); + + const loadConfigs = async () => { + try { + const result = await chrome.storage.local.get('proxyConfigs'); + if (!result.proxyConfigs) { + const defaultConfigs: ProxyConfig[] = [ + { + id: "direct", + name: "直接连接", + proxyType: "direct" as const, + enabled: true + }, + { + id: "default", + name: "默认代理", + proxyType: "fixed_server" as const, + host: "127.0.0.1", + port: 8080, + scheme: "http", + enabled: false + } + ]; + await chrome.storage.local.set({proxyConfigs: defaultConfigs}); + setProxyConfigs(defaultConfigs); + setCurrentMode("direct"); + } else { + setProxyConfigs(result.proxyConfigs); + const currentConfig = result.proxyConfigs.find((c: ProxyConfig) => c.id === currentMode); + if (!currentConfig || !currentConfig.enabled) { + setCurrentMode("direct"); + } + } + return true; + } catch (error) { + console.error("Error in loadConfigs:", error); + throw error; + } + }; + + const applyProxySettings = async (modeId: string) => { + try { + const config = proxyConfigs.find((c: ProxyConfig) => c.id === modeId); + if (!config || !config.enabled) return false; + + if (config.proxyType === "direct") { + chrome.proxy.settings.clear({scope: 'regular'}); + return true; + } else if (config.proxyType === "fixed_server") { + chrome.proxy.settings.set({ + value: { + mode: "fixed_servers", + rules: { + singleProxy: { + scheme: config.scheme, + host: config.host, + port: Number(config.port) + }, + bypassList: ["<-loopback>"] + } + }, + scope: "regular" + }); + return true; + } else if (config.proxyType === "pac_script") { + chrome.proxy.settings.set({ + value: { + mode: "pac_script", + pacScript: { + data: config.pacScript + } + }, + scope: "regular" + }); + return true; + } + return false; + } catch (error) { + console.error("Failed to apply proxy settings:", error); + throw error; + } + }; + + const handleAddProxy = () => { + form.resetFields(); + setIsModalVisible(true); + }; + + const handleModalOk = () => { + form.validateFields().then(async values => { + try { + const newConfig: ProxyConfig = { + id: Date.now().toString(), + name: values.name, + proxyType: values.proxyType as "direct" | "fixed_server" | "pac_script" | "auto_detect", + host: values.host, + port: values.port ? parseInt(values.port) : undefined, + scheme: values.scheme, + enabled: true, + }; + const updatedConfigs = [...proxyConfigs, newConfig]; + await chrome.storage.local.set({proxyConfigs: updatedConfigs}); + setProxyConfigs(updatedConfigs); + setIsModalVisible(false); + } catch (error) { + console.error("Failed to save new proxy config:", error); + // 可以在这里添加错误提示 + } + }); + }; + + const handleSettingClick = () => { + if (chrome.runtime.openOptionsPage) { + chrome.runtime.openOptionsPage(); + } else { + chrome.tabs.create({ + url: chrome.runtime.getURL('proxy/options.html') + }); + } + }; + + const handleApplyProxy = async () => { + try { + const response = await chrome.runtime.sendMessage({ + action: 'SET_PROXY_CONFIG', + config: { + host: proxyHost, + port: parseInt(proxyPort), + scheme: 'http', + proxyType: 'fixed_server', + id: 'default', + name: '默认代理' + } + }); + + if (response.success) { + message.success('代理设置已应用'); + } else { + message.error(response.error || '代理设置失败'); + } + } catch (error) { + console.error('Error applying proxy:', error); + message.error('应用代理设置时发生错误'); + } + }; + + return ( +
+
+ + + + + 直接连接 + 代理服务器 + PAC 脚本 + + + prev.proxyType !== curr.proxyType} + > + {({getFieldValue}) => + getFieldValue('proxyType') === 'fixed_server' && ( + <> + + + + + + + + + + + ) + } + + + +
+ ); +}; \ No newline at end of file diff --git a/src/pages/OptionsPage/index.css b/src/pages/OptionsPage/index.css new file mode 100644 index 0000000..ca5208c --- /dev/null +++ b/src/pages/OptionsPage/index.css @@ -0,0 +1,100 @@ +.options-page { + min-height: 100vh; + background: #f0f2f5; +} + +.options-page .ant-layout-content { + padding: 24px; +} + +.proxy-tabs { + background: #fff; + padding: 24px; + border-radius: 8px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.options-page .ant-tabs-nav::before { + border-bottom-color: #f0f0f0; +} + +.options-page .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn { + color: var(--yakit-primary); +} + +.options-page .ant-tabs-ink-bar { + background: var(--yakit-primary); +} + +.options-page .ant-card { + margin-bottom: 16px; + border-radius: 4px; + border: 1px solid #f0f0f0; +} + +.options-page .ant-card-head { + border-bottom: none; + min-height: 48px; +} + +.options-page .ant-card-body { + padding: 16px; +} + +.options-page .ant-btn-primary { + background: var(--yakit-primary); + border-color: var(--yakit-primary); +} + +.options-page .ant-btn-primary:hover { + background: var(--yakit-primary-hover); + border-color: var(--yakit-primary-hover); +} + +.proxy-action-btn.ant-btn-default { + color: rgba(0, 0, 0, 0.65); + border-color: #d9d9d9; + background: #fff; +} + +.proxy-action-btn.ant-btn-default:hover { + color: var(--yakit-primary); + border-color: var(--yakit-primary); +} + +.proxy-action-btn.ant-btn-primary { + color: #fff; + background: var(--yakit-primary); + border-color: var(--yakit-primary); +} + +.proxy-action-btn.ant-btn-primary.ant-btn-dangerous { + background: #ff4d4f; + border-color: #ff4d4f; +} + +.proxy-action-btn.ant-btn-primary.ant-btn-dangerous:hover { + background: #ff7875; + border-color: #ff7875; +} + +.options-page .ant-table-wrapper { + background: #fff; + padding: 24px; + border-radius: 8px; +} + +.options-page .ant-input:focus, +.options-page .ant-input-focused { + border-color: var(--yakit-primary); + box-shadow: 0 0 0 2px var(--yakit-primary-5); +} + +.options-page .ant-select-focused .ant-select-selector { + border-color: var(--yakit-primary) !important; + box-shadow: 0 0 0 2px var(--yakit-primary-5) !important; +} + +.options-page .ant-select-item-option-selected:not(.ant-select-item-option-disabled) { + background-color: var(--yakit-primary-5); +} \ No newline at end of file diff --git a/src/pages/OptionsPage/index.tsx b/src/pages/OptionsPage/index.tsx new file mode 100644 index 0000000..8418e1a --- /dev/null +++ b/src/pages/OptionsPage/index.tsx @@ -0,0 +1,385 @@ +import React, { useEffect, useState } from "react"; +import { Layout, Button, Card, Input, Select, InputNumber, Space, Typography, Modal, Switch, Tabs, Table, Form, App } from "antd"; +import { PlusOutlined, DeleteOutlined, ImportOutlined, ExportOutlined } from "@ant-design/icons"; +import { ProxyConfig } from "@/types/proxy"; +import { StorageChanges } from "@/types/chrome"; +import './index.css'; +import { ProxyActionType } from '@/types/action'; + +const { Header, Content } = Layout; +const { Title } = Typography; +const { TextArea } = Input; + +const headerStyle = { + background: '#fff', + padding: '0 24px', + borderBottom: '1px solid #f0f0f0' +}; + +const contentStyle = { + padding: '24px', + background: '#f0f2f5', + minHeight: '100vh' +}; + +const titleStyle = { + margin: '16px 0', + color: '#31343F' +}; + +interface ProxyLog { + id: string; + timestamp: number; + url: string; + proxyId: string; + proxyName: string; + status: 'success' | 'error'; + errorMessage?: string; +} + +export const OptionsPage: React.FC = () => { + const { message } = App.useApp(); + const [proxyConfigs, setProxyConfigs] = useState([]); + const [proxyLogs, setProxyLogs] = useState([]); + const [activeTab, setActiveTab] = useState('settings'); + const [currentConfigId, setCurrentConfigId] = useState(''); + + useEffect(() => { + loadConfigs(); + }, []); + + const loadConfigs = async () => { + const result = await chrome.storage.local.get('proxyConfigs'); + setProxyConfigs(result.proxyConfigs || []); + }; + + const handleAddProxy = () => { + const newConfig: ProxyConfig = { + id: Date.now().toString(), + name: '新建代理', + proxyType: 'fixed_server', + scheme: 'http', + host: '127.0.0.1', + port: 8080, + enabled: false + }; + const updatedConfigs = [...proxyConfigs, newConfig]; + chrome.storage.local.set({ proxyConfigs: updatedConfigs }); + setProxyConfigs(updatedConfigs); + }; + + const handleConfigChange = (configId: string, field: keyof ProxyConfig, value: any) => { + const updatedConfigs = proxyConfigs.map(config => { + if (config.id === configId) { + return { ...config, [field]: value }; + } + return config; + }); + chrome.storage.local.set({ proxyConfigs: updatedConfigs }); + setProxyConfigs(updatedConfigs); + }; + + const handleDeleteProxy = (configId: string) => { + Modal.confirm({ + title: '确认删除', + content: '确定要删除这个代理配置吗?', + onOk: () => { + const updatedConfigs = proxyConfigs.filter(config => config.id !== configId); + chrome.storage.local.set({ proxyConfigs: updatedConfigs }); + setProxyConfigs(updatedConfigs); + } + }); + }; + + useEffect(() => { + const handleStorageChange = (changes: StorageChanges) => { + if (changes.proxyConfigs) { + setProxyConfigs(changes.proxyConfigs.newValue || []); + } + }; + + chrome.storage.onChanged.addListener(handleStorageChange); + return () => chrome.storage.onChanged.removeListener(handleStorageChange); + }, []); + + useEffect(() => { + const loadLogs = async () => { + const result = await chrome.storage.local.get('proxyLogs'); + setProxyLogs(result.proxyLogs || []); + }; + loadLogs(); + + const handleStorageChange = (changes: StorageChanges) => { + if (changes.proxyLogs) { + setProxyLogs(changes.proxyLogs.newValue); + } + }; + chrome.storage.onChanged.addListener(handleStorageChange); + return () => chrome.storage.onChanged.removeListener(handleStorageChange); + }, []); + + const columns = [ + { + title: '时间', + dataIndex: 'timestamp', + key: 'timestamp', + render: (timestamp: number) => new Date(timestamp).toLocaleString() + }, + { + title: 'URL', + dataIndex: 'url', + key: 'url', + ellipsis: true, + render: (url: string) => ( + { + e.preventDefault(); + chrome.tabs.create({ url }); + }} + > + {url} + + ) + }, + { + title: '使用代理', + dataIndex: 'proxyName', + key: 'proxyName', + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + render: (status: string) => ( + + {status === 'success' ? '成功' : '失败'} + + ) + }, + { + title: '错误信息', + dataIndex: 'errorMessage', + key: 'errorMessage', + ellipsis: true, + } + ]; + + const handleApplyConfig = async (configId: string) => { + const config = proxyConfigs.find(c => c.id === configId); + if (config) { + try { + const response = await new Promise((resolve) => { + chrome.runtime.sendMessage({ + action: ProxyActionType.SET_PROXY_CONFIG, + config: { + ...config, + scheme: config.scheme || 'http', + host: config.host || '127.0.0.1', + port: Number(config.port) || 8080, + } + }, resolve); + }); + + if (response && response.success) { + const updatedConfigs = proxyConfigs.map(c => ({ + ...c, + enabled: c.id === configId + })); + await chrome.storage.local.set({ proxyConfigs: updatedConfigs }); + setProxyConfigs(updatedConfigs); + message.success('代理设置已应用'); + } else { + message.error((response && response.error) || '代理设置失败'); + } + } catch (error) { + console.error('Failed to apply proxy config:', error); + message.error('操作失败'); + } + } + }; + + const handleClearProxy = async (configId: string) => { + try { + const response = await new Promise((resolve) => { + chrome.runtime.sendMessage({ + action: ProxyActionType.CLEAR_PROXY_CONFIG + }, resolve); + }); + + if (response && response.success) { + const updatedConfigs = proxyConfigs.map(c => ({ + ...c, + enabled: false + })); + await chrome.storage.local.set({ proxyConfigs: updatedConfigs }); + setProxyConfigs(updatedConfigs); + message.success('代理已取消'); + } else { + message.error((response && response.error) || '取消代理失败'); + } + } catch (error) { + console.error('Error clearing proxy:', error); + message.error('操作失败'); + } + }; + + return ( + + + + + + + + ) + }} + items={[ + { + key: 'settings', + label: '代理设置', + children: ( + + {proxyConfigs.map(config => ( + handleConfigChange(config.id, 'name', e.target.value)} + disabled={config.id === 'direct'} + variant="borderless" + style={{ fontSize: '16px', padding: 0 }} + /> + } + extra={ + + + {config.id !== 'direct' && ( +