server.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. const express = require('express');
  2. const https = require('https');
  3. const http = require('http');
  4. const tls = require('tls');
  5. const fs = require('fs');
  6. const path = require('path');
  7. const url = require('url');
  8. const { execFile } = require('child_process');
  9. require('dotenv').config({ path: path.join(__dirname, '.env') });
  10. const app = express();
  11. // Parse ALLOWED_DOMAINS environment variable as an array
  12. const allowedDomains = process.env.ALLOWED_DOMAINS ? process.env.ALLOWED_DOMAINS.split(',') : [];
  13. // Middleware to parse JSON request bodies
  14. app.use(express.json());
  15. // Helper function to get SSL options based on hostname
  16. const getSSLOptions = (hostname) => {
  17. const sslPrefix = process.env.SSL_PREFIX || '/etc/letsencrypt/live/';
  18. try {
  19. return {
  20. key: fs.readFileSync(path.join(sslPrefix, hostname, 'privkey.pem')),
  21. cert: fs.readFileSync(path.join(sslPrefix, hostname, 'fullchain.pem')),
  22. };
  23. } catch (error) {
  24. // Fallback to default certificates if domain-specific ones aren't found
  25. console.warn(`Could not load SSL certificates for ${hostname}, using defaults`);
  26. return {
  27. key: fs.readFileSync(process.env.SSL_KEY_FILE),
  28. cert: fs.readFileSync(process.env.SSL_CRT_FILE),
  29. };
  30. }
  31. };
  32. // Redirect HTTP to HTTPS
  33. app.use((req, res, next) => {
  34. if (!req.secure && req.headers['x-forwarded-proto'] !== 'https') {
  35. return res.redirect(`https://${req.headers.host}${req.url}`);
  36. }
  37. next();
  38. });
  39. // Handle API requests without using wildcard routes
  40. app.use((req, res, next) => {
  41. // Check if the path starts with '/api/v1/'
  42. if (req.path.startsWith('/api/v1/')) {
  43. // Extract the part after '/api/v1/'
  44. const apiPath = req.path.substring('/api/v1/'.length);
  45. if (!apiPath) {
  46. return res.status(400).json({ error: 'Bad Request: No dynamic route found' });
  47. }
  48. const parsedUrl = url.parse(req.url);
  49. const requestOptions = {
  50. hostname: 'localhost',
  51. port: 3000,
  52. path: `/api/v1/${apiPath}${parsedUrl.search || ''}`,
  53. method: req.method,
  54. headers: { ...req.headers },
  55. };
  56. let jsonPayload = null;
  57. if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
  58. jsonPayload = JSON.stringify(req.body);
  59. requestOptions.headers['Content-Type'] = 'application/json';
  60. requestOptions.headers['Content-Length'] = Buffer.byteLength(jsonPayload);
  61. }
  62. const proxyReq = http.request(requestOptions, (proxyRes) => {
  63. res.writeHead(proxyRes.statusCode, proxyRes.headers);
  64. proxyRes.pipe(res);
  65. });
  66. proxyReq.on('error', (error) => {
  67. console.error(`Error forwarding request: ${error.message}`);
  68. res.status(500).json({ error: 'Internal Server Error' });
  69. });
  70. if (jsonPayload) {
  71. proxyReq.write(jsonPayload);
  72. }
  73. proxyReq.end();
  74. return; // Stop further middleware execution
  75. }
  76. // If not an API request, continue to next middleware
  77. next();
  78. });
  79. // Function for direct PHP execution
  80. const executePhpDirectly = (phpFilePath, domainPath, req, res) => {
  81. // Create environment variables that PHP might expect
  82. const env = {
  83. ...process.env,
  84. DOCUMENT_ROOT: domainPath,
  85. SCRIPT_FILENAME: phpFilePath,
  86. REQUEST_URI: req.url,
  87. QUERY_STRING: url.parse(req.url).query || '',
  88. REQUEST_METHOD: req.method,
  89. HTTP_HOST: req.headers.host,
  90. REMOTE_ADDR: req.ip,
  91. SERVER_NAME: req.headers.host.split(':')[0],
  92. // Additional variables that PHP might expect
  93. GATEWAY_INTERFACE: 'CGI/1.1',
  94. SERVER_PROTOCOL: 'HTTP/1.1',
  95. // Pass HTTP headers as environment variables
  96. ...Object.entries(req.headers).reduce((env, [key, value]) => {
  97. env[`HTTP_${key.toUpperCase().replace(/-/g, '_')}`] = value;
  98. return env;
  99. }, {})
  100. };
  101. execFile('php', [phpFilePath], { env }, (error, stdout, stderr) => {
  102. if (error) {
  103. console.error(`PHP execution error: ${error.message}`);
  104. return res.status(500).send(`PHP Execution Error: ${error.message}`);
  105. }
  106. if (stderr) {
  107. console.error(`PHP stderr output: ${stderr}`);
  108. }
  109. // Set Content-Type to HTML for proper rendering
  110. res.setHeader('Content-Type', 'text/html; charset=utf-8');
  111. res.send(stdout);
  112. });
  113. };
  114. // Helper function to check for index files
  115. const findIndexFile = (directoryPath) => {
  116. // Common index file types to check for
  117. const indexFiles = ['index.html', 'index.htm', 'index.php'];
  118. for (const indexFile of indexFiles) {
  119. const filePath = path.join(directoryPath, indexFile);
  120. if (fs.existsSync(filePath)) {
  121. return { path: filePath, type: indexFile.endsWith('.php') ? 'php' : 'html' };
  122. }
  123. }
  124. return null;
  125. };
  126. // Handle PHP for all domains
  127. app.use((req, res, next) => {
  128. const host = req.headers.host.split(':')[0]; // Remove port if present
  129. const domainPath = path.join('/var/www', host);
  130. // Make sure the domain directory exists
  131. if (!fs.existsSync(domainPath)) {
  132. return next();
  133. }
  134. // Handle direct PHP file requests
  135. if (req.path.endsWith('.php')) {
  136. const phpFilePath = path.join(domainPath, req.path);
  137. if (fs.existsSync(phpFilePath)) {
  138. return executePhpDirectly(phpFilePath, domainPath, req, res);
  139. }
  140. }
  141. // Handle directory requests - check for index files
  142. if (req.path === '/' || req.path.endsWith('/')) {
  143. const indexResult = findIndexFile(path.join(domainPath, req.path));
  144. if (indexResult) {
  145. if (indexResult.type === 'php') {
  146. return executePhpDirectly(indexResult.path, domainPath, req, res);
  147. } else {
  148. return res.sendFile(indexResult.path);
  149. }
  150. }
  151. }
  152. next();
  153. });
  154. // Serve static files from domain root first for all domains
  155. app.use((req, res, next) => {
  156. const host = req.headers.host.split(':')[0];
  157. const domainPath = path.join('/var/www', host);
  158. if (!fs.existsSync(domainPath)) {
  159. return next();
  160. }
  161. express.static(domainPath)(req, res, (err) => {
  162. next();
  163. });
  164. });
  165. // Serve static files from build directory as fallback
  166. app.use((req, res, next) => {
  167. const host = req.headers.host.split(':')[0];
  168. const domainPath = path.join('/var/www', host);
  169. const buildPath = path.join(domainPath, 'build');
  170. if (fs.existsSync(buildPath)) {
  171. express.static(buildPath)(req, res, next);
  172. } else {
  173. next();
  174. }
  175. });
  176. // Fall back to index.html without using catch-all route
  177. app.use((req, res) => {
  178. const host = req.headers.host.split(':')[0];
  179. const domainPath = path.join('/var/www', host);
  180. // Check for index.html in domain root
  181. const rootIndexPath = path.join(domainPath, 'index.html');
  182. if (fs.existsSync(rootIndexPath)) {
  183. return res.sendFile(rootIndexPath);
  184. }
  185. // Check build folder
  186. const buildPath = path.join(domainPath, 'build');
  187. if (fs.existsSync(buildPath)) {
  188. const buildIndexPath = path.join(buildPath, 'index.html');
  189. if (fs.existsSync(buildIndexPath)) {
  190. return res.sendFile(buildIndexPath);
  191. }
  192. }
  193. // No matching file found
  194. res.status(404).send('Not Found');
  195. });
  196. // Create HTTPS server with SNI support for multiple domains
  197. const server = https.createServer({
  198. SNICallback: (hostname, cb) => {
  199. const secureContext = tls.createSecureContext(getSSLOptions(hostname));
  200. if (cb) {
  201. cb(null, secureContext);
  202. } else {
  203. return secureContext;
  204. }
  205. }
  206. }, app);
  207. server.listen(443, '0.0.0.0', () => {
  208. console.log(`Server running on https://0.0.0.0:443`);
  209. });
  210. // Also create a HTTP server that redirects to HTTPS
  211. http.createServer((req, res) => {
  212. res.writeHead(301, { Location: `https://${req.headers.host}${req.url}` });
  213. res.end();
  214. }).listen(80, '0.0.0.0', () => {
  215. console.log('HTTP to HTTPS redirect server running on port 80');
  216. });