feat: Add mitmproxy HTTPS interception proxy packages

New packages for full URL/cookie/header capture via MITM proxy:

secubox-app-mitmproxy:
- Downloads mitmproxy v11.1.2 binary for aarch64
- Transparent proxy mode with iptables integration
- mitmweb UI on port 8081
- Auto CA certificate generation
- mitmproxyctl CLI management tool

luci-app-mitmproxy:
- SecuBox themed dashboard with red color scheme
- Real-time request capture view
- Top hosts statistics
- CA certificate management
- Full UCI settings interface
- RPCD backend for ubus API

This enables full HTTP/HTTPS inspection including:
- Complete URLs (not just hostnames like nDPId)
- Cookies and headers
- Request/response bodies
- Flow recording for replay

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-10 14:25:12 +01:00
parent 03552c55e9
commit 6c4257f950
13 changed files with 2449 additions and 0 deletions

View File

@ -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 <contact@cybermind.fr>
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))

View File

@ -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();
}
};

View File

@ -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);
}
}

View File

@ -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 = '<span class="mp-status-dot"></span>' +
(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
});

View File

@ -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 = '<div class="mp-empty"><div class="mp-empty-icon">🔍</div><div class="mp-empty-text">No requests captured</div><p>Generate HTTP traffic to see requests</p></div>';
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
});

View File

@ -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;
});
}
});

View File

@ -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 <<EOF
{
"running": $([ "$running" = "1" ] && echo "true" || echo "false"),
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
"pid": ${pid:-0},
"mode": "$mode",
"proxy_mode": "$proxy_mode",
"listen_port": $listen_port,
"web_port": $web_port,
"web_url": "$web_url",
"ca_installed": $([ -f "$CONF_DIR/mitmproxy-ca-cert.pem" ] && echo "true" || echo "false")
}
EOF
}
# Get configuration
get_config() {
local enabled=$(uci -q get mitmproxy.main.enabled || echo "0")
local mode=$(uci -q get mitmproxy.main.mode || echo "transparent")
local listen_host=$(uci -q get mitmproxy.main.listen_host || echo "0.0.0.0")
local listen_port=$(uci -q get mitmproxy.main.listen_port || echo "8080")
local web_host=$(uci -q get mitmproxy.main.web_host || echo "0.0.0.0")
local web_port=$(uci -q get mitmproxy.main.web_port || echo "8081")
local ssl_insecure=$(uci -q get mitmproxy.main.ssl_insecure || echo "0")
local flow_detail=$(uci -q get mitmproxy.main.flow_detail || echo "2")
local save_flows=$(uci -q get mitmproxy.capture.save_flows || echo "1")
local capture_urls=$(uci -q get mitmproxy.capture.capture_urls || echo "1")
local capture_cookies=$(uci -q get mitmproxy.capture.capture_cookies || echo "1")
local capture_headers=$(uci -q get mitmproxy.capture.capture_headers || echo "1")
local capture_body=$(uci -q get mitmproxy.capture.capture_body || echo "0")
cat <<EOF
{
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
"mode": "$mode",
"listen_host": "$listen_host",
"listen_port": $listen_port,
"web_host": "$web_host",
"web_port": $web_port,
"ssl_insecure": $([ "$ssl_insecure" = "1" ] && echo "true" || echo "false"),
"flow_detail": $flow_detail,
"save_flows": $([ "$save_flows" = "1" ] && echo "true" || echo "false"),
"capture_urls": $([ "$capture_urls" = "1" ] && echo "true" || echo "false"),
"capture_cookies": $([ "$capture_cookies" = "1" ] && echo "true" || echo "false"),
"capture_headers": $([ "$capture_headers" = "1" ] && echo "true" || echo "false"),
"capture_body": $([ "$capture_body" = "1" ] && echo "true" || echo "false")
}
EOF
}
# Get statistics
get_stats() {
local total_requests=0
local unique_hosts=0
local flow_size="0"
if [ -f "$LOG_FILE" ]; then
total_requests=$(wc -l < "$LOG_FILE" 2>/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 <<EOF
{
"total_requests": $total_requests,
"unique_hosts": $unique_hosts,
"flow_file_size": $flow_size
}
EOF
}
# Get recent requests
get_requests() {
local limit="${1:-50}"
if [ ! -f "$LOG_FILE" ]; then
echo '{"requests":[]}'
return
fi
if command -v jq >/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 <<EOF
{
"installed": true,
"path": "$cert",
"subject": "$subject",
"expires": "$expires",
"download_url": "http://$router_ip:$web_port/cert"
}
EOF
else
cat <<EOF
{
"installed": false,
"path": "$cert",
"download_url": ""
}
EOF
fi
}
# RPCD list method
case "$1" in
list)
cat <<EOF
{
"get_status": {},
"get_config": {},
"get_stats": {},
"get_requests": {"limit": 50},
"get_top_hosts": {"limit": 20},
"get_ca_info": {},
"service_start": {},
"service_stop": {},
"service_restart": {},
"set_config": {"key": "string", "value": "string"},
"clear_data": {}
}
EOF
;;
call)
case "$2" in
get_status)
get_status
;;
get_config)
get_config
;;
get_stats)
get_stats
;;
get_requests)
read -r input
limit=$(echo "$input" | jsonfilter -e '@.limit' 2>/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

View File

@ -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"
}
}
}

View File

@ -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"
]
}
}
}

View File

@ -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 <contact@cybermind.fr>
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))

View File

@ -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'

View File

@ -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
}

View File

@ -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 <<EOF
mitmproxyctl - mitmproxy management utility
Usage: mitmproxyctl <command> [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 <<EOF
=== Installing mitmproxy CA Certificate ===
To intercept HTTPS traffic, clients must trust the mitmproxy CA.
1. Access the certificate at:
http://$router_ip:$(uci -q get mitmproxy.main.web_port || echo '8081')/cert
2. Or download directly:
scp root@$router_ip:$cert ./mitmproxy-ca.pem
3. Install on devices:
Windows:
- Double-click the .pem file
- Install to "Trusted Root Certification Authorities"
macOS:
- Double-click to add to Keychain
- In Keychain Access, find the cert and set "Always Trust"
Linux:
- Copy to /usr/local/share/ca-certificates/
- Run: sudo update-ca-certificates
Android:
- Settings > 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