DockerERTFF/lib/setup/routes.js

305 lines
13 KiB
JavaScript

const fs = require('fs-extra');
const debug = require('debug')('server-connect:setup:routes');
const config = require('./config');
const { map } = require('../core/async');
const { posix, extname } = require('path');
const { cache, serverConnect, templateView } = require('../core/middleware');
const database = require('./database');
const webhooks = require('./webhooks');
module.exports = async function (app) {
app.use((req, res, next) => {
req.fragment = (req.headers['accept'] || '*/*').includes('fragment');
next();
});
if (fs.existsSync('extensions/server_connect/routes')) {
const entries = fs.readdirSync('extensions/server_connect/routes', { withFileTypes: true });
for (let entry of entries) {
if (entry.isFile() && extname(entry.name) == '.js') {
let hook = require(`../../extensions/server_connect/routes/${entry.name}`);
if (hook.before) hook.before(app);
if (hook.handler) hook.handler(app);
debug(`Custom router ${entry.name} loaded`);
}
}
}
database(app);
webhooks(app);
if (config.createApiRoutes) {
fs.ensureDirSync('app/api');
createApiRoutes('app/api');
}
if (fs.existsSync('app/config/routes.json')) {
const { routes, layouts } = fs.readJSONSync('app/config/routes.json');
parseRoutes(routes, null);
function parseRoutes(routes, parent) {
for (let route of routes) {
if (!route.path) continue;
createRoute(route, parent);
if (Array.isArray(route.routes)) {
parseRoutes(route.routes, route);
}
}
}
function createRoute({ auth, path, method, redirect, url, page, layout, exec, data, ttl, status, proxy }, parent) {
method = method || 'all';
data = data || {};
if (page) page = page.replace(/^\//, '');
if (layout) layout = layout.replace(/^\//, '');
if (parent && parent.path) path = parent.path + path;
if (auth) {
app.use(path, (req, res, next) => {
if (typeof auth == 'string' && req.session && req.session[auth + 'Id']) {
next();
} else if (typeof auth == 'object' && auth.user) {
const b64auth = (req.headers.authorization || '').split(' ')[1] || '';
const [user, password] = Buffer.from(b64auth, 'base64').toString().split(':');
if (user && password && user === auth.user && password === auth.password) {
next();
} else {
res.set('WWW-Authenticate', 'Basic realm="401"');
res.status(401).json({ error: 'Unauthorized' });
}
} else {
res.status(401).json({ error: 'Unauthorized' });
}
});
}
if (proxy) {
const httpProxy = require('http-proxy');
const proxyServer = httpProxy.createProxyServer(proxy);
app.use(path, (req, res) => {
proxyServer.web(req, res);
});
} else if (redirect) {
app.get(path, (req, res) => res.redirect(status == 302 ? 302 : 301, redirect));
} else if (url) {
app[method](path, (req, res, next) => {
next(parent && !req.fragment ? 'route' : null);
}, (req, res) => {
res.sendFile(url, { root: 'public' })
});
if (parent) {
createRoute({
path,
method: parent.method,
redirect: parent.redirect,
url: parent.url,
page: parent.page,
layout: parent.layout,
exec: parent.exec,
data: parent.data
});
}
} else if (page) {
if (path == '/404') {
app.set('has404', true);
}
if (path == '/500') {
app.set('has500', true);
}
if (exec) {
if (fs.existsSync(`app/${exec}.json`)) {
let json = fs.readJSONSync(`app/${exec}.json`);
if (json.exec && json.exec.steps) {
json = json.exec.steps;
} else if (json.steps) {
json = json.steps;
}
if (!Array.isArray(json)) {
json = [json];
}
if (layout && layouts && layouts[layout]) {
if (layouts[layout].data) {
data = Object.assign({}, layouts[layout].data, data);
}
if (layouts[layout].exec) {
if (fs.existsSync(`app/${layouts[layout].exec}.json`)) {
let _json = fs.readJSONSync(`app/${layouts[layout].exec}.json`);
if (_json.exec && _json.exec.steps) {
_json = _json.exec.steps;
} else if (_json.steps) {
_json = _json.steps;
}
if (!Array.isArray(_json)) {
_json = [_json];
}
json = _json.concat(json);
} else {
debug(`Route ${path} skipped, "app/${exec}.json" not found`);
return;
}
}
}
app[method](path, (req, res, next) => {
next(parent && !req.fragment ? 'route' : null);
}, cache({ttl}), templateView(layout, page, data, json));
} else {
debug(`Route ${path} skipped, "app/${exec}.json" not found`);
return;
}
} else {
let json = [];
if (layout && layouts && layouts[layout]) {
if (layouts[layout].data) {
data = Object.assign({}, layouts[layout].data, data);
}
if (layouts[layout].exec) {
if (fs.existsSync(`app/${layouts[layout].exec}.json`)) {
let _json = fs.readJSONSync(`app/${layouts[layout].exec}.json`);
if (_json.exec && _json.exec.steps) {
_json = _json.exec.steps;
} else if (_json.steps) {
_json = _json.steps;
}
if (!Array.isArray(_json)) {
_json = [_json];
}
json = _json.concat(json);
} else {
debug(`Route ${path} skipped, "app/${exec}.json" not found`);
return;
}
}
}
app[method](path, (req, res, next) => {
next(parent && !req.fragment ? 'route' : null);
}, cache({ttl}), templateView(layout, page, data, json));
}
if (parent) {
createRoute({
path,
method: parent.method,
redirect: parent.redirect,
url: parent.url,
page: parent.page,
layout: parent.layout,
exec: parent.exec,
data: parent.data
});
}
} else if (exec) {
if (fs.existsSync(`app/${exec}.json`)) {
let json = fs.readJSONSync(`app/${exec}.json`);
app[method](path, cache({ttl}), serverConnect(json));
return;
}
}
}
}
if (fs.existsSync('extensions/server_connect/routes')) {
const entries = fs.readdirSync('extensions/server_connect/routes', { withFileTypes: true });
for (let entry of entries) {
if (entry.isFile() && extname(entry.name) == '.js') {
let hook = require(`../../extensions/server_connect/routes/${entry.name}`);
if (hook.after) hook.after(app);
debug(`Custom router ${entry.name} loaded`);
}
}
}
function createApiRoutes(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
return map(entries, async (entry) => {
if (entry.name.startsWith('_')) return;
let path = posix.join(dir, entry.name);
if (entry.isFile() && extname(path) == '.json') {
let json = fs.readJSONSync(path);
let routePath = (json.settings?.options?.path) ? json.settings.options.path : path.replace(/^app/i, '').replace(/.json$/, '(.json)?');
let routeMethod = (json.settings?.options?.method) ? json.settings.options.method : 'all';
let ttl = (json.settings?.options?.ttl) ? json.settings.options.ttl : 0;
let csrf = (json.settings?.options?.nocsrf) ? false : config.csrf?.enabled;
let points = (json.settings?.options?.points) ? json.settings.options.points : 1;
app[routeMethod](routePath.replace(/\/\(.*?\)\//gi, '/'), (req, res, next) => {
if (app.rateLimiter && points > 0) {
let isPrivate = false;
let key = req.ip;
if (app.privateRateLimiter) {
if (req.session && req.session[config.rateLimit.private.provider + 'Id']) {
isPrivate = true;
key = req.session[config.rateLimit.private.provider + 'Id'];
}
}
app[isPrivate ? 'privateRateLimiter' : 'rateLimiter'].consume(key, points).then(rateLimiterRes => {
const reset = Math.ceil(rateLimiterRes.msBeforeNext / 1000);
const { points, duration } = isPrivate ? config.rateLimit.private : config.rateLimit;
res.set('RateLimit-Policy', `${points};w=${duration}`);
res.set('RateLimit', `limit=${points}, remaining=${rateLimiterRes.remainingPoints}, reset=${reset}`);
next();
}).catch(rateLimiterRes => {
const reset = Math.ceil(rateLimiterRes.msBeforeNext / 1000);
const { points, duration } = isPrivate ? config.rateLimit.private : config.rateLimit;
res.set('RateLimit-Policy', `${points};w=${duration}`);
res.set('RateLimit', `limit=${points}, remaining=${rateLimiterRes.remainingPoints}, reset=${reset}`);
res.set('Retry-After', reset);
if (req.is('json')) {
res.status(429).json({ error: 'Too Many Requests' });
} else {
res.status(429).send('Too Many Requests');
}
});
} else {
next();
}
}, (req, res, next) => {
if (!csrf) return next();
if (config.csrf.exclude.split(',').includes(req.method)) return next();
if (!req.validateCSRF()) return res.status(403).send('Invalid CSRF token');
next();
}, cache({ttl}), serverConnect(json));
debug(`Api route ${routePath} created`);
}
if (entry.isDirectory()) {
return createApiRoutes(path);
}
});
}
};