This commit is contained in:
devdatt 2025-12-02 14:11:51 +05:30
parent fb2ac26ab0
commit 48b51acbc8
2 changed files with 518 additions and 9 deletions

510
encoder/firewall.php Normal file
View File

@ -0,0 +1,510 @@
<?php
/******************************************************
* firewall_manager.php
* Simple web UI to manage UFW firewall rules.
*
* IMPORTANT:
* - Protect this file (HTTP auth / VPN / IP allowlist).
* - PHP must run as root OR allow ufw in sudoers, e.g.:
* www-data ALL=(ALL) NOPASSWD:/usr/sbin/ufw
******************************************************/
// If ufw is not in this path, adjust:
define('UFW_BIN', '/usr/sbin/ufw');
// Set to true to only preview commands and not actually run them
$DRY_RUN = false;
$messages = [];
$errors = [];
/**
* Validate port number (165535). Returns int or null.
*/
function sanitize_port($port) {
$port = trim($port);
if ($port === '') return null;
if (!ctype_digit($port)) return null;
$p = (int)$port;
if ($p < 1 || $p > 65535) return null;
return $p;
}
/**
* Very basic CIDR/subnet validation.
* Accepts forms like:
* 10.0.0.0/24
* 192.168.1.5
*/
function sanitize_subnet($subnet) {
$subnet = trim($subnet);
if ($subnet === '') return null;
// allow plain IP
if (filter_var($subnet, FILTER_VALIDATE_IP)) {
return $subnet;
}
// allow IP/CIDR
$parts = explode('/', $subnet);
if (count($parts) === 2) {
[$ip, $mask] = $parts;
$ip = trim($ip);
$mask = trim($mask);
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
return null;
}
if (!ctype_digit($mask)) {
return null;
}
$m = (int)$mask;
if ($m < 0 || $m > 32) {
return null;
}
return $ip . '/' . $m;
}
return null;
}
/**
* Run a UFW command and capture output.
*/
function run_ufw_command($cmd, $dryRun = false) {
$cmdline = UFW_BIN . ' ' . $cmd;
if ($dryRun) {
return [
'cmd' => $cmdline,
'output' => ['[DRY RUN] Command not executed'],
'exit_code' => 0,
];
}
$output = [];
$ret = 0;
exec($cmdline . ' 2>&1', $output, $ret);
return [
'cmd' => $cmdline,
'output' => $output,
'exit_code' => $ret,
];
}
// Handle POST
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
if ($action === 'apply_firewall') {
$globalPorts = $_POST['global_ports'] ?? [];
$restrictedPorts = $_POST['restricted_port'] ?? [];
$restrictedSubnets = $_POST['restricted_subnet'] ?? [];
$dryRunChecked = isset($_POST['dry_run']) && $_POST['dry_run'] === '1';
$didAnything = false;
// 1) Open ports for all IP (allow)
foreach ($globalPorts as $rawPort) {
$rawPort = trim($rawPort);
if ($rawPort === '') {
continue;
}
$port = sanitize_port($rawPort);
if ($port === null) {
$errors[] = "Invalid open port: " . htmlspecialchars($rawPort);
continue;
}
$didAnything = true;
$res = run_ufw_command('allow ' . (int)$port, $dryRunChecked || $DRY_RUN);
if ($res['exit_code'] !== 0) {
$errors[] = "Failed to allow port {$port}: " . implode(" ", $res['output']);
} else {
$messages[] = "Allowed port {$port} for all IPs.";
}
}
// 2) Restricted ports with subnets
// Model: Deny port globally, then allow from specified subnets.
$count = max(count($restrictedPorts), count($restrictedSubnets));
for ($i = 0; $i < $count; $i++) {
$rawPort = $restrictedPorts[$i] ?? '';
$rawSubnet = $restrictedSubnets[$i] ?? '';
$rawPort = trim($rawPort);
$rawSubnet = trim($rawSubnet);
if ($rawPort === '' || $rawSubnet === '') {
continue;
}
$port = sanitize_port($rawPort);
$subnet = sanitize_subnet($rawSubnet);
if ($port === null) {
$errors[] = "Invalid restricted port: " . htmlspecialchars($rawPort);
continue;
}
if ($subnet === null) {
$errors[] = "Invalid subnet/CIDR for port {$port}: " . htmlspecialchars($rawSubnet);
continue;
}
$didAnything = true;
// Deny port from everywhere
$denyRes = run_ufw_command('deny ' . (int)$port, $dryRunChecked || $DRY_RUN);
if ($denyRes['exit_code'] !== 0) {
$errors[] = "Failed to deny port {$port}: " . implode(" ", $denyRes['output']);
} else {
$messages[] = "Denied port {$port} for all IPs.";
}
// Allow from subnet
$allowCmd = 'allow from ' . escapeshellarg($subnet) . ' to any port ' . (int)$port;
// We used escapeshellarg() here, but run_ufw_command expects only the ufw arguments,
// so we need to build carefully:
// Rebuild without full path:
$allowCmdForRun = 'allow from ' . $subnet . ' to any port ' . (int)$port;
$allowRes = run_ufw_command($allowCmdForRun, $dryRunChecked || $DRY_RUN);
if ($allowRes['exit_code'] !== 0) {
$errors[] = "Failed to allow port {$port} from {$subnet}: " . implode(" ", $allowRes['output']);
} else {
$messages[] = "Allowed port {$port} only from {$subnet}.";
}
}
if (!$didAnything) {
$errors[] = "No valid firewall rules submitted.";
}
// Optional: reload or enable ufw here
// $reload = run_ufw_command('reload', $dryRunChecked || $DRY_RUN);
}
}
// Get current UFW status
$currentStatus = [];
$statusExit = 0;
if (file_exists(UFW_BIN)) {
exec(UFW_BIN . ' status numbered 2>&1', $currentStatus, $statusExit);
} else {
$currentStatus[] = "UFW binary not found at " . UFW_BIN;
$statusExit = 1;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Firewall Rule Manager</title>
<style>
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f4f5f7;
margin: 0;
padding: 0;
}
.page {
max-width: 1100px;
margin: 24px auto;
padding: 0 16px 32px;
}
h1 {
margin-bottom: 4px;
}
.subtitle {
color: #555;
margin-bottom: 16px;
font-size: 14px;
}
.card {
background: #ffffff;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.12);
}
.card h2 {
margin-top: 0;
font-size: 18px;
margin-bottom: 8px;
}
.card p {
margin-top: 0;
font-size: 13px;
color: #555;
}
.messages {
margin-bottom: 16px;
}
.msg {
padding: 8px 10px;
border-radius: 4px;
margin-bottom: 6px;
font-size: 13px;
}
.msg.success {
background: #e6f4ea;
border: 1px solid #9ad0a6;
color: #225c2e;
}
.msg.error {
background: #fdecea;
border: 1px solid #f5c2c0;
color: #611a15;
}
table {
border-collapse: collapse;
width: 100%;
margin-top: 8px;
margin-bottom: 8px;
}
th, td {
border-bottom: 1px solid #e2e8f0;
padding: 6px 8px;
text-align: left;
font-size: 13px;
}
th {
background: #f9fafb;
font-weight: 600;
}
input[type="text"],
input[type="number"] {
width: 100%;
box-sizing: border-box;
padding: 6px 8px;
font-size: 13px;
border: 1px solid #cbd5e1;
border-radius: 4px;
}
input[type="checkbox"] {
transform: scale(1.1);
}
.btn {
display: inline-block;
padding: 6px 12px;
font-size: 13px;
border-radius: 4px;
border: none;
cursor: pointer;
background: #2563eb;
color: #ffffff;
}
.btn.secondary {
background: #e2e8f0;
color: #111827;
}
.btn.sm {
padding: 4px 10px;
font-size: 12px;
}
.btn + .btn {
margin-left: 6px;
}
.row-actions {
text-align: right;
}
pre {
background: #0b1120;
color: #e2e8f0;
padding: 10px;
border-radius: 4px;
font-size: 12px;
overflow-x: auto;
}
.flex {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.flex-right {
margin-left: auto;
}
.small-note {
font-size: 12px;
color: #64748b;
margin-top: 6px;
}
.section-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
}
</style>
</head>
<body>
<div class="page">
<h1>Firewall Rule Manager</h1>
<div class="subtitle">
Configure simple UFW rules to open ports for all IPs and restrict ports to specific subnets.
Ensure this interface is accessible only to trusted administrators.
</div>
<div class="messages">
<?php foreach ($messages as $m): ?>
<div class="msg success"><?php echo htmlspecialchars($m); ?></div>
<?php endforeach; ?>
<?php foreach ($errors as $e): ?>
<div class="msg error"><?php echo htmlspecialchars($e); ?></div>
<?php endforeach; ?>
</div>
<form method="post">
<input type="hidden" name="action" value="apply_firewall">
<div class="card">
<div class="section-header">
<h2>Open Ports (All IPs)</h2>
<button type="button" class="btn sm secondary" onclick="addGlobalPortRow()">+ Add Port</button>
</div>
<p>Ports in this list will be <strong>allowed from any IP</strong>.</p>
<table id="globalPortsTable">
<thead>
<tr>
<th style="width: 140px;">Port</th>
<th>Comment (optional)</th>
<th style="width: 80px;"></th>
</tr>
</thead>
<tbody>
<!-- Default rows (80, 443, 1935) -->
<tr>
<td><input type="number" name="global_ports[]" value="80" min="1" max="65535"></td>
<td><input type="text" name="global_comment[]" placeholder="HTTP"></td>
<td class="row-actions">
<button type="button" class="btn sm secondary" onclick="removeRow(this)">Remove</button>
</td>
</tr>
<tr>
<td><input type="number" name="global_ports[]" value="443" min="1" max="65535"></td>
<td><input type="text" name="global_comment[]" placeholder="HTTPS"></td>
<td class="row-actions">
<button type="button" class="btn sm secondary" onclick="removeRow(this)">Remove</button>
</td>
</tr>
<tr>
<td><input type="number" name="global_ports[]" value="1935" min="1" max="65535"></td>
<td><input type="text" name="global_comment[]" placeholder="RTMP"></td>
<td class="row-actions">
<button type="button" class="btn sm secondary" onclick="removeRow(this)">Remove</button>
</td>
</tr>
</tbody>
</table>
<div class="small-note">
Example: 80, 443, 1935, 22 etc. Empty rows will be ignored.
</div>
</div>
<div class="card">
<div class="section-header">
<h2>Restricted Ports (Only From Subnets)</h2>
<button type="button" class="btn sm secondary" onclick="addRestrictedRow()">+ Add Restricted Rule</button>
</div>
<p>
For each row: port is first globally <strong>denied</strong>, then <strong>allowed only from the subnet</strong>.
Typical use: restrict 8080/8443 to internal networks.
</p>
<table id="restrictedTable">
<thead>
<tr>
<th style="width: 140px;">Port</th>
<th>Subnet / CIDR (e.g. 192.168.1.0/24)</th>
<th style="width: 80px;"></th>
</tr>
</thead>
<tbody>
<!-- Example rows -->
<tr>
<td><input type="number" name="restricted_port[]" value="8080" min="1" max="65535"></td>
<td><input type="text" name="restricted_subnet[]" value="192.168.0.0/16"></td>
<td class="row-actions">
<button type="button" class="btn sm secondary" onclick="removeRow(this)">Remove</button>
</td>
</tr>
<tr>
<td><input type="number" name="restricted_port[]" value="8443" min="1" max="65535"></td>
<td><input type="text" name="restricted_subnet[]" value="10.0.0.0/8"></td>
<td class="row-actions">
<button type="button" class="btn sm secondary" onclick="removeRow(this)">Remove</button>
</td>
</tr>
</tbody>
</table>
<div class="small-note">
Subnet examples: <code>192.168.1.0/24</code>, <code>10.0.0.0/8</code>, or a single IP like <code>203.0.113.5</code>.
</div>
</div>
<div class="card">
<div class="flex">
<label>
<input type="checkbox" name="dry_run" value="1">
Preview only (do not execute commands)
</label>
<div class="flex-right">
<button type="submit" class="btn">Apply Firewall Rules</button>
</div>
</div>
<div class="small-note">
Use preview first to verify behaviour. Always test on a non-production system before applying to live servers.
</div>
</div>
</form>
<div class="card">
<h2>Current UFW Status</h2>
<p>Output of <code>ufw status numbered</code>:</p>
<pre><?php echo htmlspecialchars(implode("\n", $currentStatus)); ?></pre>
</div>
</div>
<script>
function removeRow(button) {
const tr = button.closest('tr');
const tbody = tr.parentNode;
tbody.removeChild(tr);
}
function addGlobalPortRow() {
const tableBody = document.querySelector('#globalPortsTable tbody');
const tr = document.createElement('tr');
tr.innerHTML = `
<td><input type="number" name="global_ports[]" min="1" max="65535" placeholder="Port"></td>
<td><input type="text" name="global_comment[]" placeholder="Comment (optional)"></td>
<td class="row-actions">
<button type="button" class="btn sm secondary" onclick="removeRow(this)">Remove</button>
</td>
`;
tableBody.appendChild(tr);
}
function addRestrictedRow() {
const tableBody = document.querySelector('#restrictedTable tbody');
const tr = document.createElement('tr');
tr.innerHTML = `
<td><input type="number" name="restricted_port[]" min="1" max="65535" placeholder="Port"></td>
<td><input type="text" name="restricted_subnet[]" placeholder="192.168.1.0/24"></td>
<td class="row-actions">
<button type="button" class="btn sm secondary" onclick="removeRow(this)">Remove</button>
</td>
`;
tableBody.appendChild(tr);
}
</script>
</body>
</html>

