Adding passwords and versionning
This commit is contained in:
97
README.md
97
README.md
@@ -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
57
includes/auth.php
Normal 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
4
includes/footer.php
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?php require_once __DIR__ . '/../version.php'; ?>
|
||||||
|
<footer>
|
||||||
|
portspoof<span>concentrator</span> · v<?= APP_VERSION ?>
|
||||||
|
</footer>
|
||||||
@@ -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); }
|
||||||
|
|||||||
@@ -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
81
login.php
Normal 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
5
logout.php
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
logout();
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
@@ -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
80
settings.php
Normal 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
2
version.php
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<?php
|
||||||
|
define('APP_VERSION', '2603.1');
|
||||||
Reference in New Issue
Block a user