218 lines
8.9 KiB
HTML
218 lines
8.9 KiB
HTML
<!DOCTYPE html>
|
|
<html data-theme="dark">
|
|
<head>
|
|
<title>Infinite-Noodles</title>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<link rel="stylesheet" href="/static/pub/bulma.min.css">
|
|
<link rel="stylesheet" href="/static/pub/all.min.css">
|
|
<style>
|
|
.banner-container {
|
|
background-image: url("/static/pub/long-banner.jpg");
|
|
background-repeat: no-repeat;
|
|
background-size: cover;
|
|
background-position: center;
|
|
position: relative;
|
|
flex: 1 1 auto;
|
|
width: 100%;
|
|
min-height: 50px;
|
|
}
|
|
img.banner {
|
|
height: 50px;
|
|
}
|
|
img.banner-long {
|
|
height: 50px;
|
|
object-fit: fill;
|
|
}
|
|
table {
|
|
width: 100%;
|
|
}
|
|
.allow-from-options {
|
|
display: flex;
|
|
gap: 0.35rem;
|
|
flex-wrap: wrap;
|
|
margin-top: 0.35rem;
|
|
}
|
|
.allow-from-options .button {
|
|
height: 1.8rem;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body class="has-navbar-fixed-top">
|
|
<nav class="navbar is-fixed-top" role="navigation" aria-label="main navigation">
|
|
<div class="navbar-brand">
|
|
<img class="banner" src="/static/pub/logo.jpg"/>
|
|
</div>
|
|
<div class="banner-container">
|
|
<!-- <img class="banner-long" src="/static/pub/long-banner.jpg"/> -->
|
|
<div class="navbar-end pr-4">
|
|
<div class="navbar-item">
|
|
<span class="has-text-white mr-3">Signed in as {{.Username}}</span>
|
|
<a class="button is-small is-warning mr-2" href="/users">{{if eq .Role "admin"}}Users{{else}}Account{{end}}</a>
|
|
<form method="post" action="/logout" autocomplete="off">
|
|
<button class="button is-small is-light" type="submit">Logout</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="table-container">
|
|
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Proto</th>
|
|
<th>Allow From</th>
|
|
<th>Listening Port</th>
|
|
<th>Dest Port</th>
|
|
<th>Dest Host/IP</th>
|
|
<th>Expiration</th>
|
|
<th>Status</th>
|
|
<th>Created By</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><input class="input is-link is-small" type="text" form="add-noodle" name="name" placeholder="Name" autocomplete="off" required/></td>
|
|
<td>
|
|
<div class="select is-small is-link">
|
|
<select form="add-noodle" name="proto" autocomplete="off" required>
|
|
<option value="TCP">TCP</option>
|
|
<option value="UDP">UDP</option>
|
|
</select>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<input class="input is-link is-small" type="text" form="add-noodle" name="src" list="allow-from-options" placeholder="All, comma-separated IPs, or CIDRs" value="All" autocomplete="off" required/>
|
|
<datalist id="allow-from-options">
|
|
<option value="All"></option>
|
|
<option value="{{.ClientIP}}"></option>
|
|
</datalist>
|
|
<div class="allow-from-options">
|
|
<button class="button is-small is-white" type="button" onclick="setAllowFromValue('All')">All</button>
|
|
<button class="button is-small is-white" type="button" onclick="setAllowFromValue('{{.ClientIP}}')">{{.ClientIP}}</button>
|
|
</div>
|
|
</td>
|
|
<td><input class="input is-link is-small" type="text" form="add-noodle" name="listen_port" placeholder="Listen Port" autocomplete="off" required/></td>
|
|
<td><input class="input is-link is-small" type="text" form="add-noodle" name="dest_port" placeholder="Destination Port" autocomplete="off" required/></td>
|
|
<td><input class="input is-link is-small" type="text" form="add-noodle" name="dest_host" placeholder="Destination Host" autocomplete="off" required/></td>
|
|
<td><input class="input is-link is-small" type="text" form="add-noodle" name="expiration" placeholder="30m, 1h15m" autocomplete="off" required/></td>
|
|
<td>
|
|
</td>
|
|
<td>
|
|
</td>
|
|
<td>
|
|
<form id="add-noodle" method="post" action="/add" autocomplete="off">
|
|
<button class="button" type="submit">
|
|
<span class="icon has-text-success"><i class="fas fa-plus"></i></span>
|
|
</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{{range .Noodles}}
|
|
<tr>
|
|
<td>{{.Name}}</td>
|
|
<td>{{.Proto}}</td>
|
|
<td>{{.Src}}</td>
|
|
<td>{{.ListenPort}}</td>
|
|
<td>{{.DestPort}}</td>
|
|
<td>{{.DestHost}}</td>
|
|
<td data-expiration-ms="{{.Expiration.Milliseconds}}" data-is-up="{{.IsUp}}">{{.Expiration}}</td>
|
|
<td>
|
|
<form method="post" action="/toggle" autocomplete="off">
|
|
<input type="hidden" name="id" value="{{.Id}}"/>
|
|
<label>
|
|
<input type="checkbox" name="is_up" onchange="this.form.submit()" {{if .IsUp}}checked{{end}}/>
|
|
</label>
|
|
</form>
|
|
</td>
|
|
<td>{{.CreatedBy}}</td>
|
|
<td>
|
|
<form method="post" action="/delete" autocomplete="off" onsubmit="return confirm('Delete this proxy?');">
|
|
<input type="hidden" name="id" value="{{.Id}}"/>
|
|
<button class="button is-white" type="submit" aria-label="Delete noodle">
|
|
<span class="icon has-text-danger"><i class="fas fa-minus"></i></span>
|
|
</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<footer class="footer">
|
|
<div class="content has-text-centered">
|
|
<p>
|
|
<strong>Infinite-Noodles Network Proxy</strong>
|
|
</p>
|
|
</div>
|
|
</footer>
|
|
<script>
|
|
function setAllowFromValue(value) {
|
|
const input = document.querySelector('input[name="src"]');
|
|
if (!input) {
|
|
return;
|
|
}
|
|
input.value = value;
|
|
input.focus();
|
|
}
|
|
|
|
function formatDuration(ms) {
|
|
if (ms <= 0) {
|
|
return "0s";
|
|
}
|
|
|
|
let totalSeconds = Math.floor(ms / 1000);
|
|
const hours = Math.floor(totalSeconds / 3600);
|
|
totalSeconds -= hours * 3600;
|
|
const minutes = Math.floor(totalSeconds / 60);
|
|
const seconds = totalSeconds % 60;
|
|
const parts = [];
|
|
|
|
if (hours > 0) {
|
|
parts.push(hours + "h");
|
|
}
|
|
if (minutes > 0 || hours > 0) {
|
|
parts.push(minutes + "m");
|
|
}
|
|
parts.push(seconds + "s");
|
|
|
|
return parts.join("");
|
|
}
|
|
|
|
function updateExpirationCountdowns() {
|
|
const expirationCells = document.querySelectorAll("[data-expiration-ms]");
|
|
expirationCells.forEach((cell) => {
|
|
if (cell.dataset.isUp !== "true") {
|
|
return;
|
|
}
|
|
const nextValue = Number(cell.dataset.expirationMs) - 1000;
|
|
const clampedValue = Math.max(nextValue, 0);
|
|
cell.dataset.expirationMs = String(clampedValue);
|
|
if (clampedValue === 0) {
|
|
const row = cell.closest("tr");
|
|
if (row) {
|
|
row.remove();
|
|
}
|
|
return;
|
|
}
|
|
cell.textContent = formatDuration(clampedValue);
|
|
});
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
const expirationCells = document.querySelectorAll("[data-expiration-ms]");
|
|
expirationCells.forEach((cell) => {
|
|
cell.textContent = formatDuration(Number(cell.dataset.expirationMs));
|
|
});
|
|
|
|
window.setInterval(updateExpirationCountdowns, 1000);
|
|
});
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|