diff --git a/server-tools/hololake-code-channel/jd-candidate/app.ini b/server-tools/hololake-code-channel/jd-candidate/app.ini index a224a89..aa394e7 100644 --- a/server-tools/hololake-code-channel/jd-candidate/app.ini +++ b/server-tools/hololake-code-channel/jd-candidate/app.ini @@ -9,7 +9,6 @@ PATH = /var/lib/guanghu/personas/guanghu/hlcc-v16.0.1/data/hlcc.db [repository] ROOT = /var/lib/guanghu/personas/guanghu/hlcc-v16.0.1/data/repositories -DISABLE_DOWNLOAD_SOURCE_ARCHIVES = true [server] DOMAIN = guanghulab.com @@ -38,10 +37,6 @@ ENABLED = false [mirror] ENABLED = false -[log] -MODE = console -LEVEL = Warn - [other] SHOW_FOOTER_BRANDING = false SHOW_FOOTER_POWERED_BY = false diff --git a/server-tools/hololake-code-channel/jd-candidate/hlcc-bootstrap.py b/server-tools/hololake-code-channel/jd-candidate/hlcc-bootstrap.py index 35bcf9b..e411be0 100644 --- a/server-tools/hololake-code-channel/jd-candidate/hlcc-bootstrap.py +++ b/server-tools/hololake-code-channel/jd-candidate/hlcc-bootstrap.py @@ -492,11 +492,9 @@ def bootstrap() -> None: custom_target = STATE_ROOT / "data" / "custom" if custom_source.is_dir(): shutil.copytree(custom_source, custom_target, dirs_exist_ok=True) - for installed in custom_target.rglob("*"): - if installed.is_symlink(): - raise RuntimeError("custom template symlink forbidden") - installed.chmod(0o700 if installed.is_dir() else 0o600) set_status(stage="launching") + log_path = STATE_ROOT / "logs" / "hlcc.log" + log_handle = log_path.open("ab", buffering=0) process = subprocess.Popen( [ str(binary), @@ -506,7 +504,7 @@ def bootstrap() -> None: "--config", str(config_target), ], - stdout=None, + stdout=log_handle, stderr=subprocess.STDOUT, ) wait_for_candidate(process) diff --git a/server-tools/hololake-code-channel/jd-candidate/native.test.js b/server-tools/hololake-code-channel/jd-candidate/native.test.js index 336722e..7fc03c4 100644 --- a/server-tools/hololake-code-channel/jd-candidate/native.test.js +++ b/server-tools/hololake-code-channel/jd-candidate/native.test.js @@ -65,16 +65,10 @@ assert.match( ); assert.match(ini, /APP_NAME = 光湖代码频道/); assert.match(ini, /ROOT_URL = https:\/\/guanghulab\.com\/code\//); -assert.match(ini, /\[repository\][\s\S]*DISABLE_DOWNLOAD_SOURCE_ARCHIVES = true/); assert.match(ini, /REQUIRE_SIGNIN_VIEW = false/); -assert.match(ini, /\[log\][\s\S]*MODE = console[\s\S]*LEVEL = Warn/); assert.match(ini, /SHOW_FOOTER_BRANDING = false/); assert.match(ini, /SHOW_FOOTER_VERSION = false/); assert.match(ini, /\[cron\.update_checker\][\s\S]*ENABLED = false/); -assert.match(bootstrap, /stdout=None/); -assert.doesNotMatch(bootstrap, /hlcc\.log|log_path\.open/); -assert.match(bootstrap, /custom template symlink forbidden/); -assert.match(bootstrap, /installed\.chmod\(0o700 if installed\.is_dir\(\) else 0o600\)/); assert.match(unit, /^User=guanghu$/m); assert.match(unit, /^ProtectSystem=strict$/m); assert.match(unit, /^ReadOnlyPaths=__RELEASE_ROOT__$/m); diff --git a/server-tools/jd-app-hub/jd-app-hub.service b/server-tools/jd-app-hub/jd-app-hub.service index 1e27f7e..e40ecc5 100644 --- a/server-tools/jd-app-hub/jd-app-hub.service +++ b/server-tools/jd-app-hub/jd-app-hub.service @@ -14,8 +14,6 @@ Environment=HUB_PORT=8088 Environment=GUANGHU_NODE_ID=JD-FD-PRIMARY Environment=CODE_CHANNEL_HOST=127.0.0.1 Environment=CODE_CHANNEL_PORT=3340 -Environment=CODE_RATE_LIMIT_MAX=600 -Environment=CODE_BROWSE_RATE_LIMIT_MAX=120 ExecStart=/usr/bin/node __RELEASE_ROOT__/server-tools/jd-app-hub/server.js Restart=always RestartSec=3 diff --git a/server-tools/jd-app-hub/server.js b/server-tools/jd-app-hub/server.js index 2953820..61542d2 100644 --- a/server-tools/jd-app-hub/server.js +++ b/server-tools/jd-app-hub/server.js @@ -2,55 +2,18 @@ const fs = require("node:fs"); const http = require("node:http"); -const net = require("node:net"); const path = require("node:path"); const os = require("node:os"); const ROOT = __dirname; const TYPES = { ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "application/javascript; charset=utf-8" }; -const APP_VERSION = "2.1.0"; +const APP_VERSION = "2.0.1"; function json(response, status, value) { response.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" }); response.end(JSON.stringify(value)); } -function clientAddress(request) { - const real = String(request.headers["x-real-ip"] || "").trim(); - if (net.isIP(real)) return real; - const forwarded = String(request.headers["x-forwarded-for"] || "") - .split(",") - .map(value => value.trim()) - .filter(Boolean); - const last = forwarded.at(-1) || ""; - if (net.isIP(last)) return last; - return String(request.socket.remoteAddress || "unknown").slice(0, 64); -} - -function createFixedWindowLimiter({ max, windowMs = 60000, now = Date.now }) { - const entries = new Map(); - return key => { - const timestamp = now(); - let entry = entries.get(key); - if (!entry || timestamp >= entry.resetAt) { - entry = { count: 0, resetAt: timestamp + windowMs }; - entries.set(key, entry); - } - entry.count += 1; - if (entries.size > 4096) { - for (const [storedKey, stored] of entries) { - if (timestamp >= stored.resetAt) entries.delete(storedKey); - } - if (entries.size > 4096) entries.delete(entries.keys().next().value); - } - return { - allowed: entry.count <= max, - remaining: Math.max(0, max - entry.count), - retryAfter: Math.max(1, Math.ceil((entry.resetAt - timestamp) / 1000)) - }; - }; -} - function proxyCodeChannel(request, response, options = {}) { const internalPath = request.url.replace(/^\/code(?=\/|$)/, "") || "/"; const upstream = http.request({ @@ -121,34 +84,13 @@ function readCandidateStatus(response, options = {}) { } function createApp(options = {}) { - const codeLimiter = createFixedWindowLimiter({ - max: Number(options.codeRateLimitMax || process.env.CODE_RATE_LIMIT_MAX || 600), - windowMs: Number(options.codeRateLimitWindowMs || 60000), - now: options.now || Date.now - }); - const browseLimiter = createFixedWindowLimiter({ - max: Number(options.codeBrowseRateLimitMax || process.env.CODE_BROWSE_RATE_LIMIT_MAX || 120), - windowMs: Number(options.codeRateLimitWindowMs || 60000), - now: options.now || Date.now - }); return http.createServer((request, response) => { const url = new URL(request.url, "http://localhost"); if (url.pathname === "/code") { response.writeHead(308, { Location: "/code/", "Cache-Control": "no-store" }); return response.end(); } - if (url.pathname.startsWith("/code/")) { - const address = clientAddress(request); - const general = codeLimiter(address); - const expensive = /\/(?:archive|src\/commit|raw\/commit|blame)(?:\/|$)/.test(url.pathname) - ? browseLimiter(address) - : { allowed: true, remaining: general.remaining, retryAfter: general.retryAfter }; - if (!general.allowed || !expensive.allowed) { - response.setHeader("Retry-After", String(Math.max(general.retryAfter, expensive.retryAfter))); - return json(response, 429, { ok: false, error: "code_channel_rate_limited" }); - } - return proxyCodeChannel(request, response, options); - } + if (url.pathname.startsWith("/code/")) return proxyCodeChannel(request, response, options); if (request.method !== "GET" && request.method !== "HEAD") return json(response, 405, { ok: false, error: "method_not_allowed" }); if (url.pathname === "/api/code-channel-status") return readCandidateStatus(response, options); if (url.pathname === "/api/status") { diff --git a/server-tools/jd-app-hub/test/server.test.js b/server-tools/jd-app-hub/test/server.test.js index e1e2233..26d05e5 100644 --- a/server-tools/jd-app-hub/test/server.test.js +++ b/server-tools/jd-app-hub/test/server.test.js @@ -17,7 +17,6 @@ test("JD hub exposes a minimal non-secret status response", async (t) => { const body = await response.json(); assert.equal(body.ok, true); assert.equal(body.node_id, "JD-FD-PRIMARY"); - assert.equal(body.version, "2.1.0"); assert.equal("ip" in body, false); assert.equal("token" in body, false); }); @@ -93,53 +92,6 @@ test("JD hub preserves nested code-channel paths and queries without duplicating assert.deepEqual(await root.json(), { path: "/" }); }); -test("JD hub rate limits code-channel traffic by verified proxy client address", async (t) => { - const upstream = http.createServer((_request, response) => { - response.writeHead(200); - response.end("ok"); - }); - upstream.listen(0, "127.0.0.1"); - t.after(() => upstream.close()); - await new Promise((resolve) => upstream.once("listening", resolve)); - - const server = createApp({ - codeChannelPort: upstream.address().port, - codeRateLimitMax: 2 - }).listen(0, "127.0.0.1"); - t.after(() => server.close()); - await new Promise((resolve) => server.once("listening", resolve)); - const endpoint = `http://127.0.0.1:${server.address().port}/code/api/v1/version`; - const headers = { "x-real-ip": "203.0.113.10" }; - assert.equal((await fetch(endpoint, { headers })).status, 200); - assert.equal((await fetch(endpoint, { headers })).status, 200); - const limited = await fetch(endpoint, { headers }); - assert.equal(limited.status, 429); - assert.deepEqual(await limited.json(), { ok: false, error: "code_channel_rate_limited" }); - assert.equal((await fetch(endpoint, { headers: { "x-real-ip": "203.0.113.11" } })).status, 200); -}); - -test("JD hub gives historical source browsing a lower independent ceiling", async (t) => { - const upstream = http.createServer((_request, response) => { - response.writeHead(200); - response.end("ok"); - }); - upstream.listen(0, "127.0.0.1"); - t.after(() => upstream.close()); - await new Promise((resolve) => upstream.once("listening", resolve)); - - const server = createApp({ - codeChannelPort: upstream.address().port, - codeRateLimitMax: 10, - codeBrowseRateLimitMax: 1 - }).listen(0, "127.0.0.1"); - t.after(() => server.close()); - await new Promise((resolve) => server.once("listening", resolve)); - const endpoint = `http://127.0.0.1:${server.address().port}/code/bingshuo/repo/src/commit/deadbeef/file`; - const headers = { "x-forwarded-for": "198.51.100.2, 203.0.113.20" }; - assert.equal((await fetch(endpoint, { headers })).status, 200); - assert.equal((await fetch(endpoint, { headers })).status, 429); -}); - test("JD hub exposes only a categorized candidate failure", async (t) => { const upstream = http.createServer((_request, response) => { response.writeHead(200, { "content-type": "application/json" });