Compare commits
2 Commits
38ad3e23e1
...
570d527309
| Author | SHA1 | Date | |
|---|---|---|---|
| 570d527309 | |||
| a2a37b7e6c |
56
install.sh
56
install.sh
@@ -29,6 +29,7 @@ COLLECTOR_SRC="$SCRIPT_DIR/bin/torpanel-collect.py"
|
|||||||
COLLECTOR_BIN="/usr/local/bin/torpanel-collect.py"
|
COLLECTOR_BIN="/usr/local/bin/torpanel-collect.py"
|
||||||
SVC="/etc/systemd/system/torpanel-collector.service"
|
SVC="/etc/systemd/system/torpanel-collector.service"
|
||||||
TIMER="/etc/systemd/system/torpanel-collector.timer"
|
TIMER="/etc/systemd/system/torpanel-collector.timer"
|
||||||
|
LOGDUMP_BIN="/usr/local/bin/torpanel-logdump"
|
||||||
|
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
@@ -137,9 +138,10 @@ info "Granting www-data access to Tor cookie"
|
|||||||
usermod -aG debian-tor www-data || true
|
usermod -aG debian-tor www-data || true
|
||||||
ok "Permissions set"
|
ok "Permissions set"
|
||||||
|
|
||||||
info "Allowing www-data to control tor (limited)"
|
info "Allowing www-data to control tor (limited) + read logs"
|
||||||
cat > "$SUDOERS_FILE" <<'SUD'
|
cat > "$SUDOERS_FILE" <<SUD
|
||||||
www-data ALL=NOPASSWD:/bin/systemctl reload tor, /bin/systemctl restart tor, /bin/systemctl start tor, /bin/systemctl stop tor
|
www-data ALL=NOPASSWD:/bin/systemctl reload tor, /bin/systemctl restart tor, /bin/systemctl start tor, /bin/systemctl stop tor
|
||||||
|
www-data ALL=NOPASSWD:$LOGDUMP_BIN
|
||||||
SUD
|
SUD
|
||||||
chmod 440 "$SUDOERS_FILE"
|
chmod 440 "$SUDOERS_FILE"
|
||||||
ok "Sudoers entry created"
|
ok "Sudoers entry created"
|
||||||
@@ -206,13 +208,61 @@ WantedBy=timers.target
|
|||||||
TIMER
|
TIMER
|
||||||
ok "Systemd units installed"
|
ok "Systemd units installed"
|
||||||
|
|
||||||
|
info "Installing tor log dumper"
|
||||||
|
cat > "$LOGDUMP_BIN" <<'BASH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
LINES="${1:-500}"
|
||||||
|
LEVEL="${2:-info}" # info|notice|warning|err|debug
|
||||||
|
|
||||||
|
case "$LINES" in ''|*[!0-9]* ) LINES=500 ;; esac
|
||||||
|
case "$LEVEL" in debug|info|notice|warning|err) ;; * ) LEVEL=info ;; esac
|
||||||
|
|
||||||
|
unit_guess() {
|
||||||
|
if systemctl list-units --type=service | grep -q '^tor@default\.service'; then
|
||||||
|
echo 'tor@default.service'
|
||||||
|
elif systemctl list-units --type=service | grep -q '^tor\.service'; then
|
||||||
|
echo 'tor.service'
|
||||||
|
else
|
||||||
|
echo ''
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
UNIT="$(unit_guess)"
|
||||||
|
|
||||||
|
if command -v journalctl >/dev/null 2>&1; then
|
||||||
|
if [[ -n "$UNIT" ]]; then
|
||||||
|
if journalctl -u "$UNIT" -t tor -p "$LEVEL" -n "$LINES" -o short-iso --no-pager; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if journalctl -t tor -p "$LEVEL" -n "$LINES" -o short-iso --no-pager; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
for f in /var/log/tor/notice.log /var/log/tor/info.log /var/log/tor/log; do
|
||||||
|
if [[ -r "$f" ]]; then
|
||||||
|
tail -n "$LINES" "$f"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "No Tor logs found via journal (identifier 'tor') or /var/log/tor/*. Enable file logs with:
|
||||||
|
Log notice file /var/log/tor/notice.log
|
||||||
|
Log info file /var/log/tor/info.log
|
||||||
|
and reload tor." >&2
|
||||||
|
exit 1
|
||||||
|
BASH
|
||||||
|
chmod 0755 "$LOGDUMP_BIN"
|
||||||
|
ok "Log dumper installed"
|
||||||
|
|
||||||
info "Restarting services"
|
info "Restarting services"
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable --now tor
|
systemctl enable --now tor
|
||||||
systemctl enable "$PHP_FPM_SVC" nginx >/dev/null
|
systemctl enable "$PHP_FPM_SVC" nginx >/dev/null
|
||||||
systemctl restart "$PHP_FPM_SVC"
|
systemctl restart "$PHP_FPM_SVC"
|
||||||
systemctl restart nginx
|
systemctl restart nginx
|
||||||
systemctl start torpanel-collector.service
|
systemctl start torpanel-collector.service || true
|
||||||
systemctl enable --now torpanel-collector.timer
|
systemctl enable --now torpanel-collector.timer
|
||||||
ok "Services running"
|
ok "Services running"
|
||||||
|
|
||||||
|
|||||||
21
web/api/tor_log.php
Normal file
21
web/api/tor_log.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
require __DIR__ . '/../lib/app.php';
|
||||||
|
auth_require();
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$n = isset($_GET['n']) ? (int)$_GET['n'] : 500;
|
||||||
|
if ($n < 50) $n = 50;
|
||||||
|
if ($n > 5000) $n = 5000;
|
||||||
|
|
||||||
|
$level = isset($_GET['level']) ? strtolower($_GET['level']) : 'info';
|
||||||
|
$allowed = ['debug','info','notice','warning','err'];
|
||||||
|
if (!in_array($level, $allowed, true)) $level = 'info';
|
||||||
|
|
||||||
|
$cmd = sprintf('sudo /usr/local/bin/torpanel-logdump %d %s', $n, escapeshellarg($level));
|
||||||
|
exec($cmd . ' 2>&1', $out, $rc);
|
||||||
|
|
||||||
|
if ($rc === 0 && !empty($out)) {
|
||||||
|
echo json_encode(['ok' => true, 'lines' => $out]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'no_log', 'rc' => $rc]);
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ auth_require();
|
|||||||
Tor Relay Panel
|
Tor Relay Panel
|
||||||
</span>
|
</span>
|
||||||
<div class="ms-auto d-flex align-items-center gap-2">
|
<div class="ms-auto d-flex align-items-center gap-2">
|
||||||
|
<button id="btnTorLog" class="btn btn-sm btn-outline-light">View Tor Log</button>
|
||||||
<button id="btnTheme" class="btn btn-sm btn-outline-light">Theme</button>
|
<button id="btnTheme" class="btn btn-sm btn-outline-light">Theme</button>
|
||||||
<a class="btn btn-sm btn-outline-light" href="/logout.php">Logout</a>
|
<a class="btn btn-sm btn-outline-light" href="/logout.php">Logout</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -198,6 +199,32 @@ auth_require();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="torlogModal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,.6); z-index:1050;">
|
||||||
|
<div style="background:#111; color:#eee; max-width:960px; height:70vh; margin:5vh auto; border-radius:12px; overflow:hidden; box-shadow:0 10px 30px rgba(0,0,0,.4); border:1px solid rgba(255,255,255,.08)">
|
||||||
|
<div class="d-flex align-items-center justify-content-between p-2" style="background:#1b1b1b; border-bottom:1px solid rgba(255,255,255,.08)">
|
||||||
|
<div class="fw-semibold">Tor Log</div>
|
||||||
|
<div class="d-flex gap-2 align-items-center">
|
||||||
|
<select id="torlogLevel" class="form-select form-select-sm bg-dark text-light" style="width:auto;">
|
||||||
|
<option value="info" selected>info</option>
|
||||||
|
<option value="notice">notice</option>
|
||||||
|
<option value="warning">warning</option>
|
||||||
|
<option value="err">err</option>
|
||||||
|
<option value="debug">debug</option>
|
||||||
|
</select>
|
||||||
|
<select id="torlogLines" class="form-select form-select-sm bg-dark text-light" style="width:auto;">
|
||||||
|
<option value="200">Last 200</option>
|
||||||
|
<option value="500" selected>Last 500</option>
|
||||||
|
<option value="1000">Last 1000</option>
|
||||||
|
<option value="2000">Last 2000</option>
|
||||||
|
</select>
|
||||||
|
<button id="torlogRefresh" class="btn btn-sm btn-outline-light">Refresh</button>
|
||||||
|
<button id="torlogClose" class="btn btn-sm btn-light text-dark">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre id="torlogPre" class="p-3 m-0" style="background:#0c0c0c; color:#cdd3df; height:calc(70vh - 56px); overflow:auto; font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace; font-size:12px; line-height:1.35"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const $ = (id) => document.getElementById(id);
|
const $ = (id) => document.getElementById(id);
|
||||||
const el = {
|
const el = {
|
||||||
@@ -240,7 +267,6 @@ function applyTheme(theme){
|
|||||||
document.documentElement.setAttribute('data-theme', theme);
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
localStorage.setItem(THEME_KEY, theme);
|
localStorage.setItem(THEME_KEY, theme);
|
||||||
setBtnLabel(theme);
|
setBtnLabel(theme);
|
||||||
// Recolor chart axes/grid
|
|
||||||
if (chart){
|
if (chart){
|
||||||
const c = getThemeColors();
|
const c = getThemeColors();
|
||||||
chart.options.scales.x.ticks.color = c.text;
|
chart.options.scales.x.ticks.color = c.text;
|
||||||
@@ -364,10 +390,13 @@ async function refreshChart(){
|
|||||||
setChartData(labels, rx, tx);
|
setChartData(labels, rx, tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function loadReach(){
|
async function loadReach(){
|
||||||
const r = await fetch('api/reach.php');
|
try{
|
||||||
|
const r = await fetch('api/reach.php', {cache:'no-store'});
|
||||||
const j = await r.json();
|
const j = await r.json();
|
||||||
if (!j.ok) return;
|
if (!j.ok) return false;
|
||||||
|
|
||||||
if (el.reachNick) el.reachNick.textContent = j.nickname || '—';
|
if (el.reachNick) el.reachNick.textContent = j.nickname || '—';
|
||||||
if (el.reachFP) el.reachFP.textContent = j.fingerprint || '—';
|
if (el.reachFP) el.reachFP.textContent = j.fingerprint || '—';
|
||||||
if (el.reachPort) el.reachPort.textContent = j.orport || '—';
|
if (el.reachPort) el.reachPort.textContent = j.orport || '—';
|
||||||
@@ -382,11 +411,20 @@ async function loadReach(){
|
|||||||
if (j.onionoo.last_seen) seenStr = 'Last seen: ' + new Date(j.onionoo.last_seen).toLocaleString();
|
if (j.onionoo.last_seen) seenStr = 'Last seen: ' + new Date(j.onionoo.last_seen).toLocaleString();
|
||||||
}
|
}
|
||||||
const running = !!(j.onionoo && j.onionoo.running);
|
const running = !!(j.onionoo && j.onionoo.running);
|
||||||
|
|
||||||
if (el.reachBadge){
|
if (el.reachBadge){
|
||||||
el.reachBadge.textContent = running ? 'Running (publicly reachable)' : (j.onionoo && j.onionoo.found ? 'Not Running yet' : 'Not in consensus yet');
|
el.reachBadge.textContent = running ? 'Running (publicly reachable)' : (j.onionoo && j.onionoo.found ? 'Not Running yet' : 'Not in consensus yet');
|
||||||
el.reachBadge.className = 'badge ' + (running ? 'bg-success' : (j.onionoo && j.onionoo.found ? 'bg-warning' : 'bg-danger'));
|
el.reachBadge.className = 'badge ' + (running ? 'bg-success' : (j.onionoo && j.onionoo.found ? 'bg-warning' : 'bg-danger'));
|
||||||
|
el.reachBadge.title = 'Last checked: ' + new Date().toLocaleString();
|
||||||
}
|
}
|
||||||
|
if (el.reachFlags) el.reachFlags.textContent = flagStr;
|
||||||
|
if (el.reachSeen) el.reachSeen.textContent = seenStr;
|
||||||
if (el.reachHelp) el.reachHelp.style.display = running ? 'none' : 'block';
|
if (el.reachHelp) el.reachHelp.style.display = running ? 'none' : 'block';
|
||||||
|
|
||||||
|
return running;
|
||||||
|
}catch(e){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (el.btnEditCfg){
|
if (el.btnEditCfg){
|
||||||
@@ -427,6 +465,26 @@ if (el.cfgForm){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (el.reachRefresh){
|
||||||
|
el.reachRefresh.addEventListener('click', async ()=>{
|
||||||
|
const btn = el.reachRefresh;
|
||||||
|
const originalClasses = btn.className;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>Checking…';
|
||||||
|
|
||||||
|
const running = await loadReach();
|
||||||
|
|
||||||
|
btn.className = running ? 'btn btn-sm btn-success' : 'btn btn-sm btn-danger';
|
||||||
|
btn.innerHTML = running ? 'Reachable ✓' : 'Not reachable ✗';
|
||||||
|
|
||||||
|
setTimeout(()=>{
|
||||||
|
btn.className = originalClasses;
|
||||||
|
btn.innerHTML = 'Check status';
|
||||||
|
btn.disabled = false;
|
||||||
|
}, 1500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
(function initTheme(){
|
(function initTheme(){
|
||||||
const t = preferredTheme();
|
const t = preferredTheme();
|
||||||
document.documentElement.setAttribute('data-theme', t);
|
document.documentElement.setAttribute('data-theme', t);
|
||||||
@@ -442,6 +500,38 @@ if (el.cfgForm){
|
|||||||
setInterval(refreshChart, 60000);
|
setInterval(refreshChart, 60000);
|
||||||
setInterval(loadReach, 60000);
|
setInterval(loadReach, 60000);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
(function(){
|
||||||
|
const m = document.getElementById('torlogModal');
|
||||||
|
const btn = document.getElementById('btnTorLog');
|
||||||
|
const pre = document.getElementById('torlogPre');
|
||||||
|
const lines= document.getElementById('torlogLines');
|
||||||
|
const lvl = document.getElementById('torlogLevel');
|
||||||
|
const close= document.getElementById('torlogClose');
|
||||||
|
const ref = document.getElementById('torlogRefresh');
|
||||||
|
|
||||||
|
async function loadLog(){
|
||||||
|
const n = lines?.value || '500';
|
||||||
|
const L = lvl?.value || 'info';
|
||||||
|
try{
|
||||||
|
const res = await fetch('api/tor_log.php?n=' + encodeURIComponent(n) + '&level=' + encodeURIComponent(L), {cache:'no-store'});
|
||||||
|
if (!res.ok){ pre.textContent = 'Failed to load log.'; return; }
|
||||||
|
const json = await res.json();
|
||||||
|
if (!json.ok){ pre.textContent = 'No logs available.'; return; }
|
||||||
|
pre.textContent = (json.lines||[]).join('\n');
|
||||||
|
pre.scrollTop = pre.scrollHeight;
|
||||||
|
}catch(e){
|
||||||
|
pre.textContent = 'Error: ' + e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
btn?.addEventListener('click', ()=>{ m.style.display='block'; loadLog(); });
|
||||||
|
lines?.addEventListener('change', loadLog);
|
||||||
|
lvl?.addEventListener('change', loadLog);
|
||||||
|
document.getElementById('torlogRefresh')?.addEventListener('click', loadLog);
|
||||||
|
document.getElementById('torlogClose')?.addEventListener('click', ()=>{ m.style.display='none'; });
|
||||||
|
m?.addEventListener('click', (e)=>{ if (e.target === m) m.style.display='none'; });
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user