mirror of
https://github.com/myronblair/jarvis
synced 2026-07-29 17:22:35 -05:00
Compare commits
12 Commits
18783dc137
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| ee0e116963 | |||
| adbd1a7a24 | |||
| 73aac8ab01 | |||
| 646da21b86 | |||
| d97a345672 | |||
| 141191bcdd | |||
| 8399048252 | |||
| 652e44f7b3 | |||
| 43be3e1105 | |||
| 3d16061709 | |||
| 80588efa7a | |||
| f7309a15fc |
@@ -1,4 +1,5 @@
|
||||
# Credentials - never commit
|
||||
public_html/admin/downloads/INFRASTRUCTURE-REFERENCE.md
|
||||
api/config.php
|
||||
backup/
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@ fi
|
||||
SUBNET="10.48.200.0/24"
|
||||
|
||||
TMPFILE=$(mktemp)
|
||||
nmap -sn --send-ip "$SUBNET" 2>/dev/null > "$TMPFILE"
|
||||
nmap -sn "$SUBNET" 2>/dev/null > "$TMPFILE"
|
||||
|
||||
if [ ! -s "$TMPFILE" ]; then
|
||||
echo "$(date): nmap produced no output" >&2
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -197,6 +197,11 @@ function collect_all(): array {
|
||||
'orbisportal' => 'https://orbis.orbishosting.com',
|
||||
'tomtomgames' => 'https://tomtomgames.com',
|
||||
];
|
||||
// Sites intentionally gated behind HTTP Basic Auth (e.g. a password-protected
|
||||
// "Coming Soon" page during a rebuild) — treat these status codes as up, not down.
|
||||
$expectedCodes = [
|
||||
'parkerslingshotrentals' => [401],
|
||||
];
|
||||
$down = [];
|
||||
foreach ($sites as $key => $url) {
|
||||
$parsed = parse_url($url);
|
||||
@@ -216,7 +221,8 @@ function collect_all(): array {
|
||||
curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
$status = ($code >= 200 && $code < 400) ? 'up' : "down-$code";
|
||||
$ok = ($code >= 200 && $code < 400) || in_array($code, $expectedCodes[$key] ?? [], true);
|
||||
$status = $ok ? 'up' : "down-$code";
|
||||
KBEngine::storeFact('sites', $key, $status, $url, 180);
|
||||
if ($status !== 'up') $down[] = "$key($code)";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -35,6 +35,11 @@ foreach ($devices as $d) {
|
||||
if (!$ip) continue;
|
||||
|
||||
$discoveredIPs[] = $ip;
|
||||
if ($mac) {
|
||||
// Device likely moved to a new IP (DHCP) — drop the stale row so it
|
||||
// doesn't linger as an orphaned duplicate under the old address.
|
||||
JarvisDB::execute('DELETE FROM network_devices WHERE mac=? AND ip<>?', [$mac, $ip]);
|
||||
}
|
||||
JarvisDB::execute(
|
||||
'INSERT INTO network_devices (ip, mac, hostname, status, last_seen)
|
||||
VALUES (?,?,?,?,NOW())
|
||||
|
||||
@@ -213,7 +213,7 @@ if ($weatherAge > 1800) {
|
||||
};
|
||||
|
||||
$weatherRaw = curlGet(
|
||||
'https://wttr.in/FortWorth,TX?format=j1',
|
||||
'https://wttr.in/76088?format=j1',
|
||||
['User-Agent: curl/7.88 Jarvis/1.0'],
|
||||
15
|
||||
);
|
||||
@@ -245,7 +245,7 @@ if ($weatherAge > 1800) {
|
||||
|
||||
cacheStore('weather', [
|
||||
'source' => 'wttr.in',
|
||||
'location' => 'Fort Worth, TX',
|
||||
'location' => 'Weatherford, TX',
|
||||
'current' => [
|
||||
'temp' => (int)($cu['temp_F'] ?? 0),
|
||||
'feels' => (int)($cu['FeelsLikeF'] ?? 0),
|
||||
|
||||
@@ -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
|
||||
+10
-1
@@ -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)
|
||||
@@ -28,6 +29,14 @@ if mysqldump -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" > "$TMPDIR/jarvis_db.sql" 2>>"
|
||||
cp -a /etc/nginx/sites-enabled/jarvis "$TMPDIR/files/etc/nginx-site-jarvis" 2>>"$LOG"
|
||||
cp -a /etc/systemd/system/jarvis-arc.service "$TMPDIR/files/etc/jarvis-arc.service" 2>>"$LOG"
|
||||
crontab -l > "$TMPDIR/files/etc/root-crontab.txt" 2>>"$LOG"
|
||||
# Phase 1/2 additions: secrets + systemd drop-ins + operational scripts
|
||||
# (these live outside /var/www/jarvis and /opt/jarvis-arc, so must be
|
||||
# captured explicitly or a restore comes back with no keys/DB pass).
|
||||
cp -a /etc/jarvis-arc "$TMPDIR/files/etc/jarvis-arc-etc" 2>>"$LOG" # reactor.env
|
||||
cp -a /etc/jarvis "$TMPDIR/files/etc/jarvis-etc" 2>>"$LOG" # db.env
|
||||
cp -a /etc/systemd/system/jarvis-arc.service.d "$TMPDIR/files/etc/jarvis-arc.service.d" 2>>"$LOG"
|
||||
mkdir -p "$TMPDIR/files/usr-local-bin"
|
||||
cp -a /usr/local/bin/jarvis-*.sh "$TMPDIR/files/usr-local-bin/" 2>>"$LOG" # health/deploy/watchdog/netscan
|
||||
|
||||
tar -czf "$OUTFILE" -C "$TMPDIR" jarvis_db.sql files
|
||||
SIZE=$(du -sh "$OUTFILE" | cut -f1)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/bin/bash
|
||||
# JARVIS Health Self-Check — runs every 5 min via root cron (separate from the
|
||||
# service watchdog). Detects silent failures the watchdog can't: stalled crons,
|
||||
# stuck Arc jobs, low disk, and services the watchdog had to restart. Writes
|
||||
# findings to the `alerts` table (auto-resolving when clear) and emails on any
|
||||
# NEW finding. Added 2026-07-07 (Phase 2 reliability).
|
||||
set -u
|
||||
[ -r /etc/jarvis/db.env ] && . /etc/jarvis/db.env
|
||||
DB_USER="jarvis_user"; DB_NAME="jarvis_db"
|
||||
MYSQL=(mysql -u "$DB_USER" -p"${JARVIS_DB_PASS:-}" "$DB_NAME" -N -B -e)
|
||||
CRONLOG=/var/log/jarvis/cron.log
|
||||
WDLOG=/var/log/jarvis/watchdog.log
|
||||
ALERT_TO="myronblair@gmail.com"
|
||||
REACTOR_ENV=/etc/jarvis-arc/reactor.env
|
||||
NEW_FINDINGS=""
|
||||
|
||||
sql() { "${MYSQL[@]}" "$1" 2>/dev/null; }
|
||||
esc() { printf '%s' "$1" | sed "s/'/''/g"; }
|
||||
|
||||
# Raise (or keep) an alert for a condition. Emails only when it is newly raised.
|
||||
# $1=source_key $2=severity $3=title $4=message
|
||||
raise() {
|
||||
local key sev title msg exists
|
||||
key=$(esc "$1"); sev=$(esc "$2"); title=$(esc "$3"); msg=$(esc "$4")
|
||||
exists=$(sql "SELECT COUNT(*) FROM alerts WHERE source_key='$key' AND resolved=0")
|
||||
if [ "${exists:-0}" = "0" ]; then
|
||||
sql "INSERT INTO alerts (alert_type,title,message,severity,source_key,auto_resolve,created_at)
|
||||
VALUES ('health','$title','$msg','$sev','$key',1,NOW())"
|
||||
NEW_FINDINGS="${NEW_FINDINGS}- [$2] $3: $4"$'\n'
|
||||
fi
|
||||
}
|
||||
# Clear a condition's alert when it's no longer true.
|
||||
clear_cond() {
|
||||
local key; key=$(esc "$1")
|
||||
sql "UPDATE alerts SET resolved=1, resolved_at=NOW()
|
||||
WHERE source_key='$key' AND resolved=0 AND auto_resolve=1"
|
||||
}
|
||||
|
||||
# 1) Disk usage on / > 85%
|
||||
DISK=$(df / | tail -1 | awk '{print $5}' | tr -d '%')
|
||||
if [ "${DISK:-0}" -gt 85 ]; then
|
||||
raise "health:disk" "critical" "Disk usage high" "Root filesystem at ${DISK}% (threshold 85%)."
|
||||
else clear_cond "health:disk"; fi
|
||||
|
||||
# 2) Arc jobs stuck in 'running' > 30 min
|
||||
STUCK=$(sql "SELECT COUNT(*) FROM arc_jobs WHERE status='running'
|
||||
AND COALESCE(started_at, created_at) < NOW() - INTERVAL 30 MINUTE")
|
||||
if [ "${STUCK:-0}" -gt 0 ]; then
|
||||
raise "health:arc_stuck" "critical" "Arc jobs stuck" "$STUCK Arc job(s) have been 'running' for over 30 minutes."
|
||||
else clear_cond "health:arc_stuck"; fi
|
||||
|
||||
# 3) Cron stalled — cron.log is written by facts_collector every 3 min. If it
|
||||
# hasn't changed in 10 min (>2 consecutive missed runs), cron work has stopped.
|
||||
if [ -f "$CRONLOG" ]; then
|
||||
AGE=$(( $(date +%s) - $(stat -c %Y "$CRONLOG") ))
|
||||
if [ "$AGE" -gt 600 ]; then
|
||||
raise "health:cron" "critical" "Cron jobs stalled" "No cron activity in $((AGE/60)) min (facts_collector runs every 3 min) — crons appear stopped."
|
||||
else clear_cond "health:cron"; fi
|
||||
fi
|
||||
|
||||
# 4) Watched service restarted by the watchdog in the last 6 min
|
||||
if [ -f "$WDLOG" ]; then
|
||||
RECENT=$(awk -v cutoff="$(date -d '6 minutes ago' '+%Y-%m-%d %H:%M:%S')" \
|
||||
'match($0,/^\[([0-9-]+ [0-9:]+)\]/,m){ if(m[1]>=cutoff && /restarted successfully/) print }' "$WDLOG")
|
||||
if [ -n "$RECENT" ]; then
|
||||
SVC=$(printf '%s' "$RECENT" | grep -oE '(nginx|php8.3-fpm|mariadb|redis-server)' | sort -u | tr '\n' ' ')
|
||||
raise "health:wd_restart" "warning" "Service auto-restarted" "Watchdog restarted: ${SVC:-a service}. Investigate why it died."
|
||||
else
|
||||
clear_cond "health:wd_restart"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Email any NEW findings via the reactor's Gmail SMTP creds.
|
||||
if [ -n "$NEW_FINDINGS" ]; then
|
||||
GPASS=""
|
||||
[ -r "$REACTOR_ENV" ] && GPASS=$(grep -E '^GMAIL_PASS=' "$REACTOR_ENV" | cut -d= -f2-)
|
||||
if [ -n "$GPASS" ]; then
|
||||
GMAIL_PASS="$GPASS" ALERT_TO="$ALERT_TO" FINDINGS="$NEW_FINDINGS" python3 - <<'PY'
|
||||
import os, smtplib, ssl, socket
|
||||
from email.mime.text import MIMEText
|
||||
user = "myronblair@gmail.com"
|
||||
msg = MIMEText("JARVIS health self-check raised new alerts on %s:\n\n%s" % (socket.gethostname(), os.environ["FINDINGS"]))
|
||||
msg["Subject"] = "JARVIS health alert"
|
||||
msg["From"] = user; msg["To"] = os.environ["ALERT_TO"]
|
||||
try:
|
||||
with smtplib.SMTP("smtp.gmail.com", 587, timeout=20) as s:
|
||||
s.starttls(context=ssl.create_default_context())
|
||||
s.login(user, os.environ["GMAIL_PASS"])
|
||||
s.send_message(msg)
|
||||
print("health-email: sent")
|
||||
except Exception as e:
|
||||
print("health-email: FAILED", e)
|
||||
PY
|
||||
else
|
||||
echo "health-email: no GMAIL_PASS available, skipped"
|
||||
fi
|
||||
fi
|
||||
@@ -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"; }
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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()
|
||||
));
|
||||
}
|
||||
|
||||
@@ -982,6 +982,7 @@ async function loadWeather() {
|
||||
const d = await api('weather');
|
||||
if (!d || !d.current) return;
|
||||
const c = d.current;
|
||||
if (d.location) document.getElementById('weather-loc').textContent = d.location.toUpperCase();
|
||||
document.getElementById('weather-temp').textContent = c.temp;
|
||||
document.getElementById('weather-desc').textContent = (c.desc || '').toUpperCase();
|
||||
document.getElementById('weather-feels').textContent = c.feels + '°F';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -84,7 +84,7 @@
|
||||
<div id="leftPanel">
|
||||
<!-- Weather Widget -->
|
||||
<div class="panel" style="flex:0 0 auto">
|
||||
<div class="panel-title">WEATHER <span id="weather-loc" style="font-size:0.55rem;color:var(--text-dim)">FORT WORTH, TX</span></div>
|
||||
<div class="panel-title">WEATHER <span id="weather-loc" style="font-size:0.55rem;color:var(--text-dim)">WEATHERFORD, TX</span></div>
|
||||
<div style="display:flex;align-items:flex-start;gap:12px;margin-bottom:8px">
|
||||
<div style="flex:1">
|
||||
<div style="display:flex;align-items:baseline;gap:8px">
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user