浏览器插件开发--通过调用本地nmap实现nmap插件扫描
用途限制声明,本文仅用于网络安全技术研究、教育与知识分享。文中涉及的渗透测试方法与工具,严禁用于未经授权的网络攻击、数据窃取或任何违法活动。任何因不当使用本文内容导致的法律后果,作者及发布平台不承担任何责任。渗透测试涉及复杂技术操作,可能对目标系统造成数据损坏、服务中断等风险。读者需充分评估技术能力与潜在后果,在合法合规前提下谨慎实践。
这次我们主要讲述如何开发浏览器插件,下面我们先讲述开发浏览器插件所需要的配置文件
1. 配置清单文件(必须)
manifest.json
这是插件的「身份证」,是所有浏览器插件的核心配置文件,用于声明插件的基本信息、权限、资源路径等。
不同浏览器可能要求不同的版本(如 Chrome 主推 manifest V3
,Firefox 兼容 V2 和 V3),主要内容包括:
- 插件名称、版本、描述、图标路径
- 权限声明(如访问标签页、存储、网络请求等)
- 核心脚本 / 页面的路径(如背景脚本、popup 页面、内容脚本等)
- 其他配置(如跨域请求白名单、web 可访问资源等)
示例片段(V3):
{"manifest_version": 3,"name": "我的插件","version": "1.0","permissions": ["storage", "tabs"],"background": { "service_worker": "background.js" },"action": { "default_popup": "popup.html" },"content_scripts": [{"matches": ["<all_urls>"],"js": ["content.js"]}]
}
2. 背景脚本 / 服务(核心逻辑)
负责插件的后台任务(如事件监听、全局状态管理、定时任务等),生命周期与浏览器一致。
- Manifest V2:使用
background.html
(页面)或background.js
(脚本),可持久运行。 - Manifest V3:改用
service_worker
(如background.js
),无 DOM 访问能力,非持久化(事件触发时激活)。
功能:监听浏览器事件(如标签页切换、网络请求)、跨脚本通信、数据持久化等。
3. 内容脚本(与网页交互)
content.js
(可自定义名称)
注入到匹配的网页中,用于操作网页 DOM、获取页面数据或修改页面样式,是插件与目标网页交互的桥梁。
特点:
- 运行在网页上下文,但与网页自身脚本隔离(有独立作用域)。
- 可访问部分浏览器 API(如
chrome.runtime
),但权限有限(如不能直接访问tabs
API)。 - 需要在
manifest.json
中通过content_scripts
声明注入规则(如匹配的网址、注入时机)。
4. 交互界面文件(可选,根据需求)
-
Popup 页面:点击插件图标时显示的小窗口(如快捷操作面板),由
popup.html
+popup.css
+popup.js
组成。
需在manifest.json
的action.default_popup
中声明路径。 -
选项页面(Options Page):用于用户配置插件参数(如开关、偏好设置),由
options.html
+ 对应的 CSS/JS 组成。
需在manifest.json
中通过options_ui
声明。 -
其他页面:如独立的设置页、帮助页等,按需添加。
5. 静态资源
- 图标文件:不同尺寸的图标(如 16x16、48x48、128x128),用于插件图标、浏览器工具栏、应用商店展示等,需在
manifest.json
的icons
中声明。 - 样式文件:
*.css
用于美化 popup、选项页或内容脚本注入的样式。 - 图片 / 字体:插件界面中用到的图片(png、svg 等)、自定义字体等静态资源。
6. 其他辅助文件(按需)
- 消息通信脚本:处理不同脚本间的通信(如 content.js 与 background.js 交互)。
- 数据存储相关:若需本地存储,可通过浏览器的
chrome.storage
API 实现,无需额外文件;若需复杂逻辑,可能需要封装存储工具类(如storage.js
)。 - 第三方库:如 jQuery、Vue 等(需注意 V3 对 eval 等特性的限制,避免使用不兼容的库)。
此次插件完整的文件结构如下
nmap-browser-plugin/
├── manifest.json # 插件配置
├── popup.html # 主界面
├── popup.js # 界面交互逻辑
├── options.html # 设置页面
├── options.js # 设置页面逻辑
└── local_nmap_server.py # 本地服务端
这里我们通过调用本地服务来构建nmap扫描插件,但是浏览器插件运行在沙箱环境中,出于安全限制,无法直接访问本地可执行文件,所以说我们需要通过「间接方式」实现,核心思路是:浏览器插件 ↔ 本地服务 ↔ nmap,即通过一个本地后台服务作为中间层,插件通过网络请求调用该服务,再由服务执行 nmap
并返回结果。
使用这插件的必要条件,就是要将nmap加入至环境变量,能够直接通过命令行来调用nmap,实例流程如下
首先下载nmap--window版本,Download the Free Nmap Security Scanner for Linux/Mac/Windows
下载安装完成后,我们就可以将nmap的绝对路径加入至环境变量的Path,比如我的nmap路径是(C:\Program Files (x86)\Nmap),如图所示,这样就算成功
准备工具完成,接下来就是使用python构建本地服务来作为中间层进行流量转发,代码如下
import os
import re
import sys
import json
import ctypes
import subprocess
import threading
from wsgiref.simple_server import make_server
from websockets import serve, WebSocketServerProtocol
import asyncio# 全局变量存储当前扫描进程
current_process = None
is_scanning = False# 检查是否为管理员/root权限
def is_admin():try:if os.name == 'nt': # Windowsreturn ctypes.windll.shell32.IsUserAnAdmin() != 0else: # Linux/macOSreturn os.geteuid() == 0except:return False# 验证目标是否安全
def is_safe_target(target):# 允许IP(IPv4)、域名(含字母、数字、.、-)ip_pattern = r'^(\d{1,3}\.){3}\d{1,3}$'domain_pattern = r'^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'return re.match(ip_pattern, target) or re.match(domain_pattern, target)# 验证参数是否安全
def is_safe_args(args):# 允许nmap常用参数格式safe_pattern = r'^[\-a-zA-Z0-9\s/=:_.,()]+$'return re.match(safe_pattern, args) is not None# 检查是否需要管理员权限
def needs_admin_privileges(args):# 需要管理员权限的参数列表privileged_args = {'-sS', '-sT', '-sU', '-O', '-A', '--script=firewall', '--script=vuln'}return any(arg in privileged_args for arg in args.split())# 执行nmap扫描
async def run_nmap(websocket, target, args, nmap_path):global current_process, is_scanningtry:# 构建nmap命令cmd = []# 添加nmap路径(如果提供)if nmap_path and os.path.exists(nmap_path):cmd.append(nmap_path)else:# 使用系统默认nmap命令cmd.append('nmap.exe' if os.name == 'nt' else 'nmap')# 添加参数if args:cmd.extend(args.split())# 添加目标cmd.append(target)# 检查是否需要管理员权限if needs_admin_privileges(args) and not is_admin():await websocket.send(json.dumps({'type': 'permission_warning','message': '此扫描需要管理员权限,部分功能可能受限。请以管理员身份重启服务以获得完整功能。'}))# 启动扫描进程current_process = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.STDOUT, # 合并stderr到stdouttext=True,bufsize=1,universal_newlines=True)is_scanning = True# 实时读取输出并发送到客户端for line in current_process.stdout:if not is_scanning: # 检查是否需要停止breakawait websocket.send(json.dumps({'type': 'output','content': line.strip()}))# 等待进程结束current_process.wait()exit_code = current_process.returncode# 发送扫描完成消息await websocket.send(json.dumps({'type': 'complete','success': exit_code == 0,'exit_code': exit_code}))except Exception as e:await websocket.send(json.dumps({'type': 'error','message': str(e)}))finally:# 清理状态current_process = Noneis_scanning = False# 处理WebSocket连接
async def handle_connection(websocket: WebSocketServerProtocol):global is_scanningasync for message in websocket:try:data = json.loads(message)if data['type'] == 'start_scan' and not is_scanning:# 验证目标和参数target = data.get('target', '').strip()args = data.get('args', '').strip()nmap_path = data.get('nmapPath', '').strip()if not target:await websocket.send(json.dumps({'type': 'error','message': '请提供目标IP或域名'}))continueif not is_safe_target(target):await websocket.send(json.dumps({'type': 'error','message': '目标格式不合法(仅支持IP或域名)'}))continueif args and not is_safe_args(args):await websocket.send(json.dumps({'type': 'error','message': '参数包含危险字符,请检查'}))continue# 启动扫描(在新线程中执行)asyncio.create_task(run_nmap(websocket, target, args, nmap_path))elif data['type'] == 'stop_scan' and is_scanning and current_process:# 终止扫描进程if os.name == 'nt':# Windows系统下需要终止整个进程树subprocess.run(['taskkill', '/F', '/T', '/PID', str(current_process.pid)])else:# Linux/macOS系统下终止进程current_process.terminate()is_scanning = Falseawait websocket.send(json.dumps({'type': 'output','content': '扫描已被用户终止'}))await websocket.send(json.dumps({'type': 'complete','success': False}))except Exception as e:await websocket.send(json.dumps({'type': 'error','message': f'处理请求时出错: {str(e)}'}))# 启动WebSocket服务器
async def start_server():print("Nmap本地服务已启动,监听 ws://127.0.0.1:8080")print("注意:扫描需要管理员权限才能使用全部功能")async with serve(handle_connection, "127.0.0.1", 8080):await asyncio.Future() # 运行永久if __name__ == "__main__":try:asyncio.run(start_server())except KeyboardInterrupt:print("\n服务已停止")sys.exit(0)except Exception as e:print(f"服务启动失败: {str(e)}")sys.exit(1)
这里代码负责接收插件的请求、调用 nmap
命令、返回执行结果。
直接通过终端来运行此python脚本,可以通过管理员运行,功能更加强大
接下来就是插件的配置文件
manifest.json--插件配置
{"manifest_version": 3,"name": "Nmap网络扫描工具","version": "1.0.0","description": "通过浏览器界面调用本地Nmap进行网络扫描","permissions": ["activeTab","storage"],"action": {"default_popup": "popup.html"}},"options_ui": {"page": "options.html","open_in_tab": false},"host_permissions": ["http://127.0.0.1:8080/*","ws://127.0.0.1:8080/*"],
}
popup.html--主页面
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><style>:root {--bg-primary: #12121b;--bg-secondary: #1e1e2e;--text-primary: #e0e0e0;--accent: #7e57c2;--accent-hover: #9c78e1;--border: #4a4a68;--warning: #ff9800;--danger: #e53935;--success: #43a047;}body {font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;background-color: var(--bg-primary);color: var(--text-primary);padding: 16px;margin: 0;width: 500px;min-height: 450px;}.header {display: flex;justify-content: space-between;align-items: center;margin-bottom: 16px;}h3 {color: var(--accent);margin: 0;font-size: 1.5rem;text-shadow: 0 0 8px rgba(126, 87, 194, 0.5);}.settings-btn {background: none;border: none;color: var(--text-primary);cursor: pointer;font-size: 1.2rem;padding: 4px;box-shadow: none;}.settings-btn:hover {color: var(--accent);box-shadow: none;}.warning-note {background-color: rgba(255, 152, 0, 0.1);border-left: 3px solid var(--warning);padding: 8px 12px;margin-bottom: 16px;font-size: 0.85rem;color: #f0f0c0;}.form-group {margin-bottom: 16px;display: flex;flex-direction: column;}label {margin-bottom: 4px;font-size: 0.9rem;color: #b0b0c0;}input, select {padding: 8px 12px;border: 1px solid var(--border);border-radius: 6px;background-color: var(--bg-secondary);color: var(--text-primary);font-size: 1rem;transition: border-color 0.3s, box-shadow 0.3s;}input:focus, select:focus {outline: none;border-color: var(--accent);box-shadow: 0 0 0 3px rgba(126, 87, 194, 0.2);}.button-group {display: flex;gap: 10px;margin-bottom: 16px;}button {padding: 10px 16px;background: linear-gradient(135deg, var(--accent), #5c3d9a);color: white;border: none;border-radius: 6px;font-size: 1rem;cursor: pointer;transition: transform 0.2s, box-shadow 0.3s;box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 0 0 2px rgba(126, 87, 194, 0.2);flex: 1;}button.secondary {background: linear-gradient(135deg, #424242, #212121);}button.danger {background: linear-gradient(135deg, var(--danger), #c62828);}button:hover:not(:disabled) {box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15), 0 0 0 3px rgba(126, 87, 194, 0.3);}button.secondary:hover:not(:disabled) {box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15), 0 0 0 3px rgba(100, 100, 100, 0.3);}button:active:not(:disabled) {transform: scale(0.98);}button:disabled {opacity: 0.7;cursor: not-allowed;box-shadow: none;}.result-container {position: relative;}#result {background-color: var(--bg-secondary);padding: 12px;border-radius: 6px;margin-top: 16px;font-family: 'Courier New', monospace;font-size: 0.9rem;line-height: 1.5;max-height: 250px;overflow-y: auto;border: 1px solid var(--border);white-space: pre-wrap;}.history-btn {position: absolute;top: 2px;right: 2px;background: rgba(30, 30, 46, 0.8);border: 1px solid var(--border);color: var(--text-primary);font-size: 0.8rem;padding: 2px 6px;border-radius: 3px;cursor: pointer;z-index: 10;}.history-btn:hover {background: rgba(126, 87, 194, 0.2);}#result::-webkit-scrollbar {width: 6px;}#result::-webkit-scrollbar-track {background: var(--bg-primary);border-radius: 3px;}#result::-webkit-scrollbar-thumb {background: var(--accent);border-radius: 3px;}#result::-webkit-scrollbar-thumb:hover {background: var(--accent-hover);}.preset-tags {display: flex;flex-wrap: wrap;gap: 8px;margin-bottom: 12px;}.preset-tag {background-color: var(--bg-secondary);border: 1px solid var(--border);border-radius: 4px;padding: 4px 8px;font-size: 0.8rem;cursor: pointer;}.preset-tag:hover {background-color: rgba(126, 87, 194, 0.2);border-color: var(--accent);}.status-bar {font-size: 0.8rem;color: #b0b0c0;margin-top: 8px;display: flex;justify-content: space-between;}</style>
</head>
<body><div class="header"><h3>Nmap网络扫描工具</h3><button class="settings-btn" id="settingsBtn">⚙️</button></div><div class="warning-note">⚠️ 注意:仅可扫描您拥有合法权限的网络/设备,未经授权的扫描可能违反法律法规。</div><div class="form-group"><label>目标IP/域名:</label><input type="text" id="target" placeholder="例如:192.168.1.1 或 example.com"></div><div class="form-group"><label>扫描参数(预设):</label><div class="preset-tags"><div class="preset-tag" data-args="-sV -p 1-100">基础端口扫描</div><div class="preset-tag" data-args="-sS -O">高级扫描(需管理员)</div><div class="preset-tag" data-args="-sV -sC -p-">全面扫描(耗时久)</div><div class="preset-tag" data-args="">清空参数</div></div></div><div class="form-group"><label>自定义参数:</label><input type="text" id="args" placeholder="例如:-sV -p 1-100(可覆盖预设)"></div><div class="button-group"><button id="scanBtn">开始扫描</button><button id="stopBtn" class="danger" disabled>终止扫描</button><button id="clearBtn" class="secondary">清空结果</button></div><div class="status-bar"><span id="status">就绪</span><span id="duration">耗时:0s</span></div><div class="result-container"><pre id="result"></pre><button class="history-btn" id="historyBtn">历史</button></div><script src="popup.js"></script>
</body>
</html>
popup.js--界面交互逻辑
document.addEventListener('DOMContentLoaded', () => {// DOM元素const targetInput = document.getElementById('target');const argsInput = document.getElementById('args');const scanBtn = document.getElementById('scanBtn');const stopBtn = document.getElementById('stopBtn');const clearBtn = document.getElementById('clearBtn');const resultEl = document.getElementById('result');const statusEl = document.getElementById('status');const durationEl = document.getElementById('duration');const settingsBtn = document.getElementById('settingsBtn');const historyBtn = document.getElementById('historyBtn');const presetTags = document.querySelectorAll('.preset-tag');// 状态变量let socket;let isScanning = false;let startTime;let durationTimer;let scanHistory = [];// 加载配置和历史记录loadSettings();loadHistory();// 预设参数标签点击事件presetTags.forEach(tag => {tag.addEventListener('click', () => {argsInput.value = tag.getAttribute('data-args');});});// 开始扫描scanBtn.addEventListener('click', async () => {const target = targetInput.value.trim();let args = argsInput.value.trim();// 输入验证if (!target) {showResult('❌ 请输入目标IP或域名');return;}// 防止重复扫描if (isScanning) return;// 准备扫描isScanning = true;updateUIState(true);showResult('📡 正在连接到本地服务...\n');statusEl.textContent = '扫描中';// 记录开始时间startTime = Date.now();startDurationTimer();// 获取nmap路径配置const { nmapPath } = await getSettings();const serverUrl = 'ws://127.0.0.1:8080';try {// 连接WebSocketsocket = new WebSocket(serverUrl);socket.onopen = () => {showResult('✅ 已连接到本地服务,开始扫描...\n\n');// 发送扫描请求socket.send(JSON.stringify({type: 'start_scan',target,args,nmapPath}));};socket.onmessage = (event) => {try {const data = JSON.parse(event.data);if (data.type === 'output') {appendResult(data.content + '\n');} else if (data.type === 'error') {appendResult(`\n❌ 错误: ${data.message}\n`);} else if (data.type === 'complete') {const status = data.success ? '✅ 扫描完成' : '❌ 扫描失败';appendResult(`\n${status}\n`);saveToHistory(target, args, new Date().toLocaleString(), data.success);finishScan();} else if (data.type === 'permission_warning') {appendResult(`\n⚠️ 警告: ${data.message}\n`);}} catch (e) {appendResult(`\n⚠️ 数据解析错误: ${e.message}\n`);}};socket.onerror = (error) => {appendResult(`\n❌ 连接错误: ${error.message || '无法连接到本地服务,请确保服务已启动'}\n`);finishScan();};socket.onclose = () => {if (isScanning) {appendResult('\n❌ 连接已关闭\n');finishScan();}};} catch (error) {appendResult(`\n❌ 启动扫描失败: ${error.message}\n`);finishScan();}});// 终止扫描stopBtn.addEventListener('click', () => {if (socket && isScanning) {socket.send(JSON.stringify({ type: 'stop_scan' }));appendResult('\n⏹️ 正在终止扫描...\n');stopBtn.disabled = true;}});// 清空结果clearBtn.addEventListener('click', () => {resultEl.textContent = '';});// 打开设置页面settingsBtn.addEventListener('click', () => {chrome.runtime.openOptionsPage();});// 显示历史记录historyBtn.addEventListener('click', () => {if (scanHistory.length === 0) {showResult('暂无扫描历史记录');return;}let historyText = '📜 扫描历史记录:\n\n';scanHistory.forEach((item, index) => {const status = item.success ? '✅' : '❌';historyText += `${index + 1}. [${item.time}] ${status} ${item.target} ${item.args}\n`;});showResult(historyText);});// 辅助函数: 显示结果function showResult(text) {resultEl.textContent = text;scrollToBottom();}// 辅助函数: 追加结果function appendResult(text) {resultEl.textContent += text;scrollToBottom();}// 辅助函数: 滚动到底部function scrollToBottom() {resultEl.scrollTop = resultEl.scrollHeight;}// 辅助函数: 更新UI状态function updateUIState(scanning) {scanBtn.disabled = scanning;stopBtn.disabled = !scanning;targetInput.disabled = scanning;argsInput.disabled = scanning;isScanning = scanning;}// 辅助函数: 完成扫描function finishScan() {clearInterval(durationTimer);updateUIState(false);statusEl.textContent = '就绪';if (socket) {socket.close();socket = null;}}// 辅助函数: 启动耗时计时器function startDurationTimer() {durationTimer = setInterval(() => {const duration = ((Date.now() - startTime) / 1000).toFixed(1);durationEl.textContent = `耗时: ${duration}s`;}, 1000);}// 加载设置function loadSettings() {getSettings().then(settings => {if (settings.defaultTarget) {targetInput.value = settings.defaultTarget;}if (settings.defaultArgs) {argsInput.value = settings.defaultArgs;}});}// 获取设置async function getSettings() {return new Promise(resolve => {chrome.storage.sync.get(['nmapPath', 'defaultTarget', 'defaultArgs'], (result) => {resolve({nmapPath: result.nmapPath || '',defaultTarget: result.defaultTarget || '',defaultArgs: result.defaultArgs || ''});});});}// 加载历史记录function loadHistory() {chrome.storage.sync.get(['scanHistory'], (result) => {scanHistory = result.scanHistory || [];});}// 保存到历史记录function saveToHistory(target, args, time, success) {scanHistory.unshift({ target, args, time, success });// 限制历史记录数量if (scanHistory.length > 20) {scanHistory = scanHistory.slice(0, 20);}chrome.storage.sync.set({ scanHistory });}
});
options.js--设置页面逻辑
document.addEventListener('DOMContentLoaded', () => {const nmapPathInput = document.getElementById('nmapPath');const defaultTargetInput = document.getElementById('defaultTarget');const defaultArgsInput = document.getElementById('defaultArgs');const saveBtn = document.getElementById('saveBtn');const statusMessage = document.getElementById('statusMessage');// 加载保存的设置loadSettings();// 保存设置saveBtn.addEventListener('click', () => {const settings = {nmapPath: nmapPathInput.value.trim(),defaultTarget: defaultTargetInput.value.trim(),defaultArgs: defaultArgsInput.value.trim()};chrome.storage.sync.set(settings, () => {showStatus('设置已保存', 'success');// 2秒后关闭设置页面setTimeout(() => {window.close();}, 2000);});});// 加载设置function loadSettings() {chrome.storage.sync.get(['nmapPath', 'defaultTarget', 'defaultArgs'], (result) => {nmapPathInput.value = result.nmapPath || '';defaultTargetInput.value = result.defaultTarget || '';defaultArgsInput.value = result.defaultArgs || '';});}// 显示状态消息function showStatus(message, type) {statusMessage.textContent = message;statusMessage.className = `status-message ${type}`;statusMessage.style.display = 'block';}
});
options.html--设置页面
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><style>body {font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;background-color: #12121b;color: #e0e0e0;padding: 20px;width: 400px;}h3 {color: #7e57c2;margin-top: 0;}.form-group {margin-bottom: 20px;}label {display: block;margin-bottom: 8px;font-weight: 500;}input {width: 100%;padding: 8px 12px;border: 1px solid #4a4a68;border-radius: 6px;background-color: #1e1e2e;color: #e0e0e0;box-sizing: border-box;}.hint {font-size: 0.85rem;color: #b0b0c0;margin-top: 4px;}button {padding: 10px 16px;background: linear-gradient(135deg, #7e57c2, #5c3d9a);color: white;border: none;border-radius: 6px;cursor: pointer;transition: transform 0.2s;}button:hover {background: linear-gradient(135deg, #9c78e1, #6e4dbf);}button:active {transform: scale(0.98);}.status-message {margin-top: 15px;padding: 10px;border-radius: 4px;display: none;}.success {background-color: rgba(67, 160, 71, 0.2);border: 1px solid #43a047;}.error {background-color: rgba(229, 57, 53, 0.2);border: 1px solid #e53935;}</style>
</head>
<body><h3>设置</h3><div class="form-group"><label for="nmapPath">Nmap可执行文件路径</label><input type="text" id="nmapPath" placeholder="例如: C:\Program Files\Nmap\nmap.exe 或 /usr/bin/nmap"><div class="hint">留空将使用系统默认路径。Windows通常为C:\Program Files\Nmap\nmap.exe,Linux/macOS通常为/usr/bin/nmap。</div></div><div class="form-group"><label for="defaultTarget">默认目标IP/域名</label><input type="text" id="defaultTarget" placeholder="例如: 192.168.1.1"><div class="hint">设置后将自动填充到扫描界面的目标输入框</div></div><div class="form-group"><label for="defaultArgs">默认扫描参数</label><input type="text" id="defaultArgs" placeholder="例如: -sV -p 1-100"><div class="hint">设置后将自动填充到扫描界面的参数输入框</div></div><button id="saveBtn">保存设置</button><div id="statusMessage" class="status-message"></div><script src="options.js"></script>
</body>
</html>
最终成果展示
这个插件的作用不是很大,但是是第一次尝试插件开发,因为经常是想要某些插件但是没有,所以只能自己手搓了。这里也会不定时发一些开发好的插件,敬请期待。我已经将打包好的文件上传,需要的可以自行下载:【免费】nmap.zip-浏览器插件包资源-CSDN下载
使用的话直接选择扩展管理然后选择加载解压缩的扩展直接加载即可