Adding passwords and versionning

This commit is contained in:
2026-03-11 10:43:23 -04:00
parent 20ed0eeadb
commit e0fe0c4d34
10 changed files with 341 additions and 6 deletions

View File

@@ -13,12 +13,13 @@ Each portspoof_py instance runs independently and exposes a JSON API. portspoof_
3. [Installation](#installation) 3. [Installation](#installation)
4. [Configuration](#configuration) 4. [Configuration](#configuration)
5. [Database schema](#database-schema) 5. [Database schema](#database-schema)
6. [Adding nodes](#adding-nodes) 6. [Web interface authentication](#web-interface-authentication)
7. [Fetch cron](#fetch-cron) 7. [Adding nodes](#adding-nodes)
8. [HTTP trigger endpoint](#http-trigger-endpoint) 8. [Fetch cron](#fetch-cron)
9. [Dashboard](#dashboard) 9. [HTTP trigger endpoint](#http-trigger-endpoint)
10. [Upgrading](#upgrading) 10. [Dashboard](#dashboard)
11. [Troubleshooting](#troubleshooting) 11. [Upgrading](#upgrading)
12. [Troubleshooting](#troubleshooting)
--- ---
@@ -43,9 +44,16 @@ portspoof_concentrator/
├── setup.php One-time install / migration script ├── setup.php One-time install / migration script
├── index.php Aggregated dashboard ├── index.php Aggregated dashboard
├── nodes.php Add / edit / delete portspoof_py nodes ├── nodes.php Add / edit / delete portspoof_py nodes
├── login.php Login form
├── logout.php Session teardown
├── settings.php Change password via the web interface
├── trigger.php HTTP endpoint to trigger a fetch run (token-protected) ├── trigger.php HTTP endpoint to trigger a fetch run (token-protected)
├── version.php Application version constant (bump on each release)
├── auth.passwd Live password hash (auto-created by settings.php, gitignore this)
├── includes/ ├── includes/
│ ├── auth.php Session management, login helpers, save_password()
│ ├── db.php PDO singleton │ ├── db.php PDO singleton
│ ├── footer.php Shared footer with version number
│ ├── functions.php Node CRUD, fetch helpers, run_fetch(), dashboard queries │ ├── functions.php Node CRUD, fetch helpers, run_fetch(), dashboard queries
│ └── style.php Shared CSS (included inline by both pages) │ └── style.php Shared CSS (included inline by both pages)
└── cron/ └── cron/
@@ -81,6 +89,19 @@ define('DB_USER', 'portspoof');
define('DB_PASS', 'strongpassword'); // match the password above define('DB_PASS', 'strongpassword'); // match the password above
``` ```
Set a UI password (see [Web interface authentication](#web-interface-authentication) for details):
```bash
php -r "echo password_hash('yourpassword', PASSWORD_DEFAULT) . PHP_EOL;"
```
Paste the output into `config.php`:
```php
define('UI_USER', 'admin');
define('UI_PASS_HASH', '$2y$12$...');
```
See [Configuration](#configuration) for the full list of constants. See [Configuration](#configuration) for the full list of constants.
### 4. Run the setup script ### 4. Run the setup script
@@ -161,6 +182,8 @@ All tunables are constants in `config.php`.
| `FETCH_TIMEOUT` | `10` | cURL timeout (seconds) for outbound calls to portspoof_py nodes | | `FETCH_TIMEOUT` | `10` | cURL timeout (seconds) for outbound calls to portspoof_py nodes |
| `FETCH_LIMIT` | `500` | Maximum connections pulled from a node per fetch run | | `FETCH_LIMIT` | `500` | Maximum connections pulled from a node per fetch run |
| `TRIGGER_TOKEN` | `''` | Secret token for `trigger.php`. Empty string disables the endpoint entirely | | `TRIGGER_TOKEN` | `''` | Secret token for `trigger.php`. Empty string disables the endpoint entirely |
| `UI_USER` | `'admin'` | Username for the web interface |
| `UI_PASS_HASH` | `''` | Bcrypt hash of the UI password. Empty string disables authentication |
--- ---
@@ -203,6 +226,68 @@ One row per connection event ingested from any node.
--- ---
## Web interface authentication
The dashboard and node management pages are protected by a session-based login form. Authentication is controlled by two constants in `config.php`.
### Setup
Generate a bcrypt hash of your chosen password:
```bash
php -r "echo password_hash('yourpassword', PASSWORD_DEFAULT) . PHP_EOL;"
```
Add it to `config.php`:
```php
define('UI_USER', 'admin'); // change to any username you like
define('UI_PASS_HASH', '$2y$12$…'); // paste the hash from the command above
```
Restart your web server / PHP-FPM if it caches config files. On the next visit to `index.php` or `nodes.php` you will be redirected to `login.php`.
### Changing the password
**Via the web interface (recommended):** navigate to **Settings** in the nav bar, enter your current password and the new one, and submit. The new hash is written to `auth.passwd` in the project root and takes effect immediately — no server restart needed.
**Via the CLI:** re-run the hash command and replace the value in `config.php` (or write directly to `auth.passwd`):
```bash
php -r "echo password_hash('newpassword', PASSWORD_DEFAULT) . PHP_EOL;" > /var/www/portspoof_concentrator/auth.passwd
```
Existing sessions remain valid until they expire or the user signs out.
### Password storage precedence
On each request, `auth.php` checks for `auth.passwd` in the project root. If the file exists its contents are used as the hash; otherwise it falls back to `UI_PASS_HASH` in `config.php`. This means:
- First-time setup: set `UI_PASS_HASH` in `config.php`.
- After the first web-interface password change: `auth.passwd` takes over and `UI_PASS_HASH` is ignored.
Add `auth.passwd` to your `.gitignore` to avoid committing credentials:
```
auth.passwd
```
### Disabling authentication
Set `UI_PASS_HASH` to an empty string:
```php
define('UI_PASS_HASH', '');
```
All pages become publicly accessible. Only do this on a private network or when another layer (firewall, VPN, web server auth) protects the interface.
### Sign out
A **Sign out** link appears in the navigation bar on every page when authentication is enabled. Visiting `logout.php` directly also works.
---
## Adding nodes ## Adding nodes
Open `http://yourserver/nodes.php` and fill in the form. Open `http://yourserver/nodes.php` and fill in the form.

57
includes/auth.php Normal file
View File

@@ -0,0 +1,57 @@
<?php
require_once __DIR__ . '/../config.php';
define('AUTH_PASSWD_FILE', __DIR__ . '/../auth.passwd');
/**
* Returns the active password hash.
* auth.passwd (written by the web interface) takes precedence over
* the UI_PASS_HASH constant in config.php.
*/
function active_pass_hash(): string {
if (is_readable(AUTH_PASSWD_FILE)) {
return trim(file_get_contents(AUTH_PASSWD_FILE));
}
return UI_PASS_HASH;
}
function auth_enabled(): bool {
return active_pass_hash() !== '';
}
function require_login(): void {
if (!auth_enabled()) {
return;
}
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (empty($_SESSION['authenticated'])) {
header('Location: login.php');
exit;
}
}
function attempt_login(string $username, string $password): bool {
if (!auth_enabled()) {
return true;
}
return $username === UI_USER && password_verify($password, active_pass_hash());
}
function logout(): void {
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$_SESSION = [];
session_destroy();
}
/**
* Hash $new_password and write it to auth.passwd.
* Returns true on success, false if the file could not be written.
*/
function save_password(string $new_password): bool {
$hash = password_hash($new_password, PASSWORD_DEFAULT);
return file_put_contents(AUTH_PASSWD_FILE, $hash . PHP_EOL, LOCK_EX) !== false;
}

4
includes/footer.php Normal file
View File

@@ -0,0 +1,4 @@
<?php require_once __DIR__ . '/../version.php'; ?>
<footer>
portspoof<span>concentrator</span> &nbsp;·&nbsp; v<?= APP_VERSION ?>
</footer>

View File

@@ -86,3 +86,10 @@ button[type=submit]:hover { opacity: .85; }
.bar { height: 6px; background: var(--accent); border-radius: 3px; min-width: 2px; } .bar { height: 6px; background: var(--accent); border-radius: 3px; min-width: 2px; }
form { max-width: 480px; } form { max-width: 480px; }
footer {
text-align: center; padding: 1.25rem; margin-top: 1rem;
font-size: .75rem; color: var(--muted);
border-top: 1px solid var(--border);
}
footer span { color: var(--accent); }

View File

@@ -1,4 +1,6 @@
<?php <?php
require_once __DIR__ . '/includes/auth.php';
require_login();
require_once __DIR__ . '/includes/functions.php'; require_once __DIR__ . '/includes/functions.php';
$nodes = get_all_nodes(); $nodes = get_all_nodes();
@@ -36,6 +38,10 @@ $max_port_cnt = $t_ports ? max(array_column($t_ports, 'cnt')) : 1;
<nav> <nav>
<a href="index.php" class="active">Dashboard</a> <a href="index.php" class="active">Dashboard</a>
<a href="nodes.php">Nodes</a> <a href="nodes.php">Nodes</a>
<a href="settings.php">Settings</a>
<?php if (auth_enabled()): ?>
<a href="logout.php" style="color:var(--muted)">Sign out</a>
<?php endif; ?>
</nav> </nav>
</header> </header>
<main> <main>
@@ -195,5 +201,6 @@ $max_port_cnt = $t_ports ? max(array_column($t_ports, 'cnt')) : 1;
</p> </p>
</main> </main>
<?php include __DIR__ . '/includes/footer.php'; ?>
</body> </body>
</html> </html>

81
login.php Normal file
View File

@@ -0,0 +1,81 @@
<?php
require_once __DIR__ . '/includes/auth.php';
if (!auth_enabled()) {
header('Location: index.php');
exit;
}
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Already logged in
if (!empty($_SESSION['authenticated'])) {
header('Location: index.php');
exit;
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
if (attempt_login($username, $password)) {
session_regenerate_id(true);
$_SESSION['authenticated'] = true;
$_SESSION['username'] = $username;
header('Location: index.php');
exit;
}
$error = 'Invalid username or password.';
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Login portspoof concentrator</title>
<style>
<?php include __DIR__ . '/includes/style.php'; ?>
.login-wrap {
min-height: 100vh;
display: flex; align-items: center; justify-content: center;
}
.login-box {
background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
padding: 2rem 2.25rem; width: 100%; max-width: 360px;
}
.login-box h1 { font-size: 1.1rem; font-weight: 600; color: #e6edf3; margin-bottom: 1.5rem; }
.login-box h1 span { color: var(--accent); }
.login-box form { max-width: 100%; }
.login-box button[type=submit] { width: 100%; margin-top: .25rem; }
</style>
</head>
<body>
<div class="login-wrap">
<div class="login-box">
<h1>portspoof<span>concentrator</span></h1>
<?php if ($error): ?>
<div class="alert err" style="margin-bottom:1rem"><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></div>
<?php endif; ?>
<form method="post">
<label>Username
<input type="text" name="username" autocomplete="username" required
value="<?= htmlspecialchars($_POST['username'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
</label>
<label>Password
<input type="password" name="password" autocomplete="current-password" required>
</label>
<button type="submit">Sign in</button>
</form>
</div>
</div>
<?php include __DIR__ . '/includes/footer.php'; ?>
</body>
</html>

5
logout.php Normal file
View File

@@ -0,0 +1,5 @@
<?php
require_once __DIR__ . '/includes/auth.php';
logout();
header('Location: login.php');
exit;

View File

@@ -1,4 +1,6 @@
<?php <?php
require_once __DIR__ . '/includes/auth.php';
require_login();
require_once __DIR__ . '/includes/functions.php'; require_once __DIR__ . '/includes/functions.php';
$errors = []; $errors = [];
@@ -65,6 +67,10 @@ if (isset($_GET['edit'])) {
<nav> <nav>
<a href="index.php">Dashboard</a> <a href="index.php">Dashboard</a>
<a href="nodes.php" class="active">Nodes</a> <a href="nodes.php" class="active">Nodes</a>
<a href="settings.php">Settings</a>
<?php if (auth_enabled()): ?>
<a href="logout.php" style="color:var(--muted)">Sign out</a>
<?php endif; ?>
</nav> </nav>
</header> </header>
<main> <main>
@@ -163,5 +169,6 @@ if (isset($_GET['edit'])) {
</section> </section>
</main> </main>
<?php include __DIR__ . '/includes/footer.php'; ?>
</body> </body>
</html> </html>

80
settings.php Normal file
View File

@@ -0,0 +1,80 @@
<?php
require_once __DIR__ . '/includes/auth.php';
require_login();
$errors = [];
$success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$current = $_POST['current_password'] ?? '';
$new = $_POST['new_password'] ?? '';
$confirm = $_POST['confirm_password'] ?? '';
if (!password_verify($current, active_pass_hash())) {
$errors[] = 'Current password is incorrect.';
} elseif (strlen($new) < 8) {
$errors[] = 'New password must be at least 8 characters.';
} elseif ($new !== $confirm) {
$errors[] = 'New passwords do not match.';
} elseif (!save_password($new)) {
$errors[] = 'Could not write auth.passwd — check file permissions.';
} else {
$success = 'Password updated successfully.';
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Settings portspoof concentrator</title>
<style>
<?php include __DIR__ . '/includes/style.php'; ?>
</style>
</head>
<body>
<header>
<h1>portspoof<span>concentrator</span></h1>
<nav>
<a href="index.php">Dashboard</a>
<a href="nodes.php">Nodes</a>
<a href="settings.php" class="active">Settings</a>
<?php if (auth_enabled()): ?>
<a href="logout.php" style="color:var(--muted)">Sign out</a>
<?php endif; ?>
</nav>
</header>
<main>
<?php if ($success): ?>
<div class="alert ok"><?= htmlspecialchars($success, ENT_QUOTES, 'UTF-8') ?></div>
<?php endif; ?>
<?php foreach ($errors as $e): ?>
<div class="alert err"><?= htmlspecialchars($e, ENT_QUOTES, 'UTF-8') ?></div>
<?php endforeach; ?>
<section class="card">
<h2>Change password</h2>
<?php if (!auth_enabled()): ?>
<p class="muted">Authentication is disabled. Set <code>UI_PASS_HASH</code> in <code>config.php</code> to enable it.</p>
<?php else: ?>
<form method="post">
<label>Current password
<input type="password" name="current_password" autocomplete="current-password" required>
</label>
<label>New password <small>(minimum 8 characters)</small>
<input type="password" name="new_password" autocomplete="new-password" required minlength="8">
</label>
<label>Confirm new password
<input type="password" name="confirm_password" autocomplete="new-password" required minlength="8">
</label>
<button type="submit">Update password</button>
</form>
<?php endif; ?>
</section>
</main>
<?php include __DIR__ . '/includes/footer.php'; ?>
</body>
</html>

2
version.php Normal file
View File

@@ -0,0 +1,2 @@
<?php
define('APP_VERSION', '2603.1');