View File

@ -57,20 +57,20 @@ function update_service($which_service)
switch ($input_source) { switch ($input_source) {
case "hdmi": case "hdmi":
$input = "ffmpeg -thread_queue_size 512 -f v4l2 -input_format mjpeg -framerate " . $data['hdmi']['framerate'] . " -video_size " . $data['hdmi']['resolution'] . " -i /dev/video0 " . $input = "ffmpeg -hide_banner -f v4l2 -input_format mjpeg -framerate " . $data['hdmi']['framerate'] . " -video_size " . $data['hdmi']['resolution'] . " -i /dev/video0 " .
"-f alsa -i " . $data['hdmi']['audio_source'] . ' -init_hw_device qsv=hw:/dev/dri/renderD128 -filter_hw_device hw -fflags +genpts -use_wallclock_as_timestamps 1 -vf "format=nv12,hwupload=extra_hw_frames=64,format=qsv" '; "-f alsa -i " . $data['hdmi']['audio_source'];
break; break;
case "url": case "url":
$input .= "ffmpeg -hwaccel auto -stream_loop -1 -re -i " . $data['url']; $input .= "ffmpeg -hide_banner -stream_loop -1 -re -i " . $data['url'];
break; break;
case "udp": case "udp":
$input .= 'ffmpeg -hwaccel auto -stream_loop -1 -re -i "' . $data['udp']; $input .= 'ffmpeg -hide_banner -stream_loop -1 -re -i "' . $data['udp'];
break; break;
case "rtmp": case "rtmp":
$input .= "ffmpeg -hwaccel auto -stream_loop -1 -re -i rtmp://127.0.0.1:1935/" . $$input_rtmp_mount . "/" . $input_rtmp_pass; $input .= "ffmpeg -hide_banner -stream_loop -1 -re -i rtmp://127.0.0.1:1935/" . $$input_rtmp_mount . "/" . $input_rtmp_pass;
break; break;
case "srt": case "srt":
$input .= "ffmpeg -hwaccel auto -stream_loop -1 -re -i srt://127.0.0.1:1937/shree/bhatt/" . $srt_pass3; $input .= "ffmpeg -hide_banner -stream_loop -1 -re -i srt://127.0.0.1:1937/shree/bhatt/" . $srt_pass3;
break; break;
} }
$input .= " "; $input .= " ";
@ -133,8 +133,7 @@ function update_service($which_service)
switch ($which_service) { switch ($which_service) {
case 'input': case 'input':
$input .= ' -c:v h264_qsv -b:v ' . $data['video']['data_rate'] . ' -maxrate ' . $data['video']['data_rate'] . ' -bufsize 11M -g ' . $data['video']['gop'] . ' -af "aresample=async=1:first_pts=0" ' . $input .= " -f mpegts udp://239.255.254.254:39000?localaddr=127.0.0.1";
' -c:a ' . $data['audio']['format'] . ' -ar ' . $data['audio']['sample_rate'] . ' -b:a ' . $data['audio']['bit_rate'] . ' -f mpegts udp://@239.255.254.254:39000';
$service = $input; $service = $input;
$file = "/var/www/encoder-main.sh"; $file = "/var/www/encoder-main.sh";
@ -261,7 +260,7 @@ http {
file_put_contents($file, $nginx); file_put_contents($file, $nginx);
if ($service_rtmp_multiple === "enable") { if ($service_rtmp_multiple === "enable") {
$rtmp = 'ffmpeg -fflags nobuffer -i "udp://@239.255.254.254:39000?fifo_size=5000000&overrun_nonfatal=1" -c:v copy -c:a aac -f flv rtmp://127.0.0.1:1935/shree/bhattji'; $rtmp = 'ffmpeg -hide_banner -fflags nobuffer -i "udp://@239.255.254.254:39000?fifo_size=5000000&overrun_nonfatal=1" -c:v copy -c:a aac -f flv rtmp://127.0.0.1:1935/shree/bhattji';
$file = "/var/www/encoder-rtmp.sh"; $file = "/var/www/encoder-rtmp.sh";
file_put_contents($file, $rtmp); file_put_contents($file, $rtmp);
exec('sudo cp /var/www/nginx.conf /etc/nginx/nginx.conf'); exec('sudo cp /var/www/nginx.conf /etc/nginx/nginx.conf');