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:
parent
03552c55e9
commit
6c4257f950
64
package/secubox/luci-app-mitmproxy/Makefile
Normal file
64
package/secubox/luci-app-mitmproxy/Makefile
Normal 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))
|
||||
@ -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();
|
||||
}
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
});
|
||||
@ -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
|
||||
});
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -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
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
121
package/secubox/secubox-app-mitmproxy/Makefile
Normal file
121
package/secubox/secubox-app-mitmproxy/Makefile
Normal 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))
|
||||
@ -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'
|
||||
150
package/secubox/secubox-app-mitmproxy/files/etc/init.d/mitmproxy
Normal file
150
package/secubox/secubox-app-mitmproxy/files/etc/init.d/mitmproxy
Normal 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
|
||||
}
|
||||
@ -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
|
||||
Loading…
Reference in New Issue
Block a user