131 lines
4.0 KiB
JavaScript
131 lines
4.0 KiB
JavaScript
'use strict';
|
|
|
|
const http = require('http');
|
|
const path = require('path');
|
|
const { S3Client, GetObjectCommand, HeadBucketCommand } = require('@aws-sdk/client-s3');
|
|
|
|
const PORT = process.env.PORT || 3000;
|
|
const VERSION = process.env.APP_VERSION || '0.0.0';
|
|
const SERVICE = 'tf-docs';
|
|
const S3_BUCKET = process.env.S3_BUCKET || '';
|
|
const DOCS_S3_PREFIX = (process.env.DOCS_S3_PREFIX || 'docs').replace(/^\/+|\/+$/g, '');
|
|
const S3_ENDPOINT = process.env.S3_ENDPOINT || '';
|
|
const CACHE_CONTROL = process.env.CACHE_CONTROL || 'public, max-age=60';
|
|
|
|
const s3 = new S3Client({
|
|
region: process.env.S3_REGION || 'us-east-1',
|
|
endpoint: S3_ENDPOINT ? `https://${S3_ENDPOINT.replace(/^https?:\/\//, '')}` : undefined,
|
|
forcePathStyle: true,
|
|
credentials: process.env.S3_ACCESS_KEY && process.env.S3_SECRET_KEY
|
|
? { accessKeyId: process.env.S3_ACCESS_KEY, secretAccessKey: process.env.S3_SECRET_KEY }
|
|
: undefined
|
|
});
|
|
|
|
function getDocsCandidates(urlPath) {
|
|
const relativePath = decodeURIComponent(urlPath.replace(/^\/docs\/?/, ''));
|
|
const normalized = path.posix.normalize(`/${relativePath}`).replace(/^\/+|\/+$/g, '');
|
|
if (!normalized || normalized === '.' || normalized.startsWith('../') || normalized.includes('/../')) {
|
|
return null;
|
|
}
|
|
|
|
const base = `${DOCS_S3_PREFIX}/${normalized}`;
|
|
const candidates = [];
|
|
if (urlPath.endsWith('/')) {
|
|
candidates.push(`${base}/index.html`);
|
|
} else {
|
|
candidates.push(base);
|
|
if (!path.posix.extname(normalized)) candidates.push(`${base}/index.html`);
|
|
candidates.push(`${DOCS_S3_PREFIX}/${normalized.split('/').slice(0, 3).join('/')}/index.html`);
|
|
}
|
|
return [...new Set(candidates)];
|
|
}
|
|
|
|
function contentTypeFor(key) {
|
|
const types = {
|
|
'.html': 'text/html; charset=utf-8',
|
|
'.css': 'text/css; charset=utf-8',
|
|
'.js': 'application/javascript; charset=utf-8',
|
|
'.json': 'application/json; charset=utf-8',
|
|
'.svg': 'image/svg+xml',
|
|
'.png': 'image/png',
|
|
'.jpg': 'image/jpeg',
|
|
'.jpeg': 'image/jpeg',
|
|
'.webp': 'image/webp'
|
|
};
|
|
return types[path.posix.extname(key).toLowerCase()] || 'application/octet-stream';
|
|
}
|
|
|
|
async function docsHandler(req, res) {
|
|
const candidates = getDocsCandidates(req.url.split('?')[0]);
|
|
if (!candidates || !S3_BUCKET) {
|
|
res.statusCode = 400;
|
|
res.end('Invalid documentation path or S3_BUCKET is not configured');
|
|
return;
|
|
}
|
|
|
|
for (const key of candidates) {
|
|
try {
|
|
const result = await s3.send(new GetObjectCommand({ Bucket: S3_BUCKET, Key: key }));
|
|
res.statusCode = 200;
|
|
res.setHeader('Content-Type', result.ContentType || contentTypeFor(key));
|
|
res.setHeader('Cache-Control', CACHE_CONTROL);
|
|
if (result.ContentLength !== undefined) res.setHeader('Content-Length', result.ContentLength);
|
|
result.Body.pipe(res);
|
|
return;
|
|
} catch (error) {
|
|
if (error.name !== 'NoSuchKey' && error.$metadata?.httpStatusCode !== 404) {
|
|
res.statusCode = 502;
|
|
res.end('S3 request failed');
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
res.statusCode = 404;
|
|
res.end('Documentation file not found');
|
|
}
|
|
|
|
async function health(_req, res) {
|
|
if (!S3_BUCKET) {
|
|
res.statusCode = 503;
|
|
res.end('s3 unreachable: S3_BUCKET not configured');
|
|
return;
|
|
}
|
|
try {
|
|
await s3.send(new HeadBucketCommand({ Bucket: S3_BUCKET }));
|
|
res.statusCode = 200;
|
|
res.end('ok');
|
|
} catch (error) {
|
|
res.statusCode = 503;
|
|
res.end('s3 unreachable: ' + (error && error.message ? error.message : error));
|
|
}
|
|
}
|
|
|
|
const server = http.createServer((req, res) => {
|
|
if (req.url === '/health') {
|
|
health(req, res);
|
|
return;
|
|
}
|
|
if (req.url.startsWith('/docs/')) {
|
|
docsHandler(req, res);
|
|
return;
|
|
}
|
|
res.statusCode = 200;
|
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
res.end(`<!DOCTYPE html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>${SERVICE}</title>
|
|
</head>
|
|
<body>
|
|
<h1>${SERVICE}</h1>
|
|
<p>version: ${VERSION}</p>
|
|
</body>
|
|
</html>`);
|
|
});
|
|
|
|
server.listen(PORT, () => {
|
|
console.log(`[${SERVICE}] listening on :${PORT} (version ${VERSION})`);
|
|
});
|