Compare commits

...

3 Commits

Author SHA1 Message Date
Claude 3d16061709 Auth hardening: login rate-limiting + session fixation defenses
- login.php: Redis-backed per-IP rate limit (10 fails / 15 min lockout), keyed off CF-Connecting-IP/X-Forwarded-For so it sees the real client behind NPM; fails open if Redis is down

- login.php: session_regenerate_id(true) on successful auth (prevents session fixation)

- php.ini: session.use_strict_mode = 1 (reject unknown/attacker-supplied session IDs)

- netscan.php: constant-time hash_equals for the registration-key check (matches agent.php)

Cookie flags already HttpOnly + SameSite=Lax (verified live). Agent auth verified: missing/bad X-Agent-Key -> 401 on every machine action.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 20:31:57 -05:00
Claude 80588efa7a Secrets sweep: move hardcoded credentials out of tracked files into env files
Removed live secret literals from git-tracked code (all were on GitHub):

- deploy/reactor.py: Claude/Groq API keys, DB pass, Gmail/iCloud app passwords now from os.environ (loaded via systemd EnvironmentFile=/etc/jarvis-arc/reactor.env, root:www-data 0640)

- public_html/login.php: used a private hardcoded PDO connection; now uses config.php DB_* constants

- deploy/jarvis-backup.sh (runs via cron), jarvis-deploy.sh, jarvis-watchdog.sh: DB pass now sourced from /etc/jarvis/db.env (root:root 0600)

- removed dead agent/jarvis-arc-reactor.py (unreferenced old duplicate leaking an old Groq key + stale Ollama IP)

- added deploy/reactor.env.example and deploy/db.env.example templates

Verified live: reactor restarted with all 21 handlers + DB poller (job round-trip OK), login works, mysqldump auth via env OK.

NOTE: these keys remain in GitHub history and should be rotated (Claude/Groq/Gmail/iCloud/DB). Separate decision needed on INFRASTRUCTURE-REFERENCE.md (full cred doc still tracked) + history purge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 20:28:42 -05:00
Claude f7309a15fc Rotate agent registration key; remove key literals from public scripts/UI
- install.sh, install-agent.sh: require JARVIS_REG_KEY env var or interactive prompt instead of baked-in key (matches install-mac.sh/install-windows.ps1 behavior)

- netscan.php: reuse AGENT_REGISTRATION_KEY constant instead of a duplicate literal

- agent.php + api.php: add session-authed "regkey" action so the admin install modal fetches the current key at runtime

- jarvis-agents.js: fetch reg key via /api/agent/regkey instead of hardcoding it; pass JARVIS_REG_KEY in the Linux install one-liner

- INFRASTRUCTURE-REFERENCE.md: scrub old key literal (rotated; real value lives only in api/config.php on VM211)

