diff --git a/package/secubox/luci-app-mitmproxy/Makefile b/package/secubox/luci-app-mitmproxy/Makefile new file mode 100644 index 00000000..7f67dd52 --- /dev/null +++ b/package/secubox/luci-app-mitmproxy/Makefile @@ -0,0 +1,64 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright (C) 2025 CyberMind.fr +# +# LuCI mitmproxy Dashboard - HTTPS Traffic Inspection Interface + +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-mitmproxy +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 +PKG_ARCH:=all + +PKG_LICENSE:=Apache-2.0 +PKG_MAINTAINER:=CyberMind + +LUCI_TITLE:=LuCI mitmproxy Dashboard +LUCI_DESCRIPTION:=Modern dashboard for mitmproxy HTTPS traffic inspection with SecuBox theme +LUCI_DEPENDS:=+luci-base +luci-app-secubox +secubox-app-mitmproxy +jq + +LUCI_PKGARCH:=all + +include $(TOPDIR)/feeds/luci/luci.mk + +define Package/$(PKG_NAME)/conffiles +/etc/config/mitmproxy +endef + +define Package/$(PKG_NAME)/install + # RPCD backend (MUST be 755 for ubus calls) + $(INSTALL_DIR) $(1)/usr/libexec/rpcd + $(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.mitmproxy $(1)/usr/libexec/rpcd/ + + # ACL permissions + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/*.json $(1)/usr/share/rpcd/acl.d/ + + # LuCI menu + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/*.json $(1)/usr/share/luci/menu.d/ + + # JavaScript resources + $(INSTALL_DIR) $(1)/www/luci-static/resources/mitmproxy + $(INSTALL_DATA) ./htdocs/luci-static/resources/mitmproxy/*.js $(1)/www/luci-static/resources/mitmproxy/ 2>/dev/null || true + $(INSTALL_DATA) ./htdocs/luci-static/resources/mitmproxy/*.css $(1)/www/luci-static/resources/mitmproxy/ 2>/dev/null || true + + # JavaScript views + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/mitmproxy + $(INSTALL_DATA) ./htdocs/luci-static/resources/view/mitmproxy/*.js $(1)/www/luci-static/resources/view/mitmproxy/ +endef + +define Package/$(PKG_NAME)/postinst +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] || { + # Restart RPCD to register new methods + /etc/init.d/rpcd restart + rm -rf /tmp/luci-modulecache /tmp/luci-indexcache 2>/dev/null + echo "mitmproxy Dashboard installed." +} +exit 0 +endef + +# call BuildPackage - OpenWrt buildroot +$(eval $(call BuildPackage,luci-app-mitmproxy)) diff --git a/package/secubox/luci-app-mitmproxy/htdocs/luci-static/resources/mitmproxy/api.js b/package/secubox/luci-app-mitmproxy/htdocs/luci-static/resources/mitmproxy/api.js new file mode 100644 index 00000000..2ff19e95 --- /dev/null +++ b/package/secubox/luci-app-mitmproxy/htdocs/luci-static/resources/mitmproxy/api.js @@ -0,0 +1,151 @@ +'use strict'; +'require rpc'; + +var callMitmproxy = rpc.declare({ + object: 'luci.mitmproxy', + method: 'get_status' +}); + +var callGetConfig = rpc.declare({ + object: 'luci.mitmproxy', + method: 'get_config' +}); + +var callGetStats = rpc.declare({ + object: 'luci.mitmproxy', + method: 'get_stats' +}); + +var callGetRequests = rpc.declare({ + object: 'luci.mitmproxy', + method: 'get_requests', + params: ['limit'] +}); + +var callGetTopHosts = rpc.declare({ + object: 'luci.mitmproxy', + method: 'get_top_hosts', + params: ['limit'] +}); + +var callGetCaInfo = rpc.declare({ + object: 'luci.mitmproxy', + method: 'get_ca_info' +}); + +var callServiceStart = rpc.declare({ + object: 'luci.mitmproxy', + method: 'service_start' +}); + +var callServiceStop = rpc.declare({ + object: 'luci.mitmproxy', + method: 'service_stop' +}); + +var callServiceRestart = rpc.declare({ + object: 'luci.mitmproxy', + method: 'service_restart' +}); + +var callSetConfig = rpc.declare({ + object: 'luci.mitmproxy', + method: 'set_config', + params: ['key', 'value'] +}); + +var callClearData = rpc.declare({ + object: 'luci.mitmproxy', + method: 'clear_data' +}); + +return { + getStatus: function() { + return callMitmproxy().catch(function() { + return { running: false, enabled: false }; + }); + }, + + getConfig: function() { + return callGetConfig().catch(function() { + return {}; + }); + }, + + getStats: function() { + return callGetStats().catch(function() { + return { total_requests: 0, unique_hosts: 0, flow_file_size: 0 }; + }); + }, + + getRequests: function(limit) { + return callGetRequests(limit || 50).catch(function() { + return { requests: [] }; + }); + }, + + getTopHosts: function(limit) { + return callGetTopHosts(limit || 20).catch(function() { + return { hosts: [] }; + }); + }, + + getCaInfo: function() { + return callGetCaInfo().catch(function() { + return { installed: false }; + }); + }, + + serviceStart: function() { + return callServiceStart(); + }, + + serviceStop: function() { + return callServiceStop(); + }, + + serviceRestart: function() { + return callServiceRestart(); + }, + + setConfig: function(key, value) { + return callSetConfig(key, value); + }, + + clearData: function() { + return callClearData(); + }, + + getAllData: function() { + return Promise.all([ + this.getStatus(), + this.getConfig(), + this.getStats(), + this.getTopHosts(10), + this.getCaInfo() + ]).then(function(results) { + return { + status: results[0], + config: results[1], + stats: results[2], + topHosts: results[3], + caInfo: results[4] + }; + }); + }, + + formatBytes: function(bytes) { + if (!bytes || bytes === 0) return '0 B'; + var k = 1024; + var sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + var i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }, + + formatNumber: function(num) { + if (!num) return '0'; + if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'; + if (num >= 1000) return (num / 1000).toFixed(1) + 'K'; + return num.toString(); + } +}; diff --git a/package/secubox/luci-app-mitmproxy/htdocs/luci-static/resources/mitmproxy/dashboard.css b/package/secubox/luci-app-mitmproxy/htdocs/luci-static/resources/mitmproxy/dashboard.css new file mode 100644 index 00000000..ca49629a --- /dev/null +++ b/package/secubox/luci-app-mitmproxy/htdocs/luci-static/resources/mitmproxy/dashboard.css @@ -0,0 +1,456 @@ +/* mitmproxy Dashboard - SecuBox Theme */ + +.mitmproxy-dashboard { + --mp-primary: #e74c3c; + --mp-primary-light: #ec7063; + --mp-secondary: #3498db; + --mp-success: #27ae60; + --mp-warning: #f39c12; + --mp-danger: #c0392b; + --mp-bg-dark: #0d0d12; + --mp-bg-card: #141419; + --mp-bg-card-hover: #1a1a22; + --mp-border: rgba(255, 255, 255, 0.08); + --mp-text: #e0e0e8; + --mp-text-muted: #8a8a9a; + --mp-gradient: linear-gradient(135deg, #e74c3c, #c0392b); + + font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + color: var(--mp-text); + padding: 20px; + max-width: 1400px; + margin: 0 auto; +} + +/* Header */ +.mp-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 24px; + background: var(--mp-bg-card); + border: 1px solid var(--mp-border); + border-radius: 16px; + margin-bottom: 20px; +} + +.mp-logo { + display: flex; + align-items: center; + gap: 12px; +} + +.mp-logo-icon { + width: 48px; + height: 48px; + background: var(--mp-gradient); + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; +} + +.mp-logo-text { + font-size: 24px; + font-weight: 700; + color: #fff; +} + +.mp-logo-text span { + color: var(--mp-primary); +} + +.mp-status-badge { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + border-radius: 20px; + font-size: 14px; + font-weight: 500; +} + +.mp-status-badge.running { + background: rgba(39, 174, 96, 0.2); + color: #27ae60; +} + +.mp-status-badge.stopped { + background: rgba(192, 57, 43, 0.2); + color: #c0392b; +} + +.mp-status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: currentColor; + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +/* Controls */ +.mp-controls { + display: flex; + align-items: center; + gap: 10px; + padding: 16px 20px; + background: var(--mp-bg-card); + border: 1px solid var(--mp-border); + border-radius: 12px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.mp-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 10px 18px; + border: none; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + background: rgba(255, 255, 255, 0.05); + color: var(--mp-text); +} + +.mp-btn:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.mp-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.mp-btn-success { + background: var(--mp-success); + color: white; +} + +.mp-btn-danger { + background: var(--mp-danger); + color: white; +} + +.mp-btn-primary { + background: var(--mp-primary); + color: white; +} + +.mp-btn-secondary { + background: var(--mp-secondary); + color: white; +} + +/* Quick Stats Grid */ +.mp-quick-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; + margin-bottom: 20px; +} + +.mp-quick-stat { + background: var(--mp-bg-card); + border: 1px solid var(--mp-border); + border-radius: 12px; + padding: 20px; + position: relative; + overflow: hidden; +} + +.mp-quick-stat::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: var(--stat-gradient, var(--mp-gradient)); +} + +.mp-quick-stat-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; +} + +.mp-quick-stat-icon { + font-size: 20px; +} + +.mp-quick-stat-label { + font-size: 13px; + color: var(--mp-text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.mp-quick-stat-value { + font-size: 28px; + font-weight: 700; + color: #fff; +} + +.mp-quick-stat-sub { + font-size: 12px; + color: var(--mp-text-muted); + margin-top: 4px; +} + +/* Cards */ +.mp-card { + background: var(--mp-bg-card); + border: 1px solid var(--mp-border); + border-radius: 12px; + margin-bottom: 20px; + overflow: hidden; +} + +.mp-card-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + border-bottom: 1px solid var(--mp-border); +} + +.mp-card-title { + display: flex; + align-items: center; + gap: 10px; + font-size: 16px; + font-weight: 600; + color: #fff; +} + +.mp-card-title-icon { + font-size: 18px; +} + +.mp-card-badge { + background: rgba(255, 255, 255, 0.1); + padding: 4px 12px; + border-radius: 16px; + font-size: 12px; + color: var(--mp-text-muted); +} + +.mp-card-body { + padding: 20px; +} + +/* Grid layouts */ +.mp-grid-2 { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: 20px; + margin-bottom: 20px; +} + +/* Host list */ +.mp-hosts-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.mp-host-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px; + background: rgba(255, 255, 255, 0.02); + border-radius: 8px; + transition: background 0.2s; +} + +.mp-host-item:hover { + background: rgba(255, 255, 255, 0.05); +} + +.mp-host-icon { + width: 36px; + height: 36px; + background: var(--mp-gradient); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; +} + +.mp-host-info { + flex: 1; + min-width: 0; +} + +.mp-host-name { + font-size: 14px; + font-weight: 500; + color: #fff; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.mp-host-count { + font-size: 12px; + color: var(--mp-text-muted); +} + +.mp-host-bar { + width: 60px; + height: 4px; + background: rgba(255, 255, 255, 0.1); + border-radius: 2px; + overflow: hidden; +} + +.mp-host-bar-fill { + height: 100%; + background: var(--mp-gradient); + border-radius: 2px; + transition: width 0.3s; +} + +/* CA Certificate card */ +.mp-ca-card { + display: flex; + align-items: center; + gap: 16px; + padding: 20px; + background: rgba(255, 255, 255, 0.02); + border-radius: 12px; +} + +.mp-ca-icon { + width: 56px; + height: 56px; + background: var(--mp-secondary); + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + font-size: 28px; +} + +.mp-ca-info { + flex: 1; +} + +.mp-ca-title { + font-size: 16px; + font-weight: 600; + color: #fff; + margin-bottom: 4px; +} + +.mp-ca-status { + font-size: 13px; + color: var(--mp-text-muted); +} + +.mp-ca-status.installed { + color: var(--mp-success); +} + +.mp-ca-status.not-installed { + color: var(--mp-warning); +} + +/* Empty state */ +.mp-empty { + text-align: center; + padding: 40px 20px; + color: var(--mp-text-muted); +} + +.mp-empty-icon { + font-size: 48px; + margin-bottom: 16px; + opacity: 0.5; +} + +.mp-empty-text { + font-size: 16px; + font-weight: 500; + margin-bottom: 8px; +} + +/* Navigation */ +.mp-app-nav { + display: flex; + gap: 8px; + margin-bottom: 20px; + padding: 12px 16px; + background: var(--mp-bg-card); + border: 1px solid var(--mp-border); + border-radius: 12px; +} + +.mp-app-nav a { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + border-radius: 8px; + text-decoration: none; + font-size: 14px; + font-weight: 500; + transition: all 0.2s; + color: var(--mp-text-muted); + background: transparent; +} + +.mp-app-nav a:hover { + background: rgba(255, 255, 255, 0.05); + color: var(--mp-text); +} + +.mp-app-nav a.active { + background: var(--mp-gradient); + color: white; +} + +/* Value update animation */ +.mp-value-updated { + animation: valueFlash 0.5s ease-out; +} + +@keyframes valueFlash { + 0% { color: var(--mp-primary); transform: scale(1.05); } + 100% { color: inherit; transform: scale(1); } +} + +/* Responsive */ +@media (max-width: 768px) { + .mp-header { + flex-direction: column; + gap: 16px; + text-align: center; + } + + .mp-controls { + justify-content: center; + } + + .mp-grid-2 { + grid-template-columns: 1fr; + } + + .mp-quick-stats { + grid-template-columns: repeat(2, 1fr); + } +} diff --git a/package/secubox/luci-app-mitmproxy/htdocs/luci-static/resources/view/mitmproxy/dashboard.js b/package/secubox/luci-app-mitmproxy/htdocs/luci-static/resources/view/mitmproxy/dashboard.js new file mode 100644 index 00000000..b454e02b --- /dev/null +++ b/package/secubox/luci-app-mitmproxy/htdocs/luci-static/resources/view/mitmproxy/dashboard.js @@ -0,0 +1,365 @@ +'use strict'; +'require view'; +'require poll'; +'require dom'; +'require ui'; +'require mitmproxy.api as api'; +'require secubox-theme/theme as Theme'; +'require secubox-portal/header as SbHeader'; + +var lang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: lang }); + +var MITMPROXY_NAV = [ + { id: 'dashboard', icon: '📊', label: 'Dashboard' }, + { id: 'requests', icon: '🔍', label: 'Requests' }, + { id: 'settings', icon: '⚙️', label: 'Settings' } +]; + +function renderMitmproxyNav(activeId) { + return E('div', { 'class': 'mp-app-nav' }, MITMPROXY_NAV.map(function(item) { + var isActive = activeId === item.id; + return E('a', { + 'href': L.url('admin', 'secubox', 'mitmproxy', item.id), + 'class': isActive ? 'active' : '' + }, [ + E('span', {}, item.icon), + E('span', {}, _(item.label)) + ]); + })); +} + +return view.extend({ + title: _('mitmproxy Dashboard'), + pollInterval: 5, + pollActive: true, + + load: function() { + return api.getAllData(); + }, + + updateDashboard: function(data) { + var status = data.status || {}; + var stats = data.stats || {}; + + // Update status badge + var statusBadge = document.querySelector('.mp-status-badge'); + if (statusBadge) { + statusBadge.classList.toggle('running', status.running); + statusBadge.classList.toggle('stopped', !status.running); + statusBadge.innerHTML = '' + + (status.running ? 'Running' : 'Stopped'); + } + + // Update stats + var updates = [ + { sel: '.mp-stat-requests', val: api.formatNumber(stats.total_requests) }, + { sel: '.mp-stat-hosts', val: api.formatNumber(stats.unique_hosts) }, + { sel: '.mp-stat-flows', val: api.formatBytes(stats.flow_file_size) } + ]; + + updates.forEach(function(u) { + var el = document.querySelector(u.sel); + if (el && el.textContent !== u.val) { + el.textContent = u.val; + el.classList.add('mp-value-updated'); + setTimeout(function() { el.classList.remove('mp-value-updated'); }, 500); + } + }); + }, + + startPolling: function() { + var self = this; + this.pollActive = true; + + poll.add(L.bind(function() { + if (!this.pollActive) return Promise.resolve(); + + return api.getAllData().then(L.bind(function(data) { + this.updateDashboard(data); + }, this)); + }, this), this.pollInterval); + }, + + stopPolling: function() { + this.pollActive = false; + poll.stop(); + }, + + handleServiceControl: function(action) { + var self = this; + + ui.showModal(_('Please wait...'), [ + E('p', { 'class': 'spinning' }, _('Processing request...')) + ]); + + var promise; + switch (action) { + case 'start': + promise = api.serviceStart(); + break; + case 'stop': + promise = api.serviceStop(); + break; + case 'restart': + promise = api.serviceRestart(); + break; + default: + ui.hideModal(); + return; + } + + promise.then(function(result) { + ui.hideModal(); + if (result.running !== undefined) { + ui.addNotification(null, E('p', {}, _('Service ' + action + ' completed')), 'info'); + location.reload(); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, _('Error: ') + err.message), 'error'); + }); + }, + + handleClearData: function() { + var self = this; + + if (!confirm(_('Clear all captured request data?'))) { + return; + } + + api.clearData().then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', {}, result.message || _('Data cleared')), 'info'); + location.reload(); + } + }); + }, + + render: function(data) { + var self = this; + var status = data.status || {}; + var config = data.config || {}; + var stats = data.stats || {}; + var topHosts = (data.topHosts || {}).hosts || []; + var caInfo = data.caInfo || {}; + + var view = E('div', { 'class': 'mitmproxy-dashboard' }, [ + E('link', { 'rel': 'stylesheet', 'href': L.resource('mitmproxy/dashboard.css') }), + + // Header + E('div', { 'class': 'mp-header' }, [ + E('div', { 'class': 'mp-logo' }, [ + E('div', { 'class': 'mp-logo-icon' }, '🔐'), + E('div', { 'class': 'mp-logo-text' }, ['mitm', E('span', {}, 'proxy')]) + ]), + E('div', {}, [ + E('div', { + 'class': 'mp-status-badge ' + (status.running ? 'running' : 'stopped') + }, [ + E('span', { 'class': 'mp-status-dot' }), + status.running ? 'Running' : 'Stopped' + ]) + ]) + ]), + + // Service controls + E('div', { 'class': 'mp-controls' }, [ + E('button', { + 'class': 'mp-btn mp-btn-success', + 'click': function() { self.handleServiceControl('start'); }, + 'disabled': status.running + }, '▶ Start'), + E('button', { + 'class': 'mp-btn mp-btn-danger', + 'click': function() { self.handleServiceControl('stop'); }, + 'disabled': !status.running + }, '⏹ Stop'), + E('button', { + 'class': 'mp-btn mp-btn-primary', + 'click': function() { self.handleServiceControl('restart'); } + }, '🔄 Restart'), + E('div', { 'style': 'flex: 1' }), + status.web_url ? E('a', { + 'class': 'mp-btn mp-btn-secondary', + 'href': status.web_url, + 'target': '_blank' + }, '🌐 Open Web UI') : null, + E('button', { + 'class': 'mp-btn', + 'click': L.bind(this.handleClearData, this) + }, '🗑 Clear Data') + ]), + + // Quick Stats + E('div', { 'class': 'mp-quick-stats' }, [ + E('div', { 'class': 'mp-quick-stat' }, [ + E('div', { 'class': 'mp-quick-stat-header' }, [ + E('span', { 'class': 'mp-quick-stat-icon' }, '📊'), + E('span', { 'class': 'mp-quick-stat-label' }, 'Total Requests') + ]), + E('div', { 'class': 'mp-quick-stat-value mp-stat-requests' }, + api.formatNumber(stats.total_requests || 0)), + E('div', { 'class': 'mp-quick-stat-sub' }, 'Captured since start') + ]), + E('div', { 'class': 'mp-quick-stat', 'style': '--stat-gradient: linear-gradient(135deg, #3498db, #2980b9)' }, [ + E('div', { 'class': 'mp-quick-stat-header' }, [ + E('span', { 'class': 'mp-quick-stat-icon' }, '🌐'), + E('span', { 'class': 'mp-quick-stat-label' }, 'Unique Hosts') + ]), + E('div', { 'class': 'mp-quick-stat-value mp-stat-hosts' }, + api.formatNumber(stats.unique_hosts || 0)), + E('div', { 'class': 'mp-quick-stat-sub' }, 'Distinct domains') + ]), + E('div', { 'class': 'mp-quick-stat', 'style': '--stat-gradient: linear-gradient(135deg, #27ae60, #1abc9c)' }, [ + E('div', { 'class': 'mp-quick-stat-header' }, [ + E('span', { 'class': 'mp-quick-stat-icon' }, '💾'), + E('span', { 'class': 'mp-quick-stat-label' }, 'Flow Data') + ]), + E('div', { 'class': 'mp-quick-stat-value mp-stat-flows' }, + api.formatBytes(stats.flow_file_size || 0)), + E('div', { 'class': 'mp-quick-stat-sub' }, 'Captured flows') + ]), + E('div', { 'class': 'mp-quick-stat', 'style': '--stat-gradient: linear-gradient(135deg, #9b59b6, #8e44ad)' }, [ + E('div', { 'class': 'mp-quick-stat-header' }, [ + E('span', { 'class': 'mp-quick-stat-icon' }, '🔌'), + E('span', { 'class': 'mp-quick-stat-label' }, 'Proxy Port') + ]), + E('div', { 'class': 'mp-quick-stat-value' }, status.listen_port || 8080), + E('div', { 'class': 'mp-quick-stat-sub' }, config.mode || 'transparent') + ]) + ]), + + // Grid layout + E('div', { 'class': 'mp-grid-2' }, [ + // Top Hosts + E('div', { 'class': 'mp-card' }, [ + E('div', { 'class': 'mp-card-header' }, [ + E('div', { 'class': 'mp-card-title' }, [ + E('span', { 'class': 'mp-card-title-icon' }, '🌐'), + 'Top Hosts' + ]), + E('div', { 'class': 'mp-card-badge' }, topHosts.length + ' hosts') + ]), + E('div', { 'class': 'mp-card-body' }, + topHosts.length > 0 ? + E('div', { 'class': 'mp-hosts-list' }, + (function() { + var maxCount = Math.max.apply(null, topHosts.map(function(h) { return h.count || 0; })) || 1; + return topHosts.slice(0, 8).map(function(host) { + var pct = Math.round(((host.count || 0) / maxCount) * 100); + return E('div', { 'class': 'mp-host-item' }, [ + E('div', { 'class': 'mp-host-icon' }, '🔗'), + E('div', { 'class': 'mp-host-info' }, [ + E('div', { 'class': 'mp-host-name' }, host.host || 'unknown'), + E('div', { 'class': 'mp-host-count' }, (host.count || 0) + ' requests') + ]), + E('div', { 'class': 'mp-host-bar' }, [ + E('div', { 'class': 'mp-host-bar-fill', 'style': 'width:' + pct + '%' }) + ]) + ]); + }); + })() + ) : + E('div', { 'class': 'mp-empty' }, [ + E('div', { 'class': 'mp-empty-icon' }, '🌐'), + E('div', { 'class': 'mp-empty-text' }, 'No hosts captured yet'), + E('p', {}, 'Start the proxy and generate traffic') + ]) + ) + ]), + + // CA Certificate + E('div', { 'class': 'mp-card' }, [ + E('div', { 'class': 'mp-card-header' }, [ + E('div', { 'class': 'mp-card-title' }, [ + E('span', { 'class': 'mp-card-title-icon' }, '🔒'), + 'CA Certificate' + ]) + ]), + E('div', { 'class': 'mp-card-body' }, [ + E('div', { 'class': 'mp-ca-card' }, [ + E('div', { 'class': 'mp-ca-icon' }, '📜'), + E('div', { 'class': 'mp-ca-info' }, [ + E('div', { 'class': 'mp-ca-title' }, 'mitmproxy CA'), + E('div', { + 'class': 'mp-ca-status ' + (caInfo.installed ? 'installed' : 'not-installed') + }, caInfo.installed ? 'Certificate installed' : 'Certificate not generated'), + caInfo.expires ? E('div', { 'class': 'mp-ca-status' }, 'Expires: ' + caInfo.expires) : null + ]), + caInfo.download_url ? E('a', { + 'class': 'mp-btn mp-btn-secondary', + 'href': caInfo.download_url, + 'target': '_blank' + }, '⬇ Download') : null + ]), + E('div', { 'style': 'margin-top: 16px; padding: 16px; background: rgba(255,255,255,0.02); border-radius: 8px; font-size: 13px; color: var(--mp-text-muted)' }, [ + E('p', { 'style': 'margin: 0 0 8px 0' }, [ + E('strong', {}, 'HTTPS Interception: '), + 'To inspect encrypted traffic, install the mitmproxy CA certificate on client devices.' + ]), + E('p', { 'style': 'margin: 0' }, [ + 'Access ', + E('code', {}, 'http://mitm.it'), + ' from any proxied device to download the certificate.' + ]) + ]) + ]) + ]) + ]), + + // Configuration Summary + E('div', { 'class': 'mp-card' }, [ + E('div', { 'class': 'mp-card-header' }, [ + E('div', { 'class': 'mp-card-title' }, [ + E('span', { 'class': 'mp-card-title-icon' }, '⚙️'), + 'Configuration' + ]), + E('a', { + 'href': L.url('admin', 'secubox', 'mitmproxy', 'settings'), + 'class': 'mp-btn' + }, '✏ Edit') + ]), + E('div', { 'class': 'mp-card-body' }, [ + E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px;' }, [ + E('div', {}, [ + E('div', { 'style': 'color: var(--mp-text-muted); font-size: 12px; text-transform: uppercase; margin-bottom: 4px' }, 'Mode'), + E('div', { 'style': 'font-weight: 500' }, config.mode || 'transparent') + ]), + E('div', {}, [ + E('div', { 'style': 'color: var(--mp-text-muted); font-size: 12px; text-transform: uppercase; margin-bottom: 4px' }, 'Proxy Port'), + E('div', { 'style': 'font-weight: 500' }, (config.listen_host || '0.0.0.0') + ':' + (config.listen_port || 8080)) + ]), + E('div', {}, [ + E('div', { 'style': 'color: var(--mp-text-muted); font-size: 12px; text-transform: uppercase; margin-bottom: 4px' }, 'Web UI Port'), + E('div', { 'style': 'font-weight: 500' }, (config.web_host || '0.0.0.0') + ':' + (config.web_port || 8081)) + ]), + E('div', {}, [ + E('div', { 'style': 'color: var(--mp-text-muted); font-size: 12px; text-transform: uppercase; margin-bottom: 4px' }, 'Capture'), + E('div', { 'style': 'font-weight: 500' }, [ + config.capture_urls ? 'URLs ' : '', + config.capture_cookies ? 'Cookies ' : '', + config.capture_headers ? 'Headers ' : '' + ].filter(Boolean).join(', ') || 'Disabled') + ]) + ]) + ]) + ]) + ]); + + // Start polling + this.startPolling(); + + var wrapper = E('div', { 'class': 'secubox-page-wrapper' }); + wrapper.appendChild(SbHeader.render()); + wrapper.appendChild(renderMitmproxyNav('dashboard')); + wrapper.appendChild(view); + return wrapper; + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-mitmproxy/htdocs/luci-static/resources/view/mitmproxy/requests.js b/package/secubox/luci-app-mitmproxy/htdocs/luci-static/resources/view/mitmproxy/requests.js new file mode 100644 index 00000000..a365c3ab --- /dev/null +++ b/package/secubox/luci-app-mitmproxy/htdocs/luci-static/resources/view/mitmproxy/requests.js @@ -0,0 +1,306 @@ +'use strict'; +'require view'; +'require poll'; +'require dom'; +'require ui'; +'require mitmproxy.api as api'; +'require secubox-theme/theme as Theme'; +'require secubox-portal/header as SbHeader'; + +var lang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: lang }); + +var MITMPROXY_NAV = [ + { id: 'dashboard', icon: '📊', label: 'Dashboard' }, + { id: 'requests', icon: '🔍', label: 'Requests' }, + { id: 'settings', icon: '⚙️', label: 'Settings' } +]; + +function renderMitmproxyNav(activeId) { + return E('div', { 'class': 'mp-app-nav' }, MITMPROXY_NAV.map(function(item) { + var isActive = activeId === item.id; + return E('a', { + 'href': L.url('admin', 'secubox', 'mitmproxy', item.id), + 'class': isActive ? 'active' : '' + }, [ + E('span', {}, item.icon), + E('span', {}, _(item.label)) + ]); + })); +} + +return view.extend({ + title: _('mitmproxy Requests'), + pollInterval: 3, + pollActive: true, + + load: function() { + return Promise.all([ + api.getStatus(), + api.getRequests(100) + ]).then(function(results) { + return { + status: results[0], + requests: results[1] + }; + }); + }, + + getMethodColor: function(method) { + var colors = { + 'GET': '#3498db', + 'POST': '#27ae60', + 'PUT': '#f39c12', + 'DELETE': '#e74c3c', + 'PATCH': '#9b59b6', + 'HEAD': '#1abc9c', + 'OPTIONS': '#95a5a6' + }; + return colors[method] || '#7f8c8d'; + }, + + getStatusColor: function(status) { + if (status >= 200 && status < 300) return '#27ae60'; + if (status >= 300 && status < 400) return '#3498db'; + if (status >= 400 && status < 500) return '#f39c12'; + if (status >= 500) return '#e74c3c'; + return '#95a5a6'; + }, + + updateRequests: function(data) { + var requests = (data.requests || {}).requests || []; + var container = document.querySelector('.mp-requests-list'); + if (!container) return; + + if (requests.length === 0) { + container.innerHTML = '
🔍
No requests captured

