mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 05:31:47 +00:00
fix(auth): real client IP rate-limiting + remote default-password guard
- Add custom-server.js: inject unspoofable socket IP, strip client XFF (wired into Docker CMD + CLI spawn + build-cli copy) - loginLimiter: key on trusted x-9r-real-ip, TRUST_PROXY opt-in, global fallback - Force password change on first remote login while default is in use - Add /api/auth/reset-password (local-only) so CLI reset writes live SQLite - CLI settings: reset via API instead of stale db.json - Fix OAuth modals opening duplicate browser tabs on add-connection - Add cli:pack / cli:publish scripts Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+6
-2
@@ -470,9 +470,13 @@ function openBrowser(url) {
|
||||
});
|
||||
}
|
||||
|
||||
// Find standalone server (bundled in bin/app for published package)
|
||||
// Find standalone server (bundled in bin/app for published package).
|
||||
// Prefer custom-server.js (injects real socket IP) when present.
|
||||
const standaloneDir = path.join(__dirname, "app");
|
||||
const serverPath = path.join(standaloneDir, "server.js");
|
||||
const customServerPath = path.join(standaloneDir, "custom-server.js");
|
||||
const serverPath = fs.existsSync(customServerPath)
|
||||
? customServerPath
|
||||
: path.join(standaloneDir, "server.js");
|
||||
|
||||
if (!fs.existsSync(serverPath)) {
|
||||
console.error("Error: Standalone build not found.");
|
||||
|
||||
@@ -154,6 +154,15 @@ if (standaloneApp !== standaloneRootToUse && fs.existsSync(standaloneNodeModules
|
||||
}
|
||||
console.log("✅ Copied standalone build\n");
|
||||
|
||||
// Step 3a: Copy custom server (injects real socket IP, strips spoofable XFF).
|
||||
const customServerSrc = path.join(appDir, "custom-server.js");
|
||||
if (fs.existsSync(customServerSrc)) {
|
||||
fs.copyFileSync(customServerSrc, path.join(cliAppDir, "custom-server.js"));
|
||||
console.log("✅ Copied custom-server.js\n");
|
||||
} else {
|
||||
console.warn("⚠️ custom-server.js not found — server will run without real-IP injection\n");
|
||||
}
|
||||
|
||||
// Step 3b: Ensure sql.js (pure JS fallback) bundled in app/cli/app/node_modules.
|
||||
// Strip better-sqlite3 (native) — it lives in ~/.9router/runtime to avoid
|
||||
// Windows EBUSY during global CLI updates. node:sqlite (Node ≥22.5) is also
|
||||
|
||||
@@ -410,6 +410,14 @@ async function updateSettings(data) {
|
||||
return makeRequest("PATCH", "/api/settings", data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset dashboard password to default (clears stored hash server-side)
|
||||
* @returns {Promise<Object>} { success }
|
||||
*/
|
||||
async function resetPassword() {
|
||||
return makeRequest("POST", "/api/auth/reset-password");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MODELS API
|
||||
// ============================================================================
|
||||
@@ -528,6 +536,7 @@ module.exports = {
|
||||
// Settings
|
||||
getSettings,
|
||||
updateSettings,
|
||||
resetPassword,
|
||||
|
||||
// Tunnel
|
||||
getTunnelStatus,
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const api = require("../api/client");
|
||||
const { confirm, pause } = require("../utils/input");
|
||||
const { showStatus } = require("../utils/display");
|
||||
@@ -18,13 +15,6 @@ const COLORS = {
|
||||
|
||||
const DEFAULT_PASSWORD = "123456";
|
||||
|
||||
// Resolve db.json path (matches app/src/lib/dataDir.js convention)
|
||||
function getDbPath() {
|
||||
return process.platform === "win32"
|
||||
? path.join(process.env.APPDATA || "", "9router", "db.json")
|
||||
: path.join(os.homedir(), ".9router", "db.json");
|
||||
}
|
||||
|
||||
/**
|
||||
* Show settings menu (tunnel + RTK + reset password)
|
||||
* @param {Array<string>} breadcrumb - Breadcrumb path
|
||||
@@ -171,18 +161,10 @@ async function toggleRtk(currentlyOn) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset dashboard password by clearing the hash in db.json (Phase B).
|
||||
* Reset dashboard password to default via server API (writes the live SQLite DB).
|
||||
* After reset, user can log in with the default password "123456".
|
||||
*/
|
||||
async function resetPassword() {
|
||||
const dbPath = getDbPath();
|
||||
|
||||
if (!fs.existsSync(dbPath)) {
|
||||
showStatus(`db.json not found at ${dbPath}`, "error");
|
||||
await pause();
|
||||
return;
|
||||
}
|
||||
|
||||
const ok = await confirm(`Reset dashboard password to default "${DEFAULT_PASSWORD}"?`);
|
||||
if (!ok) {
|
||||
showStatus("Cancelled", "info");
|
||||
@@ -190,16 +172,11 @@ async function resetPassword() {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(dbPath, "utf-8");
|
||||
const db = JSON.parse(raw);
|
||||
if (db.settings && Object.prototype.hasOwnProperty.call(db.settings, "password")) {
|
||||
delete db.settings.password;
|
||||
}
|
||||
fs.writeFileSync(dbPath, JSON.stringify(db, null, 2));
|
||||
const result = await api.resetPassword();
|
||||
if (result.success) {
|
||||
showStatus(`Password reset. Default: ${DEFAULT_PASSWORD}`, "success");
|
||||
} catch (err) {
|
||||
showStatus(`Failed to reset password: ${err.message}`, "error");
|
||||
} else {
|
||||
showStatus(`Failed to reset password: ${result.error}`, "error");
|
||||
}
|
||||
await pause();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user