This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* Local static HTTP server for BridgeSwarm examples (127.0.0.1).
|
||||
* Started/stopped via native messaging (examplesServer.start / stop / status).
|
||||
*/
|
||||
|
||||
const http = require('bare-http1');
|
||||
const fs = require('bare-fs');
|
||||
const path = require('bare-path');
|
||||
|
||||
const DEFAULT_HOST = '127.0.0.1';
|
||||
const DEFAULT_PORT = 4173;
|
||||
|
||||
const MIME = {
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.js': 'text/javascript; charset=utf-8',
|
||||
'.mjs': 'text/javascript; charset=utf-8',
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
'.json': 'application/json; charset=utf-8',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.gif': 'image/gif',
|
||||
'.webp': 'image/webp',
|
||||
'.ico': 'image/x-icon',
|
||||
'.woff': 'font/woff',
|
||||
'.woff2': 'font/woff2',
|
||||
'.map': 'application/json',
|
||||
'.txt': 'text/plain; charset=utf-8',
|
||||
'.md': 'text/markdown; charset=utf-8',
|
||||
};
|
||||
|
||||
/** @type {import('bare-http1').Server | null} */
|
||||
let server = null;
|
||||
let boundHost = DEFAULT_HOST;
|
||||
let boundPort = DEFAULT_PORT;
|
||||
let examplesRoot = null;
|
||||
|
||||
function exists(p) {
|
||||
try {
|
||||
return fs.existsSync(p);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveExamplesRoot() {
|
||||
if (process.env.BRIDGESWARM_EXAMPLES_DIR) {
|
||||
return path.resolve(process.env.BRIDGESWARM_EXAMPLES_DIR);
|
||||
}
|
||||
|
||||
const candidates = [];
|
||||
if (process.env.BRIDGE_SWARM_STORAGE) {
|
||||
candidates.push(path.join(path.dirname(process.env.BRIDGE_SWARM_STORAGE), 'examples'));
|
||||
}
|
||||
candidates.push(path.join(process.cwd(), 'examples'));
|
||||
candidates.push(path.join(__dirname, 'examples'));
|
||||
candidates.push(path.join(__dirname, '..', 'examples'));
|
||||
|
||||
for (const dir of candidates) {
|
||||
if (exists(path.join(dir, 'index.html'))) return dir;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function safeJoin(root, urlPath) {
|
||||
const decoded = decodeURIComponent((urlPath || '/').split('?')[0]);
|
||||
const cleaned = path.normalize(decoded).replace(/^(\.\.[/\\])+/, '');
|
||||
const full = path.join(root, cleaned);
|
||||
const rootNorm = path.resolve(root);
|
||||
const fullNorm = path.resolve(full);
|
||||
if (fullNorm !== rootNorm && !fullNorm.startsWith(rootNorm + path.sep)) return null;
|
||||
return fullNorm;
|
||||
}
|
||||
|
||||
function contentType(filePath) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
return MIME[ext] || 'application/octet-stream';
|
||||
}
|
||||
|
||||
function send(res, status, body, headers = {}) {
|
||||
const buf = typeof body === 'string' ? Buffer.from(body) : body;
|
||||
res.statusCode = status;
|
||||
res.setHeader('Cache-Control', 'no-store');
|
||||
for (const [name, value] of Object.entries(headers)) {
|
||||
res.setHeader(name, value);
|
||||
}
|
||||
res.end(buf);
|
||||
}
|
||||
|
||||
function handleRequest(req, res) {
|
||||
const root = examplesRoot;
|
||||
if (!root) {
|
||||
send(res, 503, 'Examples not found on disk');
|
||||
return;
|
||||
}
|
||||
|
||||
let urlPath = req.url || '/';
|
||||
if (urlPath === '/') urlPath = '/index.html';
|
||||
|
||||
const filePath = safeJoin(root, urlPath);
|
||||
if (!filePath) {
|
||||
send(res, 403, 'Forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
let st;
|
||||
try {
|
||||
st = fs.statSync(filePath);
|
||||
} catch (_) {
|
||||
send(res, 404, 'Not found');
|
||||
return;
|
||||
}
|
||||
|
||||
let target = filePath;
|
||||
if (st.isDirectory()) {
|
||||
target = path.join(filePath, 'index.html');
|
||||
if (!exists(target)) {
|
||||
send(res, 404, 'Not found');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const data = fs.readFileSync(target);
|
||||
send(res, 200, data, { 'Content-Type': contentType(target) });
|
||||
} catch (_) {
|
||||
send(res, 404, 'Not found');
|
||||
}
|
||||
}
|
||||
|
||||
function status() {
|
||||
const root = examplesRoot || resolveExamplesRoot();
|
||||
const url = server ? `http://${boundHost}:${boundPort}/` : null;
|
||||
return {
|
||||
ok: true,
|
||||
running: !!server,
|
||||
url,
|
||||
host: boundHost,
|
||||
port: boundPort,
|
||||
root: root || null,
|
||||
rootFound: !!(root && exists(path.join(root, 'index.html'))),
|
||||
};
|
||||
}
|
||||
|
||||
function start(opts = {}) {
|
||||
if (server) {
|
||||
return { ...status(), ok: true };
|
||||
}
|
||||
|
||||
const root = resolveExamplesRoot();
|
||||
if (!root) {
|
||||
return {
|
||||
ok: false,
|
||||
error:
|
||||
'Examples directory not found. Reinstall BridgeSwarm or set BRIDGESWARM_EXAMPLES_DIR.',
|
||||
running: false,
|
||||
url: null,
|
||||
root: null,
|
||||
rootFound: false,
|
||||
};
|
||||
}
|
||||
|
||||
examplesRoot = root;
|
||||
boundHost = typeof opts.host === 'string' && opts.host ? opts.host : DEFAULT_HOST;
|
||||
boundPort = typeof opts.port === 'number' && opts.port > 0 ? opts.port : DEFAULT_PORT;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const s = http.createServer(handleRequest);
|
||||
const onError = (err) => {
|
||||
s.removeListener('listening', onListening);
|
||||
server = null;
|
||||
resolve({
|
||||
ok: false,
|
||||
error: err.message || String(err),
|
||||
running: false,
|
||||
url: null,
|
||||
host: boundHost,
|
||||
port: boundPort,
|
||||
root: examplesRoot,
|
||||
rootFound: true,
|
||||
});
|
||||
};
|
||||
const onListening = () => {
|
||||
s.removeListener('error', onError);
|
||||
server = s;
|
||||
resolve({ ...status(), ok: true });
|
||||
};
|
||||
s.once('error', onError);
|
||||
s.once('listening', onListening);
|
||||
s.listen(boundPort, boundHost);
|
||||
});
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (!server) {
|
||||
return Promise.resolve({ ...status(), ok: true, running: false, url: null });
|
||||
}
|
||||
const s = server;
|
||||
server = null;
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
s.close(() => resolve({ ...status(), ok: true, running: false, url: null }));
|
||||
} catch (_) {
|
||||
resolve({ ...status(), ok: true, running: false, url: null });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { start, stop, status, resolveExamplesRoot, DEFAULT_HOST, DEFAULT_PORT };
|
||||
Reference in New Issue
Block a user