Generate HTTP traffic to see requests

'; + return; + } + + var self = this; + container.innerHTML = ''; + + requests.slice(-50).reverse().forEach(function(req) { + var request = req.request || req; + var response = req.response || {}; + var method = request.method || 'GET'; + var host = request.host || request.headers && request.headers.host || 'unknown'; + var path = request.path || '/'; + var status = response.status_code || response.status || 0; + var contentType = response.headers && (response.headers['content-type'] || response.headers['Content-Type']) || ''; + + var item = E('div', { 'class': 'mp-request-item' }, [ + E('div', { 'class': 'mp-request-method', 'style': 'background:' + self.getMethodColor(method) }, method), + E('div', { 'class': 'mp-request-info' }, [ + E('div', { 'class': 'mp-request-url' }, [ + E('span', { 'class': 'mp-request-host' }, host), + E('span', { 'class': 'mp-request-path' }, path) + ]), + E('div', { 'class': 'mp-request-meta' }, [ + status ? E('span', { 'class': 'mp-request-status', 'style': 'color:' + self.getStatusColor(status) }, status) : null, + contentType ? E('span', {}, contentType.split(';')[0]) : null, + req.timestamp ? E('span', {}, new Date(req.timestamp).toLocaleTimeString()) : null + ].filter(Boolean)) + ]), + E('div', { 'class': 'mp-request-actions' }, [ + E('button', { + 'class': 'mp-btn-icon', + 'title': 'View details', + 'click': function() { self.showRequestDetails(req); } + }, '👁') + ]) + ]); + + container.appendChild(item); + }); + }, + + showRequestDetails: function(req) { + var request = req.request || req; + var response = req.response || {}; + + var content = E('div', { 'class': 'mp-request-details' }, [ + E('h3', {}, 'Request'), + E('pre', {}, [ + (request.method || 'GET') + ' ' + (request.path || '/') + ' HTTP/1.1\n', + 'Host: ' + (request.host || 'unknown') + '\n', + request.headers ? Object.keys(request.headers).map(function(k) { + return k + ': ' + request.headers[k]; + }).join('\n') : '' + ].join('')), + + response.status_code ? E('h3', {}, 'Response') : null, + response.status_code ? E('pre', {}, [ + 'HTTP/1.1 ' + response.status_code + ' ' + (response.reason || '') + '\n', + response.headers ? Object.keys(response.headers).map(function(k) { + return k + ': ' + response.headers[k]; + }).join('\n') : '' + ].join('')) : null, + + request.cookies && Object.keys(request.cookies).length ? E('h3', {}, 'Cookies') : null, + request.cookies && Object.keys(request.cookies).length ? E('pre', {}, + Object.keys(request.cookies).map(function(k) { + return k + '=' + request.cookies[k]; + }).join('\n') + ) : null + ].filter(Boolean)); + + ui.showModal(_('Request Details'), [ + content, + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'btn', + 'click': ui.hideModal + }, _('Close')) + ]) + ]); + }, + + startPolling: function() { + var self = this; + this.pollActive = true; + + poll.add(L.bind(function() { + if (!this.pollActive) return Promise.resolve(); + + return api.getRequests(100).then(L.bind(function(data) { + this.updateRequests({ requests: data }); + }, this)); + }, this), this.pollInterval); + }, + + stopPolling: function() { + this.pollActive = false; + poll.stop(); + }, + + render: function(data) { + var self = this; + var status = data.status || {}; + var requests = (data.requests || {}).requests || []; + + var view = E('div', { 'class': 'mitmproxy-dashboard' }, [ + E('link', { 'rel': 'stylesheet', 'href': L.resource('mitmproxy/dashboard.css') }), + E('style', {}, [ + '.mp-request-item { display: flex; align-items: center; gap: 12px; padding: 12px 16px; background: rgba(255,255,255,0.02); border-radius: 8px; margin-bottom: 8px; transition: background 0.2s; }', + '.mp-request-item:hover { background: rgba(255,255,255,0.05); }', + '.mp-request-method { min-width: 60px; padding: 4px 8px; border-radius: 4px; color: white; font-size: 11px; font-weight: 600; text-align: center; }', + '.mp-request-info { flex: 1; min-width: 0; }', + '.mp-request-url { display: flex; gap: 4px; font-size: 14px; }', + '.mp-request-host { font-weight: 500; color: #fff; }', + '.mp-request-path { color: var(--mp-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }', + '.mp-request-meta { display: flex; gap: 16px; font-size: 12px; color: var(--mp-text-muted); margin-top: 4px; }', + '.mp-request-status { font-weight: 500; }', + '.mp-request-actions { display: flex; gap: 8px; }', + '.mp-btn-icon { background: rgba(255,255,255,0.1); border: none; border-radius: 6px; width: 32px; height: 32px; cursor: pointer; font-size: 14px; transition: background 0.2s; }', + '.mp-btn-icon:hover { background: rgba(255,255,255,0.2); }', + '.mp-request-details pre { background: #0d0d12; padding: 16px; border-radius: 8px; font-size: 12px; overflow-x: auto; white-space: pre-wrap; word-break: break-all; }', + '.mp-request-details h3 { margin: 16px 0 8px; font-size: 14px; color: var(--mp-primary); }' + ].join('')), + + // Header + E('div', { 'class': 'mp-header' }, [ + E('div', { 'class': 'mp-logo' }, [ + E('div', { 'class': 'mp-logo-icon' }, '🔍'), + E('div', { 'class': 'mp-logo-text' }, 'Requests') + ]), + E('div', {}, [ + E('div', { + 'class': 'mp-status-badge ' + (status.running ? 'running' : 'stopped') + }, [ + E('span', { 'class': 'mp-status-dot' }), + status.running ? 'Capturing' : 'Stopped' + ]) + ]) + ]), + + // Controls + E('div', { 'class': 'mp-controls' }, [ + E('span', {}, _('Showing last 50 requests')), + E('div', { 'style': 'flex: 1' }), + E('button', { + 'class': 'mp-btn', + 'id': 'mp-poll-toggle', + 'click': L.bind(function(ev) { + var btn = ev.target; + if (this.pollActive) { + this.stopPolling(); + btn.textContent = '▶ Resume'; + } else { + this.startPolling(); + btn.textContent = '⏸ Pause'; + } + }, this) + }, '⏸ Pause'), + E('button', { + 'class': 'mp-btn', + 'click': function() { location.reload(); } + }, '🔄 Refresh') + ]), + + // Requests list + E('div', { 'class': 'mp-card' }, [ + E('div', { 'class': 'mp-card-header' }, [ + E('div', { 'class': 'mp-card-title' }, [ + E('span', { 'class': 'mp-card-title-icon' }, '📋'), + 'Captured Requests' + ]), + E('div', { 'class': 'mp-card-badge' }, requests.length + ' requests') + ]), + E('div', { 'class': 'mp-card-body mp-requests-list' }, + requests.length > 0 ? + requests.slice(-50).reverse().map(function(req) { + var request = req.request || req; + var response = req.response || {}; + var method = request.method || 'GET'; + var host = request.host || (request.headers && request.headers.host) || 'unknown'; + var path = request.path || '/'; + var status_code = response.status_code || response.status || 0; + var contentType = response.headers && (response.headers['content-type'] || response.headers['Content-Type']) || ''; + + return E('div', { 'class': 'mp-request-item' }, [ + E('div', { 'class': 'mp-request-method', 'style': 'background:' + self.getMethodColor(method) }, method), + E('div', { 'class': 'mp-request-info' }, [ + E('div', { 'class': 'mp-request-url' }, [ + E('span', { 'class': 'mp-request-host' }, host), + E('span', { 'class': 'mp-request-path' }, path) + ]), + E('div', { 'class': 'mp-request-meta' }, [ + status_code ? E('span', { 'class': 'mp-request-status', 'style': 'color:' + self.getStatusColor(status_code) }, String(status_code)) : null, + contentType ? E('span', {}, contentType.split(';')[0]) : null + ].filter(Boolean)) + ]), + E('div', { 'class': 'mp-request-actions' }, [ + E('button', { + 'class': 'mp-btn-icon', + 'title': 'View details', + 'click': function() { self.showRequestDetails(req); } + }, '👁') + ]) + ]); + }) : + E('div', { 'class': 'mp-empty' }, [ + E('div', { 'class': 'mp-empty-icon' }, '🔍'), + E('div', { 'class': 'mp-empty-text' }, 'No requests captured'), + E('p', {}, 'Start the proxy and generate HTTP traffic') + ]) + ) + ]) + ]); + + // Start polling + this.startPolling(); + + var wrapper = E('div', { 'class': 'secubox-page-wrapper' }); + wrapper.appendChild(SbHeader.render()); + wrapper.appendChild(renderMitmproxyNav('requests')); + wrapper.appendChild(view); + return wrapper; + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-mitmproxy/htdocs/luci-static/resources/view/mitmproxy/settings.js b/package/secubox/luci-app-mitmproxy/htdocs/luci-static/resources/view/mitmproxy/settings.js new file mode 100644 index 00000000..1d1a1e3d --- /dev/null +++ b/package/secubox/luci-app-mitmproxy/htdocs/luci-static/resources/view/mitmproxy/settings.js @@ -0,0 +1,193 @@ +'use strict'; +'require view'; +'require form'; +'require uci'; +'require mitmproxy.api as api'; +'require secubox-theme/theme as Theme'; +'require secubox-portal/header as SbHeader'; + +var lang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: lang }); + +var MITMPROXY_NAV = [ + { id: 'dashboard', icon: '📊', label: 'Dashboard' }, + { id: 'requests', icon: '🔍', label: 'Requests' }, + { id: 'settings', icon: '⚙️', label: 'Settings' } +]; + +function renderMitmproxyNav(activeId) { + return E('div', { + 'class': 'mp-app-nav', + 'style': 'display:flex;gap:8px;margin-bottom:20px;padding:12px 16px;background:#141419;border:1px solid rgba(255,255,255,0.08);border-radius:12px;' + }, MITMPROXY_NAV.map(function(item) { + var isActive = activeId === item.id; + return E('a', { + 'href': L.url('admin', 'secubox', 'mitmproxy', item.id), + 'style': 'display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:8px;text-decoration:none;font-size:14px;font-weight:500;transition:all 0.2s;' + + (isActive ? 'background:linear-gradient(135deg,#e74c3c,#c0392b);color:white;' : 'color:#a0a0b0;background:transparent;') + }, [ + E('span', {}, item.icon), + E('span', {}, _(item.label)) + ]); + })); +} + +return view.extend({ + title: _('mitmproxy Settings'), + + load: function() { + return uci.load('mitmproxy'); + }, + + render: function() { + var m, s, o; + + m = new form.Map('mitmproxy', _('mitmproxy Settings'), + _('Configure the mitmproxy HTTPS interception proxy.')); + + // Main settings + s = m.section(form.TypedSection, 'mitmproxy', _('Proxy Configuration')); + s.anonymous = true; + s.addremove = false; + + o = s.option(form.Flag, 'enabled', _('Enable'), + _('Enable mitmproxy at startup')); + o.default = '0'; + o.rmempty = false; + + o = s.option(form.ListValue, 'mode', _('Proxy Mode'), + _('How clients connect to the proxy')); + o.value('transparent', _('Transparent - Intercept traffic automatically')); + o.value('regular', _('Regular - Clients must configure proxy settings')); + o.value('upstream', _('Upstream - Forward to another proxy')); + o.default = 'transparent'; + + o = s.option(form.Value, 'listen_host', _('Listen Address'), + _('IP address to bind the proxy to')); + o.default = '0.0.0.0'; + o.placeholder = '0.0.0.0'; + o.datatype = 'ipaddr'; + + o = s.option(form.Value, 'listen_port', _('Proxy Port'), + _('Port for HTTP/HTTPS interception')); + o.default = '8080'; + o.placeholder = '8080'; + o.datatype = 'port'; + + o = s.option(form.Value, 'web_host', _('Web UI Address'), + _('IP address for mitmweb interface')); + o.default = '0.0.0.0'; + o.placeholder = '0.0.0.0'; + o.datatype = 'ipaddr'; + + o = s.option(form.Value, 'web_port', _('Web UI Port'), + _('Port for mitmweb interface')); + o.default = '8081'; + o.placeholder = '8081'; + o.datatype = 'port'; + + o = s.option(form.Flag, 'ssl_insecure', _('Allow Insecure SSL'), + _('Accept invalid/self-signed SSL certificates from upstream servers')); + o.default = '0'; + + o = s.option(form.ListValue, 'flow_detail', _('Log Detail Level'), + _('Amount of detail in flow logs')); + o.value('0', _('Minimal')); + o.value('1', _('Summary')); + o.value('2', _('Full headers')); + o.value('3', _('Full headers + body preview')); + o.value('4', _('Full headers + full body')); + o.default = '2'; + + // Capture settings + s = m.section(form.TypedSection, 'capture', _('Capture Settings')); + s.anonymous = true; + s.addremove = false; + + o = s.option(form.Flag, 'save_flows', _('Save Flows'), + _('Save captured flows to disk for later replay')); + o.default = '1'; + + o = s.option(form.Value, 'flow_file', _('Flow File'), + _('Path to save captured flows')); + o.default = '/tmp/mitmproxy/flows.bin'; + o.depends('save_flows', '1'); + + o = s.option(form.Flag, 'capture_urls', _('Capture URLs'), + _('Log full URLs of requests')); + o.default = '1'; + + o = s.option(form.Flag, 'capture_cookies', _('Capture Cookies'), + _('Log cookie headers')); + o.default = '1'; + + o = s.option(form.Flag, 'capture_headers', _('Capture Headers'), + _('Log all HTTP headers')); + o.default = '1'; + + o = s.option(form.Flag, 'capture_body', _('Capture Body'), + _('Log request/response bodies (increases storage usage)')); + o.default = '0'; + + // Logging settings + s = m.section(form.TypedSection, 'logging', _('Logging')); + s.anonymous = true; + s.addremove = false; + + o = s.option(form.Flag, 'enabled', _('Enable Request Logging'), + _('Log requests to file')); + o.default = '1'; + + o = s.option(form.Value, 'log_file', _('Log File'), + _('Path to request log file')); + o.default = '/tmp/mitmproxy/requests.log'; + o.depends('enabled', '1'); + + o = s.option(form.ListValue, 'log_format', _('Log Format'), + _('Format of log entries')); + o.value('json', _('JSON')); + o.value('text', _('Plain text')); + o.default = 'json'; + o.depends('enabled', '1'); + + o = s.option(form.Value, 'max_size', _('Max Log Size (MB)'), + _('Rotate log when it reaches this size')); + o.default = '10'; + o.datatype = 'uinteger'; + o.depends('enabled', '1'); + + // Filter settings + s = m.section(form.TypedSection, 'filter', _('Filtering')); + s.anonymous = true; + s.addremove = false; + + o = s.option(form.Flag, 'enabled', _('Enable Filtering'), + _('Enable content filtering')); + o.default = '0'; + + o = s.option(form.Flag, 'block_ads', _('Block Ads'), + _('Block known advertising domains')); + o.default = '0'; + o.depends('enabled', '1'); + + o = s.option(form.Flag, 'block_trackers', _('Block Trackers'), + _('Block known tracking domains')); + o.default = '0'; + o.depends('enabled', '1'); + + o = s.option(form.DynamicList, 'ignore_host', _('Ignore Hosts'), + _('Hosts to pass through without interception')); + o.placeholder = '*.example.com'; + + var wrapper = E('div', { 'class': 'secubox-page-wrapper' }); + wrapper.appendChild(SbHeader.render()); + wrapper.appendChild(renderMitmproxyNav('settings')); + + return m.render().then(function(mapEl) { + wrapper.appendChild(mapEl); + return wrapper; + }); + } +}); diff --git a/package/secubox/luci-app-mitmproxy/root/usr/libexec/rpcd/luci.mitmproxy b/package/secubox/luci-app-mitmproxy/root/usr/libexec/rpcd/luci.mitmproxy new file mode 100644 index 00000000..ef62920e --- /dev/null +++ b/package/secubox/luci-app-mitmproxy/root/usr/libexec/rpcd/luci.mitmproxy @@ -0,0 +1,303 @@ +#!/bin/sh +# +# RPCD backend for mitmproxy LuCI interface +# Copyright (C) 2025 CyberMind.fr (SecuBox) +# + +. /lib/functions.sh + +CONF_DIR=/etc/mitmproxy +DATA_DIR=/tmp/mitmproxy +LOG_FILE=/tmp/mitmproxy/requests.log +FLOW_FILE=/tmp/mitmproxy/flows.bin + +# JSON helpers +json_init() { echo "{"; } +json_close() { echo "}"; } +json_add_string() { printf '"%s":"%s"' "$1" "$2"; } +json_add_int() { printf '"%s":%d' "$1" "${2:-0}"; } +json_add_bool() { [ "$2" = "1" ] && printf '"%s":true' "$1" || printf '"%s":false' "$1"; } + +# Get service status +get_status() { + local running=0 + local pid="" + local mode="unknown" + local web_url="" + + if pgrep mitmweb >/dev/null 2>&1; then + running=1 + pid=$(pgrep mitmweb | head -1) + mode="mitmweb" + elif pgrep mitmdump >/dev/null 2>&1; then + running=1 + pid=$(pgrep mitmdump | head -1) + mode="mitmdump" + fi + + local enabled=$(uci -q get mitmproxy.main.enabled || echo "0") + local listen_port=$(uci -q get mitmproxy.main.listen_port || echo "8080") + local web_port=$(uci -q get mitmproxy.main.web_port || echo "8081") + local proxy_mode=$(uci -q get mitmproxy.main.mode || echo "transparent") + local router_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1") + + [ "$running" = "1" ] && [ "$mode" = "mitmweb" ] && web_url="http://${router_ip}:${web_port}" + + cat </dev/null || echo "0") + if command -v jq >/dev/null 2>&1; then + unique_hosts=$(jq -r '.request.host // .host // empty' "$LOG_FILE" 2>/dev/null | sort -u | wc -l) + fi + fi + + if [ -f "$FLOW_FILE" ]; then + flow_size=$(ls -l "$FLOW_FILE" 2>/dev/null | awk '{print $5}' || echo "0") + fi + + cat </dev/null 2>&1; then + echo '{"requests":' + tail -"$limit" "$LOG_FILE" 2>/dev/null | jq -s '.' 2>/dev/null || echo '[]' + echo '}' + else + echo '{"requests":[]}' + fi +} + +# Get top hosts +get_top_hosts() { + local limit="${1:-20}" + + if [ ! -f "$LOG_FILE" ] || ! command -v jq >/dev/null 2>&1; then + echo '{"hosts":[]}' + return + fi + + echo '{"hosts":[' + jq -r '.request.host // .host // "unknown"' "$LOG_FILE" 2>/dev/null | \ + sort | uniq -c | sort -rn | head -"$limit" | \ + awk 'BEGIN{first=1} { + if(!first) printf ","; + first=0; + gsub(/"/, "\\\"", $2); + printf "{\"host\":\"%s\",\"count\":%d}", $2, $1 + }' + echo ']}' +} + +# Service control +service_start() { + /etc/init.d/mitmproxy start >/dev/null 2>&1 + sleep 2 + get_status +} + +service_stop() { + /etc/init.d/mitmproxy stop >/dev/null 2>&1 + sleep 1 + get_status +} + +service_restart() { + /etc/init.d/mitmproxy restart >/dev/null 2>&1 + sleep 2 + get_status +} + +# Set configuration +set_config() { + local key="$1" + local value="$2" + local section="main" + + case "$key" in + save_flows|capture_*) + section="capture" + ;; + esac + + uci set "mitmproxy.$section.$key=$value" + uci commit mitmproxy + echo '{"success":true}' +} + +# Clear captured data +clear_data() { + rm -f "$DATA_DIR"/*.log "$DATA_DIR"/*.bin 2>/dev/null + echo '{"success":true,"message":"Captured data cleared"}' +} + +# Get CA certificate info +get_ca_info() { + local cert="$CONF_DIR/mitmproxy-ca-cert.pem" + local router_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1") + local web_port=$(uci -q get mitmproxy.main.web_port || echo "8081") + + if [ -f "$cert" ]; then + local subject=$(openssl x509 -in "$cert" -noout -subject 2>/dev/null | sed 's/subject=//') + local expires=$(openssl x509 -in "$cert" -noout -enddate 2>/dev/null | sed 's/notAfter=//') + + cat </dev/null || echo "50") + get_requests "$limit" + ;; + get_top_hosts) + read -r input + limit=$(echo "$input" | jsonfilter -e '@.limit' 2>/dev/null || echo "20") + get_top_hosts "$limit" + ;; + get_ca_info) + get_ca_info + ;; + service_start) + service_start + ;; + service_stop) + service_stop + ;; + service_restart) + service_restart + ;; + set_config) + read -r input + key=$(echo "$input" | jsonfilter -e '@.key' 2>/dev/null) + value=$(echo "$input" | jsonfilter -e '@.value' 2>/dev/null) + set_config "$key" "$value" + ;; + clear_data) + clear_data + ;; + *) + echo '{"error":"Unknown method"}' + ;; + esac + ;; + *) + echo '{"error":"Unknown command"}' + ;; +esac diff --git a/package/secubox/luci-app-mitmproxy/root/usr/share/luci/menu.d/luci-app-mitmproxy.json b/package/secubox/luci-app-mitmproxy/root/usr/share/luci/menu.d/luci-app-mitmproxy.json new file mode 100644 index 00000000..cc868029 --- /dev/null +++ b/package/secubox/luci-app-mitmproxy/root/usr/share/luci/menu.d/luci-app-mitmproxy.json @@ -0,0 +1,38 @@ +{ + "admin/secubox/mitmproxy": { + "title": "mitmproxy", + "order": 45, + "action": { + "type": "view", + "path": "mitmproxy/dashboard" + }, + "depends": { + "acl": ["luci-app-mitmproxy"], + "uci": {"mitmproxy": true} + } + }, + "admin/secubox/mitmproxy/dashboard": { + "title": "Dashboard", + "order": 10, + "action": { + "type": "view", + "path": "mitmproxy/dashboard" + } + }, + "admin/secubox/mitmproxy/requests": { + "title": "Requests", + "order": 20, + "action": { + "type": "view", + "path": "mitmproxy/requests" + } + }, + "admin/secubox/mitmproxy/settings": { + "title": "Settings", + "order": 30, + "action": { + "type": "view", + "path": "mitmproxy/settings" + } + } +} diff --git a/package/secubox/luci-app-mitmproxy/root/usr/share/rpcd/acl.d/luci-app-mitmproxy.json b/package/secubox/luci-app-mitmproxy/root/usr/share/rpcd/acl.d/luci-app-mitmproxy.json new file mode 100644 index 00000000..74c41806 --- /dev/null +++ b/package/secubox/luci-app-mitmproxy/root/usr/share/rpcd/acl.d/luci-app-mitmproxy.json @@ -0,0 +1,34 @@ +{ + "luci-app-mitmproxy": { + "description": "Grant access to mitmproxy LuCI app", + "read": { + "ubus": { + "luci.mitmproxy": [ + "get_status", + "get_config", + "get_stats", + "get_requests", + "get_top_hosts", + "get_ca_info" + ] + }, + "uci": [ + "mitmproxy" + ] + }, + "write": { + "ubus": { + "luci.mitmproxy": [ + "service_start", + "service_stop", + "service_restart", + "set_config", + "clear_data" + ] + }, + "uci": [ + "mitmproxy" + ] + } + } +} diff --git a/package/secubox/secubox-app-mitmproxy/Makefile b/package/secubox/secubox-app-mitmproxy/Makefile new file mode 100644 index 00000000..9d70357b --- /dev/null +++ b/package/secubox/secubox-app-mitmproxy/Makefile @@ -0,0 +1,121 @@ +# +# Copyright (C) 2025 CyberMind.fr (SecuBox) +# +# This is free software, licensed under the MIT License. +# +# mitmproxy - Interactive HTTPS proxy for traffic inspection +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=secubox-app-mitmproxy +PKG_VERSION:=11.1.2 +PKG_RELEASE:=1 + +# Download mitmproxy standalone binary +PKG_SOURCE:=mitmproxy-$(PKG_VERSION)-linux-arm64.tar.gz +PKG_SOURCE_URL:=https://downloads.mitmproxy.org/$(PKG_VERSION)/ +PKG_HASH:=skip + +PKG_MAINTAINER:=CyberMind +PKG_LICENSE:=MIT +PKG_LICENSE_FILES:=LICENSE + +include $(INCLUDE_DIR)/package.mk + +define Package/secubox-app-mitmproxy + SECTION:=net + CATEGORY:=Network + SUBMENU:=SecuBox Apps + TITLE:=mitmproxy - Interactive HTTPS Proxy + URL:=https://mitmproxy.org/ + DEPENDS:=@(aarch64||arm) +ca-bundle +libopenssl + PKGARCH:=aarch64_cortex-a72 +endef + +define Package/secubox-app-mitmproxy/description + mitmproxy is a free and open source interactive HTTPS proxy. + It can intercept, inspect, modify and replay HTTP/HTTPS traffic. + + Features: + - Intercept and modify HTTP/HTTPS traffic + - Web-based interface (mitmweb) + - Scripting API for automation + - SSL/TLS certificate generation + - Request/response inspection + - URL and cookie capture + + Use cases: + - Security testing and penetration testing + - API debugging and development + - Network traffic analysis + - Parental controls and content filtering +endef + +define Package/secubox-app-mitmproxy/conffiles +/etc/config/mitmproxy +endef + +define Build/Prepare + mkdir -p $(PKG_BUILD_DIR) + $(TAR) -xzf $(DL_DIR)/$(PKG_SOURCE) -C $(PKG_BUILD_DIR) +endef + +define Build/Compile +endef + +define Package/secubox-app-mitmproxy/install + # Binaries + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) $(PKG_BUILD_DIR)/mitmproxy $(1)/usr/bin/ + $(INSTALL_BIN) $(PKG_BUILD_DIR)/mitmdump $(1)/usr/bin/ + $(INSTALL_BIN) $(PKG_BUILD_DIR)/mitmweb $(1)/usr/bin/ + + # Config + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./files/etc/config/mitmproxy $(1)/etc/config/mitmproxy + + # Init script + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/etc/init.d/mitmproxy $(1)/etc/init.d/mitmproxy + + # Controller script + $(INSTALL_DIR) $(1)/usr/sbin + $(INSTALL_BIN) ./files/usr/sbin/mitmproxyctl $(1)/usr/sbin/mitmproxyctl + + # CA certificate directory + $(INSTALL_DIR) $(1)/etc/mitmproxy +endef + +define Package/secubox-app-mitmproxy/postinst +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] || { + # Create data directory + mkdir -p /var/lib/mitmproxy + mkdir -p /tmp/mitmproxy + + # Generate CA certificate if not exists + if [ ! -f /etc/mitmproxy/mitmproxy-ca.pem ]; then + echo "Generating mitmproxy CA certificate..." + /usr/bin/mitmdump --set confdir=/etc/mitmproxy -q & + sleep 3 + killall mitmdump 2>/dev/null + fi + + /etc/init.d/mitmproxy enable + echo "mitmproxy installed. Start with: /etc/init.d/mitmproxy start" + echo "Web interface at: http://router:8081" +} +exit 0 +endef + +define Package/secubox-app-mitmproxy/prerm +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] || { + /etc/init.d/mitmproxy stop + /etc/init.d/mitmproxy disable +} +exit 0 +endef + +$(eval $(call BuildPackage,secubox-app-mitmproxy)) diff --git a/package/secubox/secubox-app-mitmproxy/files/etc/config/mitmproxy b/package/secubox/secubox-app-mitmproxy/files/etc/config/mitmproxy new file mode 100644 index 00000000..ebd61cd0 --- /dev/null +++ b/package/secubox/secubox-app-mitmproxy/files/etc/config/mitmproxy @@ -0,0 +1,32 @@ +config mitmproxy 'main' + option enabled '0' + option mode 'transparent' + option listen_host '0.0.0.0' + option listen_port '8080' + option web_port '8081' + option web_host '0.0.0.0' + option confdir '/etc/mitmproxy' + option ssl_insecure '0' + option showhost '1' + option flow_detail '2' + +config logging 'logging' + option enabled '1' + option log_file '/tmp/mitmproxy/requests.log' + option log_format 'json' + option max_size '10' + +config capture 'capture' + option save_flows '1' + option flow_file '/tmp/mitmproxy/flows.bin' + option capture_urls '1' + option capture_cookies '1' + option capture_headers '1' + option capture_body '0' + +config filter 'filter' + option enabled '0' + option block_ads '0' + option block_trackers '0' + list ignore_host 'localhost' + list ignore_host '*.local' diff --git a/package/secubox/secubox-app-mitmproxy/files/etc/init.d/mitmproxy b/package/secubox/secubox-app-mitmproxy/files/etc/init.d/mitmproxy new file mode 100644 index 00000000..7f4b72f4 --- /dev/null +++ b/package/secubox/secubox-app-mitmproxy/files/etc/init.d/mitmproxy @@ -0,0 +1,150 @@ +#!/bin/sh /etc/rc.common +# +# mitmproxy init script for OpenWrt +# Copyright (C) 2025 CyberMind.fr (SecuBox) +# + +START=95 +STOP=10 +USE_PROCD=1 + +PROG=/usr/bin/mitmweb +CONF_DIR=/etc/mitmproxy +PID_FILE=/var/run/mitmproxy.pid + +validate_section() { + uci_load_validate mitmproxy main "$1" "$2" \ + 'enabled:bool:0' \ + 'mode:string:transparent' \ + 'listen_host:string:0.0.0.0' \ + 'listen_port:port:8080' \ + 'web_port:port:8081' \ + 'web_host:string:0.0.0.0' \ + 'confdir:string:/etc/mitmproxy' \ + 'ssl_insecure:bool:0' \ + 'showhost:bool:1' \ + 'flow_detail:range(0,4):2' +} + +start_mitmproxy() { + [ "$2" = 0 ] || { + echo "mitmproxy: validation failed" >&2 + return 1 + } + + [ "$enabled" = "1" ] || { + echo "mitmproxy: disabled in config" + return 0 + } + + # Create directories + mkdir -p /tmp/mitmproxy + mkdir -p /var/lib/mitmproxy + + procd_open_instance mitmproxy + procd_set_param command $PROG + + # Core options + procd_append_param command --set confdir="$confdir" + procd_append_param command --listen-host "$listen_host" + procd_append_param command --listen-port "$listen_port" + procd_append_param command --web-host "$web_host" + procd_append_param command --web-port "$web_port" + procd_append_param command --set flow_detail="$flow_detail" + + # Mode + case "$mode" in + transparent) + procd_append_param command --mode transparent + ;; + regular) + procd_append_param command --mode regular + ;; + upstream) + procd_append_param command --mode upstream + ;; + esac + + # SSL options + [ "$ssl_insecure" = "1" ] && procd_append_param command --ssl-insecure + [ "$showhost" = "1" ] && procd_append_param command --showhost + + # Capture options + local save_flows flow_file + config_get save_flows capture save_flows 0 + config_get flow_file capture flow_file "/tmp/mitmproxy/flows.bin" + [ "$save_flows" = "1" ] && procd_append_param command -w "$flow_file" + + procd_set_param respawn + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_set_param pidfile $PID_FILE + + procd_close_instance + + # Setup iptables rules for transparent mode + [ "$mode" = "transparent" ] && setup_iptables "$listen_port" +} + +setup_iptables() { + local port="$1" + + # Remove existing rules first + cleanup_iptables + + # Get LAN interface + local lan_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1") + + # Redirect HTTP traffic + iptables -t nat -A PREROUTING -i br-lan -p tcp --dport 80 \ + -j REDIRECT --to-port "$port" 2>/dev/null + + # Redirect HTTPS traffic + iptables -t nat -A PREROUTING -i br-lan -p tcp --dport 443 \ + -j REDIRECT --to-port "$port" 2>/dev/null + + # Mark mitmproxy traffic + iptables -t nat -I PREROUTING -p tcp -m mark --mark 0x1/0x1 -j ACCEPT 2>/dev/null +} + +cleanup_iptables() { + # Remove mitmproxy redirect rules + iptables -t nat -D PREROUTING -i br-lan -p tcp --dport 80 \ + -j REDIRECT --to-port 8080 2>/dev/null + iptables -t nat -D PREROUTING -i br-lan -p tcp --dport 443 \ + -j REDIRECT --to-port 8080 2>/dev/null + iptables -t nat -D PREROUTING -p tcp -m mark --mark 0x1/0x1 -j ACCEPT 2>/dev/null +} + +start_service() { + config_load mitmproxy + config_foreach validate_section main start_mitmproxy +} + +stop_service() { + cleanup_iptables +} + +reload_service() { + stop + start +} + +service_triggers() { + procd_add_reload_trigger "mitmproxy" +} + +status() { + if pgrep mitmweb >/dev/null 2>&1; then + echo "mitmproxy is running" + pgrep mitmweb + return 0 + elif pgrep mitmdump >/dev/null 2>&1; then + echo "mitmdump is running" + pgrep mitmdump + return 0 + else + echo "mitmproxy is not running" + return 1 + fi +} diff --git a/package/secubox/secubox-app-mitmproxy/files/usr/sbin/mitmproxyctl b/package/secubox/secubox-app-mitmproxy/files/usr/sbin/mitmproxyctl new file mode 100644 index 00000000..29d1b8c5 --- /dev/null +++ b/package/secubox/secubox-app-mitmproxy/files/usr/sbin/mitmproxyctl @@ -0,0 +1,236 @@ +#!/bin/sh +# +# mitmproxyctl - mitmproxy management utility +# Copyright (C) 2025 CyberMind.fr (SecuBox) +# + +CONF_DIR=/etc/mitmproxy +DATA_DIR=/tmp/mitmproxy +LOG_FILE=/tmp/mitmproxy/requests.log + +usage() { + cat < [options] + +Commands: + status Show service status + start Start mitmproxy + stop Stop mitmproxy + restart Restart mitmproxy + enable Enable at boot + disable Disable at boot + logs Show recent logs + flows List captured flows + clear Clear captured data + ca-cert Show CA certificate path + install-ca Install CA cert instructions + stats Show traffic statistics + +Options: + -h, --help Show this help message +EOF +} + +cmd_status() { + if pgrep mitmweb >/dev/null 2>&1; then + echo "Status: Running (mitmweb)" + echo "PID: $(pgrep mitmweb)" + echo "Web UI: http://$(uci -q get network.lan.ipaddr || echo '192.168.1.1'):$(uci -q get mitmproxy.main.web_port || echo '8081')" + elif pgrep mitmdump >/dev/null 2>&1; then + echo "Status: Running (mitmdump)" + echo "PID: $(pgrep mitmdump)" + else + echo "Status: Stopped" + fi + + echo "" + echo "Configuration:" + echo " Mode: $(uci -q get mitmproxy.main.mode || echo 'transparent')" + echo " Listen: $(uci -q get mitmproxy.main.listen_host || echo '0.0.0.0'):$(uci -q get mitmproxy.main.listen_port || echo '8080')" + echo " Enabled: $(uci -q get mitmproxy.main.enabled || echo '0')" +} + +cmd_start() { + echo "Starting mitmproxy..." + /etc/init.d/mitmproxy start +} + +cmd_stop() { + echo "Stopping mitmproxy..." + /etc/init.d/mitmproxy stop +} + +cmd_restart() { + echo "Restarting mitmproxy..." + /etc/init.d/mitmproxy restart +} + +cmd_enable() { + uci set mitmproxy.main.enabled='1' + uci commit mitmproxy + /etc/init.d/mitmproxy enable + echo "mitmproxy enabled at boot" +} + +cmd_disable() { + uci set mitmproxy.main.enabled='0' + uci commit mitmproxy + /etc/init.d/mitmproxy disable + echo "mitmproxy disabled at boot" +} + +cmd_logs() { + if [ -f "$LOG_FILE" ]; then + tail -50 "$LOG_FILE" + else + echo "No logs available at $LOG_FILE" + fi +} + +cmd_flows() { + local flow_file=$(uci -q get mitmproxy.capture.flow_file || echo "/tmp/mitmproxy/flows.bin") + if [ -f "$flow_file" ]; then + echo "Flow file: $flow_file" + echo "Size: $(ls -lh "$flow_file" | awk '{print $5}')" + echo "" + echo "Use 'mitmproxy -r $flow_file' to replay flows" + else + echo "No flow file found" + fi +} + +cmd_clear() { + echo "Clearing captured data..." + rm -f "$DATA_DIR"/*.log "$DATA_DIR"/*.bin + echo "Done" +} + +cmd_ca_cert() { + local cert="$CONF_DIR/mitmproxy-ca-cert.pem" + if [ -f "$cert" ]; then + echo "CA Certificate: $cert" + echo "" + echo "Certificate details:" + openssl x509 -in "$cert" -noout -subject -issuer -dates 2>/dev/null || \ + cat "$cert" + else + echo "CA certificate not found" + echo "Start mitmproxy once to generate the certificate" + fi +} + +cmd_install_ca() { + local cert="$CONF_DIR/mitmproxy-ca-cert.pem" + local router_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1") + + cat < Security > Install from storage + - Select the certificate file + + iOS: + - Email the cert and open it + - Settings > General > Profile > Install + - Settings > General > About > Certificate Trust Settings + +EOF +} + +cmd_stats() { + echo "=== mitmproxy Statistics ===" + echo "" + + if [ -f "$LOG_FILE" ]; then + local total=$(wc -l < "$LOG_FILE" 2>/dev/null || echo "0") + echo "Total requests logged: $total" + + if command -v jq >/dev/null 2>&1; then + echo "" + echo "Top 10 hosts:" + jq -r '.request.host // .host // "unknown"' "$LOG_FILE" 2>/dev/null | \ + sort | uniq -c | sort -rn | head -10 + + echo "" + echo "Request methods:" + jq -r '.request.method // .method // "GET"' "$LOG_FILE" 2>/dev/null | \ + sort | uniq -c | sort -rn + fi + else + echo "No statistics available" + fi +} + +# Parse arguments +case "$1" in + status) + cmd_status + ;; + start) + cmd_start + ;; + stop) + cmd_stop + ;; + restart) + cmd_restart + ;; + enable) + cmd_enable + ;; + disable) + cmd_disable + ;; + logs) + cmd_logs + ;; + flows) + cmd_flows + ;; + clear) + cmd_clear + ;; + ca-cert|ca|cert) + cmd_ca_cert + ;; + install-ca|install) + cmd_install_ca + ;; + stats|statistics) + cmd_stats + ;; + -h|--help|help) + usage + ;; + *) + usage + exit 1 + ;; +esac + +exit 0