first commit
This commit is contained in:
commit
ba1a91d623
|
|
@ -0,0 +1 @@
|
|||
node_modules
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
🧠 System Overview
|
||||
Frontend (HTML/JS/CSS)
|
||||
|
|
||||
--> /api/getListApp (reads listApp.txt + fetches init from OPNsense)
|
||||
--> /update (applies app_controls to Zenarmor via fetch)
|
||||
|
||||
Backend (Node.js + Express)
|
||||
|
|
||||
--> Puppeteer for headless login
|
||||
--> Session auto-refresh every 5 minutes
|
||||
--> API forwarding with CSRF token + cookie
|
||||
|
||||
📁 Project Structure
|
||||
├── public/
|
||||
│ └── index.html ← Frontend UI
|
||||
├── listApp.txt ← Line-separated JSON data of apps
|
||||
├── server.js ← Express backend + OPNsense bridge
|
||||
├── README.md ← This file
|
||||
|
||||
🛠️ Setup & Run
|
||||
1. Install dependencies
|
||||
npm install
|
||||
⚠️ If using node-fetch@3+, it requires ESM. The script handles this via dynamic import.
|
||||
|
||||
2. Start the server
|
||||
node server.js
|
||||
Login will be performed immediately, then auto-refresh every 5 minutes.
|
||||
|
||||
🔐 Config
|
||||
- Change "SERVER_DOMAIN" in public/index.html
|
||||
- Change "OPENSENCE_DOMAIN" in server.js
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "opnsenseappcontrol",
|
||||
"version": "1.0.0",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"express": "^5.1.0",
|
||||
"fs": "^0.0.1-security",
|
||||
"node-fecth": "^0.0.1-security",
|
||||
"node-fetch": "^2.7.0",
|
||||
"path": "^0.12.7",
|
||||
"puppeteer": "^24.14.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,330 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>App Selector</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: "Segoe UI", sans-serif;
|
||||
padding: 20px;
|
||||
background-color: #f7f9fc;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 15px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 12px;
|
||||
width: 320px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 4px #007bff33;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
margin: 6px 0;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.category-item input[type="checkbox"] {
|
||||
margin-right: 8px;
|
||||
transform: scale(1.1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.children {
|
||||
margin-left: 20px;
|
||||
border-left: 1px dashed #ccc;
|
||||
padding-left: 10px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 14px;
|
||||
border: none;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.quick-filter {
|
||||
margin: 5px 8px 15px 0;
|
||||
padding: 6px 12px;
|
||||
background-color: #f0f0f0;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.quick-filter:hover {
|
||||
background-color: #e0e0e0;
|
||||
border-color: #aaa;
|
||||
color: green;
|
||||
}
|
||||
|
||||
/* Scrollbar đẹp */
|
||||
ul::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
ul::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
ul::-webkit-scrollbar-thumb {
|
||||
background-color: #ccc;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
ul::-webkit-scrollbar-thumb:hover {
|
||||
background: #bbb;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h3>Search Categories</h3>
|
||||
<div>
|
||||
<button class="quick-filter" data-keyword="Facebook">Facebook</button>
|
||||
<button class="quick-filter" data-keyword="Youtube">Youtube</button>
|
||||
<button class="quick-filter" data-keyword="Proxy">Proxy</button>
|
||||
<button class="quick-filter" data-keyword="Gaming">Gaming</button>
|
||||
</div>
|
||||
|
||||
<input type="text" id="searchInput" placeholder="Search..." />
|
||||
<label><input type="checkbox" id="selectAll"/> Block All</label>
|
||||
<button id="applyBtn">Apply</button>
|
||||
<ul id="categoryList"></ul>
|
||||
|
||||
|
||||
<script>
|
||||
const selectedAppIds = new Set();
|
||||
const SERVER_DOMAIN = "http://localhost:3000"
|
||||
let allData = [];
|
||||
let treeData = [];
|
||||
let initAppIds = [];
|
||||
async function fetchData() {
|
||||
const res = await fetch(SERVER_DOMAIN+'/api/getListApp');
|
||||
const list = await res.json();
|
||||
initAppIds = list.init
|
||||
allData = list.data;
|
||||
|
||||
// Move init apps to top
|
||||
allData.sort((a, b) => {
|
||||
const aIsInit = list.init?.includes(a.id);
|
||||
const bIsInit = list.init?.includes(b.id);
|
||||
return aIsInit === bIsInit ? 0 : aIsInit ? -1 : 1;
|
||||
});
|
||||
console.log("allData", allData)
|
||||
// Tạo map để truy cập theo ID
|
||||
const idMap = new Map();
|
||||
allData.forEach(item => idMap.set(item.id, { ...item, children: [] }));
|
||||
|
||||
// Gắn children object thay vì chỉ id
|
||||
idMap.forEach(item => {
|
||||
if (Array.isArray(item.children) && item.children.length > 0) {
|
||||
item.children = item.children.map(id => idMap.get(id)).filter(Boolean);
|
||||
}
|
||||
});
|
||||
|
||||
// Xác định các node gốc (không nằm trong children của bất kỳ ai)
|
||||
const childIds = new Set();
|
||||
idMap.forEach(item => {
|
||||
item.children.forEach(child => childIds.add(child.id));
|
||||
});
|
||||
|
||||
treeData = Array.from(idMap.values()).filter(item => !childIds.has(item.id));
|
||||
render();
|
||||
}
|
||||
|
||||
function buildTree(data, parentId) {
|
||||
return data
|
||||
.filter(item => item.parent_id === parentId)
|
||||
.map(item => ({
|
||||
...item,
|
||||
children: buildTree(data, item.id),
|
||||
}));
|
||||
}
|
||||
|
||||
function render() {
|
||||
const container = document.getElementById('categoryList');
|
||||
container.innerHTML = '';
|
||||
renderTree(treeData, container, initAppIds); // ⬅ truyền thêm init vào
|
||||
}
|
||||
|
||||
function renderTree(tree, container, initIds = []) {
|
||||
tree.forEach(item => {
|
||||
const li = document.createElement("li");
|
||||
li.classList.add("category-item");
|
||||
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.dataset.id = item.id;
|
||||
|
||||
// ✅ Nếu ID nằm trong init => mặc định được chọn
|
||||
if (initIds.includes(item.id)) {
|
||||
checkbox.checked = true;
|
||||
selectedAppIds.add(item.id); // lưu vào set để submit
|
||||
} else {
|
||||
checkbox.checked = selectedAppIds.has(item.id);
|
||||
}
|
||||
|
||||
checkbox.addEventListener("change", (e) => {
|
||||
const id = parseInt(e.target.dataset.id);
|
||||
if (e.target.checked) {
|
||||
selectedAppIds.add(id);
|
||||
} else {
|
||||
selectedAppIds.delete(id);
|
||||
}
|
||||
});
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.appendChild(checkbox);
|
||||
label.appendChild(document.createTextNode(" " + item.name));
|
||||
li.appendChild(label);
|
||||
|
||||
if (item.children.length > 0) {
|
||||
const ul = document.createElement("ul");
|
||||
ul.classList.add("children");
|
||||
renderTree(item.children, ul, initIds); // ⬅ truyền tiếp danh sách init
|
||||
li.appendChild(ul);
|
||||
}
|
||||
|
||||
container.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll(".quick-filter").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
render()
|
||||
const keyword = btn.dataset.keyword;
|
||||
|
||||
document.getElementById("searchInput").value = keyword;
|
||||
|
||||
if (keyword === "Proxy" || keyword ==="Gaming") {
|
||||
// Tìm node có name = "Proxy"
|
||||
const proxyNode = allData.find(item => item.name === keyword);
|
||||
|
||||
if (proxyNode && Array.isArray(proxyNode.children)) {
|
||||
// ⚠️ KHÔNG thay đổi treeData gốc
|
||||
|
||||
// Tạo tạm cây con để render
|
||||
const idMap = new Map();
|
||||
allData.forEach(item => idMap.set(item.id, { ...item, children: [] }));
|
||||
|
||||
// Gắn lại children object
|
||||
idMap.forEach(item => {
|
||||
if (Array.isArray(item.children) && item.children.length > 0) {
|
||||
item.children = item.children.map(id => idMap.get(id)).filter(Boolean);
|
||||
}
|
||||
});
|
||||
|
||||
// Render tạm UI chỉ phần children của Proxy
|
||||
const proxyChildren = proxyNode.children.map(id => idMap.get(id)).filter(Boolean);
|
||||
const container = document.getElementById('categoryList');
|
||||
container.innerHTML = '';
|
||||
renderTree(proxyChildren, container, initAppIds); // render tạm
|
||||
}
|
||||
} else {
|
||||
// Filter từ khóa bình thường
|
||||
const searchKeyword = keyword.toLowerCase();
|
||||
const items = categoryList.querySelectorAll(":scope > li");
|
||||
items.forEach(item => filterTree(item, searchKeyword));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
function filterTree(element, query) {
|
||||
const text = element.textContent.toLowerCase();
|
||||
const match = text.includes(query);
|
||||
|
||||
let childMatch = false;
|
||||
const children = element.querySelectorAll(":scope > ul > li");
|
||||
|
||||
children.forEach(child => {
|
||||
if (filterTree(child, query)) childMatch = true;
|
||||
});
|
||||
|
||||
const visible = match || childMatch;
|
||||
element.style.display = visible ? "" : "none";
|
||||
return visible;
|
||||
}
|
||||
|
||||
document.getElementById("searchInput").addEventListener("input", e => {
|
||||
const query = e.target.value.toLowerCase();
|
||||
const items = categoryList.querySelectorAll(":scope > li");
|
||||
items.forEach(item => filterTree(item, query));
|
||||
});
|
||||
|
||||
document.getElementById("selectAll").addEventListener("change", e => {
|
||||
const checked = e.target.checked;
|
||||
const visibleCheckboxes = categoryList.querySelectorAll("input[type='checkbox']");
|
||||
visibleCheckboxes.forEach(cb => {
|
||||
if (cb.closest("li").offsetParent !== null) {
|
||||
cb.checked = checked;
|
||||
const id = parseInt(cb.dataset.id);
|
||||
if (checked) selectedAppIds.add(id);
|
||||
else selectedAppIds.delete(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("applyBtn").addEventListener("click", async () => {
|
||||
const body = JSON.stringify({ app_ids: Array.from(selectedAppIds) });
|
||||
const res = await fetch(SERVER_DOMAIN+"/update", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body
|
||||
});
|
||||
const json = await res.json();
|
||||
alert("Submitted:\n" + JSON.stringify(json, null, 2));
|
||||
});
|
||||
|
||||
// Initial fetch
|
||||
fetchData();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
const express = require("express");
|
||||
const puppeteer = require("puppeteer");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const app = express();
|
||||
const fetch = require("node-fetch");
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.static("public"));
|
||||
|
||||
// ========== Global token store ==========
|
||||
let globalAuth = {
|
||||
csrfToken: null,
|
||||
cookieHeader: null
|
||||
};
|
||||
|
||||
const OPENSENCE_DOMAIN = "http://192.168.1.1/"
|
||||
// ========== Auto login with Puppeteer ==========
|
||||
async function refreshLoginSession() {
|
||||
console.log("🔐 Refreshing login session...");
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ["--no-sandbox"]
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
const opnUrl = OPENSENCE_DOMAIN;
|
||||
const username = "root";
|
||||
const password = "opnsense";
|
||||
|
||||
try {
|
||||
await page.goto(opnUrl, { waitUntil: "networkidle2" });
|
||||
|
||||
const csrf = await page.$eval('form input[type="hidden"]', input => ({
|
||||
name: input.name,
|
||||
value: input.value
|
||||
}));
|
||||
|
||||
await page.type("#usernamefld", username);
|
||||
await page.type("#passwordfld", password);
|
||||
await page.click(".btn");
|
||||
|
||||
setTimeout(async()=>{
|
||||
const cookies = await page.cookies();
|
||||
const cookieHeader = cookies.map(c => `${c.name}=${c.value}`).join("; ");
|
||||
globalAuth.csrfToken = csrf.value;
|
||||
globalAuth.cookieHeader = cookieHeader;
|
||||
await browser.close();
|
||||
console.log("✅ Login refreshed!");
|
||||
}, 2000)
|
||||
|
||||
|
||||
} catch (err) {
|
||||
console.error("❌ Login failed:", err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Run once immediately, then every 5 minutes
|
||||
refreshLoginSession();
|
||||
setInterval(refreshLoginSession, 5 * 60 * 1000);
|
||||
|
||||
// ========== API Routes ==========
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
res.sendFile(path.join(__dirname, "public", "index.html"));
|
||||
});
|
||||
|
||||
// GET /api/getListApp
|
||||
app.get("/api/getListApp", async (req, res) => {
|
||||
const filePath = path.join(__dirname, "listApp.txt");
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, "utf-8");
|
||||
const apps = JSON.parse(content);
|
||||
|
||||
// Gọi API để lấy init từ policy detail
|
||||
const policyRes = await fetch(OPENSENCE_DOMAIN+"api/zenarmor/policy/detail?id=0", {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Cookie": globalAuth.cookieHeader,
|
||||
"X-CSRFTOKEN": globalAuth.csrfToken
|
||||
}
|
||||
});
|
||||
const policyJson = await policyRes.json();
|
||||
const init = policyJson.app_controls || [];
|
||||
|
||||
res.json({ data: apps, init });
|
||||
} catch (err) {
|
||||
console.error("❌ Error in /api/getListApp:", JSON.stringify(err));
|
||||
res.status(500).json({ error: "Failed to load apps" });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /update
|
||||
app.post("/update", async (req, res) => {
|
||||
const { app_ids } = req.body;
|
||||
if (!Array.isArray(app_ids)) {
|
||||
return res.status(400).json({ error: "app_ids must be an array" });
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(OPENSENCE_DOMAIN+"api/zenarmor/policy/apply", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Accept": "*/*",
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRFTOKEN": globalAuth.csrfToken,
|
||||
"Cookie": globalAuth.cookieHeader
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: "0",
|
||||
checksum: 0,
|
||||
cloud_id: 0,
|
||||
is_active: true,
|
||||
is_centralized: false,
|
||||
is_default: false,
|
||||
name: "Default",
|
||||
decision_is_block: false,
|
||||
block_untrusted_devices: false,
|
||||
groups: [],
|
||||
usernames: [],
|
||||
interfaces: [],
|
||||
vlans: [],
|
||||
devices: [],
|
||||
device_categories: [],
|
||||
mac_addresses: [],
|
||||
networks: [],
|
||||
directions: {
|
||||
inbound: false,
|
||||
outbound: false
|
||||
},
|
||||
webcategory_type: "permissive",
|
||||
sort_number: 0,
|
||||
security: false,
|
||||
app: true,
|
||||
web: false,
|
||||
tls: false,
|
||||
advanced_security: [],
|
||||
essential_security: [],
|
||||
app_controls: app_ids,
|
||||
app_controls_custom: [],
|
||||
web_controls: [],
|
||||
web_controls_custom: ["b665988a-05ed-4c7e-8964-50459265e8c9"],
|
||||
url_blocks: [],
|
||||
safe_search: "off",
|
||||
block_ech: true,
|
||||
schedules: [],
|
||||
tls_controls: {
|
||||
enabled: false,
|
||||
ignore_pinned_certs: false,
|
||||
lazy_inspection: false,
|
||||
webs_all: false,
|
||||
webs: []
|
||||
},
|
||||
casb: {
|
||||
actions: []
|
||||
},
|
||||
exclusion_blacklist: [],
|
||||
exclusion_whitelist: [],
|
||||
exclusion_blacklist_global: [],
|
||||
exclusion_whitelist_global: []
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.text();
|
||||
res.json({ success: true, result });
|
||||
} catch (err) {
|
||||
console.error("❌ Error in /update:", err.message);
|
||||
res.status(500).json({ error: "Failed to apply policy" });
|
||||
}
|
||||
});
|
||||
|
||||
const PORT = 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 Server running at http://localhost:${PORT}`);
|
||||
});
|
||||
Loading…
Reference in New Issue