Key rotated on the box + rolled out to all 11 agents (verified all re-register online). New value is in the gitignored api/config.php only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 20:22:00 -05:00
15 changed files with 944 additions and 3662 deletions
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -54,7 +54,7 @@ function update_agent_seen(string $agentId, string $status = 'online', ?string $
// ── Auth (all actions except register) ───────────────────────────────────────
$agentKey = $_SERVER['HTTP_X_AGENT_KEY'] ?? '';
$browserActions = ['list', 'status', 'myip'];
$browserActions = ['list', 'status', 'myip', 'regkey'];
if ($agentAction !== 'register') {
if (in_array($agentAction, $browserActions)) {
@@ -212,6 +212,10 @@ switch ($agentAction) {
);
agent_ok();
// ── REGKEY (browser: session-authed fetch of registration key) ───────────
case 'regkey':
agent_ok(['registration_key' => AGENT_REGISTRATION_KEY]);
// ── LIST (admin: get all agents status) ──────────────────────────────────
case 'list':
// Mark agents offline if last_seen > 2 minutes ago
+2 -2
View File
@@ -2,14 +2,14 @@
// Network scan push endpoint — called by PVE1 cron with nmap results
// Authenticates via X-Registration-Key header (same key as agent installer)
define('NETSCAN_KEY', 'f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518');
define('NETSCAN_KEY', AGENT_REGISTRATION_KEY);
if ($method !== 'POST') {
echo json_encode(['error' => 'POST only']); exit;
}
$reqKey = $_SERVER['HTTP_X_REGISTRATION_KEY'] ?? '';
if ($reqKey !== NETSCAN_KEY) {
if (!hash_equals(NETSCAN_KEY, $reqKey)) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']); exit;
}
+3
View File
@@ -0,0 +1,3 @@
# Copy to /etc/jarvis/db.env (root:root 0600). Sourced by the root cron scripts
# (jarvis-backup.sh, jarvis-deploy.sh, jarvis-watchdog.sh).
JARVIS_DB_PASS=your-db-password
+2 -1
View File
@@ -1,4 +1,5 @@
#!/bin/bash
[ -r /etc/jarvis/db.env ] && . /etc/jarvis/db.env
# JARVIS backup — DB dump + all files needed to actually restore JARVIS, as tar.gz
# Fixed 2026-07-07: this only ever backed up the MySQL database. If this VM were
# lost, the DB alone is useless without the application code, the reactor daemon,
@@ -9,7 +10,7 @@ LOG="$BACKUP_DIR/backup.log"
LOCK="$BACKUP_DIR/backup.lock"
DB_NAME="jarvis_db"
DB_USER="jarvis_user"
DB_PASS="J4rv1s_Pr0t0c0l_2026!"
DB_PASS="${JARVIS_DB_PASS:?DB pass unset - see /etc/jarvis/db.env}"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
OUTFILE="$BACKUP_DIR/jarvis_backup_${TIMESTAMP}.tar.gz"
TMPDIR=$(mktemp -d)
+2 -1
View File
@@ -1,4 +1,5 @@
#!/bin/bash
[ -r /etc/jarvis/db.env ] && . /etc/jarvis/db.env
# JARVIS Auto-Deploy Runner — processes GitHub webhook queue every minute.
# Validates PHP syntax before deploying; auto-reverts on bad code.
# Restarts OLS after JARVIS deploys to pick up PHP changes.
@@ -64,7 +65,7 @@ while IFS= read -r path; do
fi
# Insert alert into JARVIS DB
BAD_ESCAPED=$(printf '%s' "$BAD_FILE" | sed "s/'/\\\\\\'/g")
mysql -u jarvis_user -pJ4rv1s_Pr0t0c0l_2026! jarvis_db -se \
mysql -u jarvis_user -p"$JARVIS_DB_PASS" jarvis_db -se \
"INSERT INTO alerts (alert_type,title,message,severity)
VALUES ('deploy_fail','Deploy reverted: syntax error',
'PHP syntax error in $BAD_ESCAPED. Commit $AFTER was reverted and force-pushed to GitHub.','critical');" 2>/dev/null
+2 -1
View File
@@ -1,11 +1,12 @@
#!/bin/bash
[ -r /etc/jarvis/db.env ] && . /etc/jarvis/db.env
# JARVIS Self-Healing Watchdog — runs every 5 min via root cron
# Checks: lsws, mysql, redis, JARVIS HTTP, disk, memory
# Auto-heals: restarts failed services, restarts offline Proxmox VM agents
# Logs to: /home/jarvis.orbishosting.com/logs/watchdog.log
LOG=/home/jarvis.orbishosting.com/logs/watchdog.log
MYSQL="mysql -u jarvis_user -pJ4rv1s_Pr0t0c0l_2026! jarvis_db -se"
MYSQL="mysql -u jarvis_user -p$JARVIS_DB_PASS jarvis_db -se"
TS() { date '+%Y-%m-%d %H:%M:%S'; }
log() { echo "[$(TS)] $1" >> "$LOG"; }
+7
View File
@@ -0,0 +1,7 @@
# JARVIS Arc Reactor — required secrets. Copy to /etc/jarvis-arc/reactor.env
# (root:www-data 0640), loaded by systemd EnvironmentFile. Not committed.
JARVIS_DB_PASS=your-db-password
CLAUDE_API_KEY=sk-ant-...
GROQ_API_KEY=gsk_...
GMAIL_PASS=your-gmail-app-password
ICLOUD_PASS=your-icloud-app-password
+5 -5
View File
@@ -39,24 +39,24 @@ VERSION = "9.0.0"
DB_HOST = "localhost"
DB_PORT = 3306
DB_USER = "jarvis_user"
DB_PASS = "J4rv1s_Pr0t0c0l_2026!"
DB_PASS = os.environ.get("JARVIS_DB_PASS", "")
DB_NAME = "jarvis_db"
LOG_FILE = "/var/log/jarvis/arc_reactor.log"
POLL_INTERVAL = 3
HEARTBEAT_INTERVAL = 30
CLAUDE_API_KEY = "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"
CLAUDE_API_KEY = os.environ.get("CLAUDE_API_KEY", "")
CLAUDE_MODEL = "claude-sonnet-4-6"
GROQ_API_KEY = "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"
GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
GROQ_MODEL = "llama-3.3-70b-versatile"
OLLAMA_HOST = "http://10.48.200.210:11434"
OLLAMA_MODEL = "llama3.1:8b"
OLLAMA_VISION_MODEL = os.environ.get("OLLAMA_VISION_MODEL", "") # e.g. "llava" or "moondream" -- empty = disabled
GMAIL_USER = "myronblair@gmail.com"
GMAIL_PASS = "demsvdylwweacbcx"
GMAIL_PASS = os.environ.get("GMAIL_PASS", "")
ICLOUD_USER = "myronblair@icloud.com"
ICLOUD_PASS = "yxfi-yvzu-geqk-japr"
ICLOUD_PASS = os.environ.get("ICLOUD_PASS", "")
# ── LOGGING ───────────────────────────────────────────────────────────────────
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
@@ -663,7 +663,7 @@ Webhook secret: `4c8805f0285214ff0a0602b5880270b935f36a896946c7f1`
### Agent System
Agents installed on all servers — phone home every 10s (heartbeat) / 30s (metrics).
Registration key: `f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518`
Registration key: `[rotated 2026-07-07 — stored in api/config.php on VM211, not documented here]`
Install command: `curl -sk http://10.48.200.211/install-agent.sh | bash -s <hostname> <linux|proxmox>`
### Self-Healing Watchdog
@@ -918,7 +918,7 @@ sshpass -p 'Joker1974!!!' ssh root@10.48.200.90 \
| Service | Key |
|---------|-----|
| GitHub PAT | `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` (rotated 2026-07-05, scopes `repo`+`workflow`) |
| JARVIS Agent Registration | `f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518` |
| JARVIS Agent Registration | `[rotated 2026-07-07 — stored in api/config.php on VM211, not documented here]` |
| Proxmox API Token | `root@pam!jarvis=c45b5feb-f9a9-445d-a626-14fbb959f78b` |
| HA Long-lived Token | `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzNmI0N2I1Njk5ZGQ0MTQ2ODMwZWFmYjZiYTQ1MjJkMSIsImlhdCI6MTc4MDIwMzU5NCwiZXhwIjoyMDk1NTYzNTk0fQ.sYRok-jRDlA4lFgWxLQELcEjkJNGQdprk6ZziLwLtXE` |
| Sonarr API | `b43e04350a594846b4ee95261c29e9e0` |
+8 -1
View File
@@ -21,7 +21,14 @@ JARVIS_HOST=""
INSTALL_DIR="/opt/jarvis-agent"
CONFIG_DIR="/etc/jarvis-agent"
STATE_DIR="/var/lib/jarvis-agent"
REG_KEY="f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518"
REG_KEY="${JARVIS_REG_KEY:-}"
if [ -z "$REG_KEY" ] && [ -r /dev/tty ]; then
read -rp "Enter JARVIS registration key: " REG_KEY </dev/tty
fi
if [ -z "$REG_KEY" ]; then
echo "ERROR: registration key required (set JARVIS_REG_KEY env var or enter at prompt)" >&2
exit 1
fi
SERVICE_FILE="/etc/systemd/system/jarvis-agent.service"
echo "=== JARVIS Agent Installer v3.0 ==="
+128 -128
View File
@@ -1,128 +1,128 @@
<?php
/**
* JARVIS API Router fault-isolated per endpoint
* A ParseError or fatal in any endpoint file returns JSON 500 for that
* endpoint only; all other endpoints continue to work normally.
*/
require_once __DIR__ . '/../api/config.php';
require_once __DIR__ . '/../api/lib/db.php';
require_once __DIR__ . '/../api/lib/kb_engine.php';
// Skip session for machine-agent calls and netscan/ping — each heartbeat would
// otherwise create an empty session file, producing millions of files that slow
// session GC for all requests. Browser-facing agent sub-actions (list/status/myip)
// still need a session to verify auth, so we only skip for machine-agent actions.
$_earlyParts = explode('/', trim(parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH), '/'));
if (($_earlyParts[0] ?? '') === 'api') array_shift($_earlyParts);
$_e0 = $_earlyParts[0] ?? '';
$_e1 = $_earlyParts[1] ?? '';
$_skipSession = match(true) {
$_e0 === 'ping' => true,
$_e0 === 'netscan' => true,
$_e0 === 'agent' && !in_array($_e1, ['list','status','myip'], true) => true,
default => false,
};
if (!$_skipSession) {
session_start();
}
header('Content-Type: application/json');
$_allowedOrigins = ['https://jarvis.orbishosting.com', 'http://jarvis.orbishosting.com'];
$_origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($_origin, $_allowedOrigins, true)) {
header('Access-Control-Allow-Origin: ' . $_origin);
header('Access-Control-Allow-Credentials: true');
}
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-Session-Token');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
$uri = $_SERVER['REQUEST_URI'] ?? '/';
$method = $_SERVER['REQUEST_METHOD'];
$path = trim(parse_url($uri, PHP_URL_PATH), '/');
$parts = explode('/', $path);
if (($parts[0] ?? '') === 'api') array_shift($parts);
$endpoint = $parts[0] ?? '';
$action = $parts[1] ?? '';
// ── Auth check (skip for auth / agent / netscan) ──────────────────────
if (!\in_array($endpoint, ['auth', 'agent', 'netscan'], true)) {
$token = $_SESSION['jarvis_token'] ?? ($_SERVER['HTTP_X_SESSION_TOKEN'] ?? '');
$isValid = !empty($token) && $token === ($_SESSION['jarvis_token'] ?? '');
if (!$isValid) {
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
$isLocal = \in_array($ip, ['127.0.0.1', '::1', JARVIS_IP], true);
if (!$isLocal && $endpoint !== 'ping') {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized', 'code' => 401]);
exit;
}
}
}
if ($endpoint !== 'auth') session_write_close();
$body = file_get_contents('php://input');
$data = json_decode($body, true) ?? [];
// ── Fast ping (no file dispatch needed) ──────────────────────────────
if ($endpoint === 'ping') {
echo json_encode(['status' => 'online', 'time' => date('c'), 'codename' => JARVIS_CODENAME]);
exit;
}
// ── Endpoint → file map ───────────────────────────────────────────────
$endpoints = [
'auth' => 'auth.php',
'chat' => 'chat.php',
'system' => 'system.php',
'netscan' => 'netscan.php',
'network' => 'network.php',
'proxmox' => 'proxmox.php',
'ha' => 'ha.php',
'tts' => 'tts.php',
'email' => 'email.php',
'do' => 'do_server.php',
'alerts' => 'alerts.php',
'facts' => 'facts_collector.php',
'weather' => 'weather.php',
'news' => 'news.php',
'sites' => 'sites.php',
'agent' => 'agent.php',
'planner' => 'planner.php',
'jellyfin' => 'jellyfin.php',
'history' => 'history.php',
'metrics' => 'metrics.php',
'suggestions' => 'suggestions.php',
'arc' => 'arc.php',
'directives' => 'directives.php',
'memory' => 'memory.php',
'calendar' => 'calendar_sync.php',
];
if (!isset($endpoints[$endpoint])) {
http_response_code(404);
echo json_encode(['error' => 'Unknown endpoint: ' . $endpoint]);
exit;
}
$file = __DIR__ . '/../api/endpoints/' . $endpoints[$endpoint];
// ── Fault-isolated dispatch ───────────────────────────────────────────
// ob_start() buffers any partial output so a mid-execution fatal doesn't
// send a broken response. catch(Throwable) catches ParseError, TypeError,
// and all other Errors + Exceptions in PHP 7+.
ob_start();
try {
require $file;
ob_end_flush();
} catch (\Throwable $e) {
ob_end_clean();
http_response_code(500);
echo json_encode(['error' => 'Endpoint unavailable', 'endpoint' => $endpoint, 'code' => 500]);
error_log(sprintf('JARVIS API [%s] %s: %s in %s:%d',
$endpoint, get_class($e), $e->getMessage(), $e->getFile(), $e->getLine()
));
}
<?php
/**
* JARVIS API Router fault-isolated per endpoint
* A ParseError or fatal in any endpoint file returns JSON 500 for that
* endpoint only; all other endpoints continue to work normally.
*/
require_once __DIR__ . '/../api/config.php';
require_once __DIR__ . '/../api/lib/db.php';
require_once __DIR__ . '/../api/lib/kb_engine.php';
// Skip session for machine-agent calls and netscan/ping — each heartbeat would
// otherwise create an empty session file, producing millions of files that slow
// session GC for all requests. Browser-facing agent sub-actions (list/status/myip)
// still need a session to verify auth, so we only skip for machine-agent actions.
$_earlyParts = explode('/', trim(parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH), '/'));
if (($_earlyParts[0] ?? '') === 'api') array_shift($_earlyParts);
$_e0 = $_earlyParts[0] ?? '';
$_e1 = $_earlyParts[1] ?? '';
$_skipSession = match(true) {
$_e0 === 'ping' => true,
$_e0 === 'netscan' => true,
$_e0 === 'agent' && !in_array($_e1, ['list','status','myip','regkey'], true) => true,
default => false,
};
if (!$_skipSession) {
session_start();
}
header('Content-Type: application/json');
$_allowedOrigins = ['https://jarvis.orbishosting.com', 'http://jarvis.orbishosting.com'];
$_origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($_origin, $_allowedOrigins, true)) {
header('Access-Control-Allow-Origin: ' . $_origin);
header('Access-Control-Allow-Credentials: true');
}
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-Session-Token');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
$uri = $_SERVER['REQUEST_URI'] ?? '/';
$method = $_SERVER['REQUEST_METHOD'];
$path = trim(parse_url($uri, PHP_URL_PATH), '/');
$parts = explode('/', $path);
if (($parts[0] ?? '') === 'api') array_shift($parts);
$endpoint = $parts[0] ?? '';
$action = $parts[1] ?? '';
// ── Auth check (skip for auth / agent / netscan) ──────────────────────
if (!\in_array($endpoint, ['auth', 'agent', 'netscan'], true)) {
$token = $_SESSION['jarvis_token'] ?? ($_SERVER['HTTP_X_SESSION_TOKEN'] ?? '');
$isValid = !empty($token) && $token === ($_SESSION['jarvis_token'] ?? '');
if (!$isValid) {
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
$isLocal = \in_array($ip, ['127.0.0.1', '::1', JARVIS_IP], true);
if (!$isLocal && $endpoint !== 'ping') {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized', 'code' => 401]);
exit;
}
}
}
if ($endpoint !== 'auth') session_write_close();
$body = file_get_contents('php://input');
$data = json_decode($body, true) ?? [];
// ── Fast ping (no file dispatch needed) ──────────────────────────────
if ($endpoint === 'ping') {
echo json_encode(['status' => 'online', 'time' => date('c'), 'codename' => JARVIS_CODENAME]);
exit;
}
// ── Endpoint → file map ───────────────────────────────────────────────
$endpoints = [
'auth' => 'auth.php',
'chat' => 'chat.php',
'system' => 'system.php',
'netscan' => 'netscan.php',
'network' => 'network.php',
'proxmox' => 'proxmox.php',
'ha' => 'ha.php',
'tts' => 'tts.php',
'email' => 'email.php',
'do' => 'do_server.php',
'alerts' => 'alerts.php',
'facts' => 'facts_collector.php',
'weather' => 'weather.php',
'news' => 'news.php',
'sites' => 'sites.php',
'agent' => 'agent.php',
'planner' => 'planner.php',
'jellyfin' => 'jellyfin.php',
'history' => 'history.php',
'metrics' => 'metrics.php',
'suggestions' => 'suggestions.php',
'arc' => 'arc.php',
'directives' => 'directives.php',
'memory' => 'memory.php',
'calendar' => 'calendar_sync.php',
];
if (!isset($endpoints[$endpoint])) {
http_response_code(404);
echo json_encode(['error' => 'Unknown endpoint: ' . $endpoint]);
exit;
}
$file = __DIR__ . '/../api/endpoints/' . $endpoints[$endpoint];
// ── Fault-isolated dispatch ───────────────────────────────────────────
// ob_start() buffers any partial output so a mid-execution fatal doesn't
// send a broken response. catch(Throwable) catches ParseError, TypeError,
// and all other Errors + Exceptions in PHP 7+.
ob_start();
try {
require $file;
ob_end_flush();
} catch (\Throwable $e) {
ob_end_clean();
http_response_code(500);
echo json_encode(['error' => 'Endpoint unavailable', 'endpoint' => $endpoint, 'code' => 500]);
error_log(sprintf('JARVIS API [%s] %s: %s in %s:%d',
$endpoint, get_class($e), $e->getMessage(), $e->getFile(), $e->getLine()
));
}
File diff suppressed because it is too large Load Diff
+8 -1
View File
@@ -21,7 +21,14 @@ JARVIS_HOST=""
INSTALL_DIR="/opt/jarvis-agent"
CONFIG_DIR="/etc/jarvis-agent"
STATE_DIR="/var/lib/jarvis-agent"
REG_KEY="f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518"
REG_KEY="${JARVIS_REG_KEY:-}"
if [ -z "$REG_KEY" ] && [ -r /dev/tty ]; then
read -rp "Enter JARVIS registration key: " REG_KEY </dev/tty
fi
if [ -z "$REG_KEY" ]; then
echo "ERROR: registration key required (set JARVIS_REG_KEY env var or enter at prompt)" >&2
exit 1
fi
SERVICE_FILE="/etc/systemd/system/jarvis-agent.service"
echo "=== JARVIS Agent Installer v3.0 ==="
+41 -20
View File
@@ -1,30 +1,51 @@
<?php
ini_set('session.cache_limiter', '');
header('Cache-Control: no-store, no-cache, must-revalidate, no-transform');
require_once __DIR__ . '/../api/config.php';
session_start();
if (!empty($_SESSION['jarvis_token'])) { header('Location: /'); exit; }
$error = '';
// ── Login rate limiting (Redis, per client IP) ────────────────────────────────
// Blocks brute force: 10 failed attempts within 15 min -> locked out for 15 min.
$clientIp = $_SERVER['HTTP_CF_CONNECTING_IP'] ?? $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$clientIp = trim(explode(',', $clientIp)[0]);
$rl = null;
try {
$rl = new Redis();
$rl->connect('127.0.0.1', 6379, 1.5);
} catch (Throwable $e) { $rl = null; } // fail open if Redis is down
$rlKey = 'login_fail:' . $clientIp;
$RL_MAX = 10; $RL_WINDOW = 900;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$u = trim($_POST['username'] ?? '');
$p = $_POST['password'] ?? '';
if ($u && $p) {
$pdo = new PDO('mysql:host=localhost;dbname=jarvis_db;charset=utf8mb4',
'jarvis_user', 'J4rv1s_Pr0t0c0l_2026!',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$row = $pdo->prepare('SELECT * FROM users WHERE username=? LIMIT 1');
$row->execute([$u]);
$user = $row->fetch(PDO::FETCH_ASSOC);
if ($user && password_verify($p, $user['password_hash'])) {
$token = bin2hex(random_bytes(32));
$_SESSION['jarvis_token'] = $token;
$_SESSION['jarvis_user_id'] = $user['id'];
$_SESSION['jarvis_name'] = $user['display_name'];
$pdo->prepare('UPDATE users SET last_seen=NOW() WHERE id=?')->execute([$user['id']]);
header('Location: /');
exit;
}
$error = 'ACCESS DENIED';
} else { $error = 'ENTER CREDENTIALS'; }
$fails = ($rl && $rl->exists($rlKey)) ? (int)$rl->get($rlKey) : 0;
if ($fails >= $RL_MAX) {
$error = 'TOO MANY ATTEMPTS — LOCKED';
} else {
$u = trim($_POST['username'] ?? '');
$p = $_POST['password'] ?? '';
if ($u && $p) {
$pdo = new PDO('mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4',
DB_USER, DB_PASS,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$row = $pdo->prepare('SELECT * FROM users WHERE username=? LIMIT 1');
$row->execute([$u]);
$user = $row->fetch(PDO::FETCH_ASSOC);
if ($user && password_verify($p, $user['password_hash'])) {
if ($rl) $rl->del($rlKey);
session_regenerate_id(true);
$token = bin2hex(random_bytes(32));
$_SESSION['jarvis_token'] = $token;
$_SESSION['jarvis_user_id'] = $user['id'];
$_SESSION['jarvis_name'] = $user['display_name'];
$pdo->prepare('UPDATE users SET last_seen=NOW() WHERE id=?')->execute([$user['id']]);
header('Location: /');
exit;
}
if ($rl) { $rl->incr($rlKey); $rl->expire($rlKey, $RL_WINDOW); }
$error = 'ACCESS DENIED';
} else { $error = 'ENTER CREDENTIALS'; }
}
}
?><!DOCTYPE html>
<html lang="en"><head>