- Add get_web_token RPCD method to retrieve auth token - Create webui.js view that embeds mitmweb in an iframe - Capture auth token at startup and save to file - Add Web UI navigation to all mitmproxy views - Fix PATH for /usr/local/bin in Docker image - Change default port from 8080 to 8888 (avoid CrowdSec conflict) secubox-app-mitmproxy: bump to r12 luci-app-mitmproxy: bump to r2 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
367 lines
12 KiB
JavaScript
367 lines
12 KiB
JavaScript
'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: 'webui', icon: '🖥️', label: 'Web UI' },
|
|
{ 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', 'security', '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
|
|
});
|