UNPKG

@underpostnet/underpost

Version:
500 lines (423 loc) 17.9 kB
import fs from 'fs-extra'; import express from 'express'; import cors from 'cors'; import dotenv from 'dotenv'; import fileUpload from 'express-fileupload'; import swaggerUi from 'swagger-ui-express'; import * as promClient from 'prom-client'; import compression from 'compression'; import { createServer } from 'http'; import { getRootDirectory } from './process.js'; import UnderpostStartUp from './start.js'; import { loggerFactory, loggerMiddleware } from './logger.js'; import { getCapVariableName, newInstance } from '../client/components/core/CommonJs.js'; import { Xampp } from '../runtime/xampp/Xampp.js'; import { MailerProvider } from '../mailer/MailerProvider.js'; import { DataBaseProvider } from '../db/DataBaseProvider.js'; // import { createProxyMiddleware } from 'http-proxy-middleware'; import { createPeerServer } from './peer.js'; import { Lampp } from '../runtime/lampp/Lampp.js'; import { getDeployId } from './conf.js'; import { JSONweb, ssrFactory } from './client-formatted.js'; import Underpost from '../index.js'; import { createValkeyConnection } from './valkey.js'; dotenv.config(); const logger = loggerFactory(import.meta); const buildRuntime = async () => { const deployId = getDeployId(); const collectDefaultMetrics = promClient.collectDefaultMetrics; collectDefaultMetrics(); const promCounterOption = { name: `${deployId.replaceAll('-', '_')}_http_requests_total`, help: 'Total number of HTTP requests', labelNames: ['instance', 'method', 'status_code'], }; // logger.info('promCounterOption', promCounterOption); const requestCounter = new promClient.Counter(promCounterOption); const initPort = parseInt(process.env.PORT) + 1; let currentPort = initPort; const confServer = JSON.parse(fs.readFileSync(`./conf/conf.server.json`, 'utf8')); const confSSR = JSON.parse(fs.readFileSync(`./conf/conf.ssr.json`, 'utf8')); const singleReplicaHosts = []; for (const host of Object.keys(confServer)) { if (singleReplicaHosts.length > 0 && !singleReplicaHosts.includes(host)) { currentPort += singleReplicaHosts.reduce((accumulator, currentValue) => accumulator + currentValue.replicas, 0); } const rootHostPath = `/public/${host}`; for (const path of Object.keys(confServer[host])) { confServer[host][path].port = newInstance(currentPort); const { runtime, port, client, apis, origins, directory, ws, mailer, db, redirect, peer, singleReplica, replicas, valkey, } = confServer[host][path]; if (singleReplica && replicas && replicas.length > 0 && !singleReplicaHosts.includes(host)) { singleReplicaHosts.push({ host, replicas: replicas.length, }); continue; } const runningData = { host, path, runtime, client, meta: import.meta, apis, }; const redirectTarget = redirect ? redirect[redirect.length - 1] === '/' ? redirect.slice(0, -1) : redirect : undefined; // if (redirect) logger.info('redirect', new URL(redirect)); switch (runtime) { case 'lampp': if (!Lampp.enabled()) continue; if (!Lampp.ports.includes(port)) Lampp.ports.push(port); if (currentPort === initPort) Lampp.removeRouter(); Lampp.appendRouter(` Listen ${port} <VirtualHost *:${port}> DocumentRoot "${directory ? directory : `${getRootDirectory()}${rootHostPath}`}" ServerName ${host}:${port} <Directory "${directory ? directory : `${getRootDirectory()}${rootHostPath}`}"> Options Indexes FollowSymLinks MultiViews AllowOverride All Require all granted </Directory> ${ redirect ? ` RewriteEngine on RewriteCond %{REQUEST_URI} !^/.well-known/acme-challenge RewriteRule ^(.*)$ ${redirectTarget}%{REQUEST_URI} [R=302,L] ` : '' } ErrorDocument 400 ${path === '/' ? '' : path}/400.html ErrorDocument 404 ${path === '/' ? '' : path}/400.html ErrorDocument 500 ${path === '/' ? '' : path}/500.html ErrorDocument 502 ${path === '/' ? '' : path}/500.html ErrorDocument 503 ${path === '/' ? '' : path}/500.html ErrorDocument 504 ${path === '/' ? '' : path}/500.html </VirtualHost> `); // ERR too many redirects: // Check: SELECT * FROM database.wp_options where option_name = 'siteurl' or option_name = 'home'; // Check: wp-config.php // if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { // $_SERVER['HTTPS'] = 'on'; // } // For plugins: // define( 'FS_METHOD', 'direct' ); // ErrorDocument 404 /custom_404.html // ErrorDocument 500 /custom_50x.html // ErrorDocument 502 /custom_50x.html // ErrorDocument 503 /custom_50x.html // ErrorDocument 504 /custom_50x.html // Respond When Error Pages are Directly Requested // <Files "custom_404.html"> // <If "-z %{ENV:REDIRECT_STATUS}"> // RedirectMatch 404 ^/custom_404.html$ // </If> // </Files> // <Files "custom_50x.html"> // <If "-z %{ENV:REDIRECT_STATUS}"> // RedirectMatch 404 ^/custom_50x.html$ // </If> // </Files> // Add www or https with htaccess rewrite // Options +FollowSymLinks // RewriteEngine On // RewriteCond %{HTTP_HOST} ^ejemplo.com [NC] // RewriteRule ^(.*)$ http://ejemplo.com/$1 [R=301,L] // Redirect http to https with htaccess rewrite // RewriteEngine On // RewriteCond %{SERVER_PORT} 80 // RewriteRule ^(.*)$ https://www.ejemplo.com/$1 [R,L] // Redirect to HTTPS with www subdomain // RewriteEngine On // RewriteCond %{HTTPS} off [OR] // RewriteCond %{HTTP_HOST} ^www\. [NC] // RewriteCond %{HTTP_HOST} ^(?:www\.)?(.+)$ [NC] // RewriteRule ^ https://%1%{REQUEST_URI} [L,NE,R=301] await UnderpostStartUp.API.listenPortController( UnderpostStartUp.API.listenServerFactory(), port, runningData, ); break; case 'xampp': if (!Xampp.enabled()) continue; if (!Xampp.ports.includes(port)) Xampp.ports.push(port); if (currentPort === initPort) Xampp.removeRouter(); Xampp.appendRouter(` Listen ${port} <VirtualHost *:${port}> DocumentRoot "${directory ? directory : `${getRootDirectory()}${rootHostPath}`}" ServerName ${host}:${port} <Directory "${directory ? directory : `${getRootDirectory()}${rootHostPath}`}"> Options Indexes FollowSymLinks MultiViews AllowOverride All Require all granted </Directory> ${ redirect ? ` RewriteEngine on RewriteCond %{REQUEST_URI} !^/.well-known/acme-challenge RewriteRule ^(.*)$ ${redirectTarget}%{REQUEST_URI} [R=302,L] ` : '' } ErrorDocument 400 ${path === '/' ? '' : path}/400.html ErrorDocument 404 ${path === '/' ? '' : path}/400.html ErrorDocument 500 ${path === '/' ? '' : path}/500.html ErrorDocument 502 ${path === '/' ? '' : path}/500.html ErrorDocument 503 ${path === '/' ? '' : path}/500.html ErrorDocument 504 ${path === '/' ? '' : path}/500.html </VirtualHost> `); // ERR too many redirects: // Check: SELECT * FROM database.wp_options where option_name = 'siteurl' or option_name = 'home'; // Check: wp-config.php // if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { // $_SERVER['HTTPS'] = 'on'; // } await UnderpostStartUp.API.listenPortController( UnderpostStartUp.API.listenServerFactory(), port, runningData, ); break; case 'nodejs': const app = express(); app.use((req, res, next) => { // const info = `${req.headers.host}${req.url}`; return next(); }); app.use((req, res, next) => { requestCounter.inc({ instance: `${host}:${port}${path}`, method: req.method, status_code: res.statusCode, }); // decodeURIComponent(req.url) return next(); }); app.get(`${path === '/' ? '' : path}/metrics`, async (req, res) => { res.set('Content-Type', promClient.register.contentType); return res.end(await promClient.register.metrics()); }); // set logger app.use(loggerMiddleware(import.meta)); // instance public static app.use('/', express.static(directory ? directory : `.${rootHostPath}`)); // js src compression app.use(compression({ filter: shouldCompress })); function shouldCompress(req, res) { if (req.headers['x-no-compression']) { // don't compress responses with this request header return false; } // fallback to standard filter function return compression.filter(req, res); } if (process.argv.includes('static')) { logger.info('Build static server runtime', `${host}${path}`); currentPort += 2; const staticPort = newInstance(currentPort); await UnderpostStartUp.API.listenPortController(app, staticPort, runningData); currentPort++; continue; } logger.info('Build api server runtime', `${host}${path}`); // parse requests of content-type - application/json app.use(express.json({ limit: '100MB' })); // parse requests of content-type - application/x-www-form-urlencoded app.use(express.urlencoded({ extended: true, limit: '20MB' })); // file upload middleware app.use(fileUpload()); // json formatted response if (process.env.NODE_ENV === 'development') app.set('json spaces', 2); // lang handling middleware app.use(function (req, res, next) { const lang = req.headers['accept-language'] || 'en'; if (typeof lang === 'string' && lang.toLowerCase().match('es')) { req.lang = 'es'; } else req.lang = 'en'; return next(); }); // cors const originPayload = { origin: origins.concat( apis && process.env.NODE_ENV === 'development' ? [`http://localhost:${currentPort + 2}`] : [], ), }; // logger.info('originPayload', originPayload); app.use(cors(originPayload)); if (redirect) { app.use(function (req = express.Request, res = express.Response, next = express.NextFunction) { if (process.env.NODE_ENV === 'production' && !req.url.startsWith(`/.well-known/acme-challenge`)) return res.status(302).redirect(redirectTarget + req.url); // if (!req.url.startsWith(`/.well-known/acme-challenge`)) return res.status(302).redirect(redirect); return next(); }); // app.use( // '*', // createProxyMiddleware({ // target: redirect, // changeOrigin: true, // }), // ); await UnderpostStartUp.API.listenPortController(app, port, runningData); break; } const swaggerJsonPath = `./public/${host}${path === '/' ? path : `${path}/`}swagger-output.json`; if (fs.existsSync(swaggerJsonPath)) { // logger.info('Build swagger serve', swaggerJsonPath); const swaggerInstance = (swaggerDoc) => (...args) => swaggerUi.setup(swaggerDoc)(...args); const swaggerDoc = JSON.parse(fs.readFileSync(swaggerJsonPath, 'utf8')); app.use(`${path === '/' ? `/api-docs` : `${path}/api-docs`}`, swaggerUi.serve, swaggerInstance(swaggerDoc)); } if (db && apis) await DataBaseProvider.load({ apis, host, path, db }); // valkey server if (valkey) await createValkeyConnection({ host, path }, valkey); if (mailer) { const mailerSsrConf = confSSR[getCapVariableName(client)]; await MailerProvider.load({ id: `${host}${path}`, meta: `mailer-${host}${path}`, host, path, ...mailer, templates: mailerSsrConf ? mailerSsrConf.mailer : {}, }); } if (apis) { const apiPath = `${path === '/' ? '' : path}/${process.env.BASE_API}`; for (const api of apis) await (async () => { const { ApiRouter } = await import(`../api/${api}/${api}.router.js`); const router = ApiRouter({ host, path, apiPath, mailer, db }); // router.use(cors({ origin: origins })); // logger.info('Load api router', { host, path: apiPath, api }); app.use(`${apiPath}/${api}`, router); })(); } const Render = await ssrFactory(); const ssrPath = path === '/' ? path : `${path}/`; const defaultHtmlSrc404 = Render({ title: '404 Not Found', ssrPath, ssrHeadComponents: '', ssrBodyComponents: (await ssrFactory(`./src/client/ssr/body/404.js`))(), renderPayload: { apiBasePath: process.env.BASE_API, version: Underpost.version, }, renderApi: { JSONweb, }, }); const path404 = `${directory ? directory : `${getRootDirectory()}${rootHostPath}`}/404/index.html`; const page404 = fs.existsSync(path404) ? `${path === '/' ? '' : path}/404` : undefined; app.use(function (req, res, next) { if (page404) return res.status(404).redirect(page404); else { res.set('Content-Type', 'text/html'); return res.status(404).send(defaultHtmlSrc404); } }); const defaultHtmlSrc500 = Render({ title: '500 Server Error', ssrPath, ssrHeadComponents: '', ssrBodyComponents: (await ssrFactory(`./src/client/ssr/body/500.js`))(), renderPayload: { apiBasePath: process.env.BASE_API, version: Underpost.version, }, renderApi: { JSONweb, }, }); const path500 = `${directory ? directory : `${getRootDirectory()}${rootHostPath}`}/500/index.html`; const page500 = fs.existsSync(path500) ? `${path === '/' ? '' : path}/500` : undefined; app.use(function (err, req, res, next) { logger.error(err, err.stack); if (page500) return res.status(500).redirect(page500); else { res.set('Content-Type', 'text/html'); return res.status(500).send(defaultHtmlSrc500); } }); // instance server const server = createServer({}, app); if (ws) await (async () => { const { createIoServer } = await import(`../ws/${ws}/${ws}.ws.server.js`); // logger.info('Load socket.io ws router', { host, ws }); // start socket.io const { options, meta } = await createIoServer(server, { host, path, db, port, origins, }); await UnderpostStartUp.API.listenPortController(UnderpostStartUp.API.listenServerFactory(), port, { runtime: 'nodejs', client: null, host, path: options.path, meta, }); })(); if (peer) { currentPort++; const peerPort = newInstance(currentPort); const { options, meta, peerServer } = await createPeerServer({ port: peerPort, devPort: port, origins, host, path, }); await UnderpostStartUp.API.listenPortController(peerServer, peerPort, { runtime: 'nodejs', client: null, host, path: options.path, meta, }); } await UnderpostStartUp.API.listenPortController(server, port, runningData); break; default: break; } currentPort++; } } if (Xampp.enabled() && Xampp.router) Xampp.initService(); if (Lampp.enabled() && Lampp.router) Lampp.initService(); UnderpostStartUp.API.logRuntimeRouter(); }; export { buildRuntime };