bend_puzzle/server/apache-bridge.js
33333-33333 9d70afb4cc
Some checks failed
BEND FIELD CI / release (push) Has been cancelled
BEND FIELD CI / production-bridge (push) Has been cancelled
t
2026-08-01 22:31:04 +09:00

101 lines
4.4 KiB
JavaScript

'use strict';
const path = require('path');
const BEGIN_MARKER = '# BEGIN LINKFIELD MANAGED PROXY';
const END_MARKER = '# END LINKFIELD MANAGED PROXY';
function normalizePort(value) {
const port = Number(value);
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) throw new Error(`Invalid Apache bridge port: ${value}`);
return port;
}
function renderPublicBoundary(){
return `\n`+
` # The service source may share this directory with Apache. Expose only\n`+
` # the curated browser bundle and deny every other direct file request.\n`+
` RewriteRule ^$ - [L]\n`+
` RewriteRule ^(?:index\\.html|style\\.css|favicon\\.(?:svg|ico)|build-meta\\.js|runtime-config\\.js|shared-contracts\\.js|store-catalog\\.(?:generated\\.js|json)|puzzle-patterns\\.js|puzzle-core\\.js|app-logic\\.js|archive-codec\\.js|field-persistence(?:-worker)?\\.js|puzzle-worker\\.js|app\\.js|api-bridge\\.php)$ - [L]\n`+
` RewriteRule ^(?:assets|client)(?:/|$) - [L]\n`+
` RewriteRule ^ - [F,L]\n`;
}
function renderSensitiveFileFallback(){
return `<FilesMatch "(?i)^(?:\\.|server\\.js$|realtime-server\\.js$|package(?:-lock)?\\.json$|build-config\\.json$|README\\.md$)">\n`+
` Require all denied\n`+
`</FilesMatch>\n`;
}
function renderApacheBridge(portValue) {
const port = normalizePort(portValue);
return `${BEGIN_MARKER}\n` +
`ServerSignature Off\n` +
`<IfModule mod_headers.c>\n` +
` Header always unset X-Powered-By\n` +
`</IfModule>\n` +
`<IfModule mod_rewrite.c>\n` +
` RewriteEngine On\n` +
`\n` +
` # Prefer a native Apache proxy when the host permits it.\n` +
` <IfModule mod_proxy.c>\n` +
` <IfModule mod_proxy_wstunnel.c>\n` +
` RewriteCond %{HTTP:Upgrade} =websocket [NC]\n` +
` RewriteRule ^api/realtime/?$ ws://127.0.0.1:${port}/api/realtime [P,L]\n` +
` </IfModule>\n` +
` <IfModule mod_proxy_http.c>\n` +
` RewriteRule ^api/(.*)$ http://127.0.0.1:${port}/api/$1 [P,L]\n` +
` </IfModule>\n` +
` </IfModule>\n` +
`\n` +
` # Shared hosts often disable mod_proxy. Route ordinary API requests\n` +
` # through the bundled PHP bridge instead.\n` +
` RewriteCond %{REQUEST_FILENAME} !-f\n` +
` RewriteRule ^api/(.*)$ api-bridge.php?path=/api/$1 [QSA,L]\n` +
renderPublicBoundary()+
`</IfModule>\n` +
renderSensitiveFileFallback()+
`${END_MARKER}\n`;
}
function renderApacheBootstrap(){
return `${BEGIN_MARKER}\n`+
`ServerSignature Off\n`+
`<IfModule mod_headers.c>\n`+
` Header always unset X-Powered-By\n`+
`</IfModule>\n`+
`<IfModule mod_rewrite.c>\n`+
` RewriteEngine On\n\n`+
` # Safe bootstrap: use the bounded PHP bridge until the Node server\n`+
` # writes a verified current proxy port after it begins listening.\n`+
` RewriteCond %{REQUEST_FILENAME} !-f\n`+
` RewriteRule ^api/(.*)$ api-bridge.php?path=/api/$1 [QSA,L]\n`+
renderPublicBoundary()+
`</IfModule>\n`+
renderSensitiveFileFallback()+
`${END_MARKER}\n`;
}
function replaceManagedBlock(existingValue, managedBlock) {
const existing = String(existingValue || '').replace(/\r\n?/g, '\n');
const pattern = new RegExp(`(?:^|\\n)${BEGIN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${END_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\n|$)`, 'g');
const preserved = existing.replace(pattern, '\n').replace(/^\n+|\n+$/g, '');
return preserved ? `${preserved}\n\n${managedBlock}` : managedBlock;
}
async function installApacheBridge({fsp, root, port, enabled = true} = {}) {
if (!enabled) return {enabled:false, written:false, file:null};
if (!fsp || typeof fsp.readFile !== 'function' || typeof fsp.writeFile !== 'function') throw new Error('A promises-compatible filesystem is required');
const file = path.join(root, '.htaccess');
let existing = '';
try { existing = await fsp.readFile(file, 'utf8'); }
catch (error) { if (error?.code !== 'ENOENT') throw error; }
const next = replaceManagedBlock(existing, renderApacheBridge(port));
if (next === existing.replace(/\r\n?/g, '\n')) return {enabled:true, written:false, file};
const temporary = `${file}.linkfield-${process.pid}-${Date.now()}.tmp`;
await fsp.writeFile(temporary, next, {encoding:'utf8', mode:0o644});
await fsp.rename(temporary, file);
return {enabled:true, written:true, file};
}
module.exports = Object.freeze({BEGIN_MARKER, END_MARKER, renderApacheBridge,renderApacheBootstrap,replaceManagedBlock,installApacheBridge});