@underpostnet/underpost
Version:
Underpost Platform — end-to-end CI/CD and application-delivery toolchain CLI. Covers bare metal, Kubernetes, K3s, kubeadm, LXD, container/image orchestration, secrets, databases, cron jobs, monitoring, SSH, runners, PWA + Workbox delivery, and release orc
360 lines (308 loc) • 12.7 kB
JavaScript
/**
* Exported singleton instance of the LamppService class.
* This object is used to interact with the Lampp configuration and service.
* @module src/runtime/lampp/Lampp.js
* @namespace LamppService
*/
import fs from 'fs-extra';
import { getRootDirectory, shellCd, shellExec } from '../../server/process.js';
import { loggerFactory } from '../../server/logger.js';
const logger = loggerFactory(import.meta);
/**
* @class LamppService
* @description Provides utilities for managing the XAMPP (Lampp) service on Linux,
* including initialization, router configuration, and virtual host creation.
* It manages the server's configuration files and controls the service process.
* @memberof LamppService
*/
class LamppService {
/**
* @method
* @type {string}
* @description Stores the accumulated Apache virtual host configuration (router definition).
* @memberof LamppService
*/
router = '';
/**
* @public
* @type {number[]}
* @description A list of ports currently configured and listened to by the Lampp service.
* @memberof LamppService
*/
ports;
/**
* Creates an instance of LamppService.
* Initializes the router configuration and ports list.
* @method constructor
* @memberof LamppService
*/
constructor() {
this.router = '';
this.ports = [];
}
/**
* Checks if the XAMPP (Lampp) service appears to be installed based on the presence of its main configuration file.
*
* @method enabled
* @memberof LamppService
* @returns {boolean} True if the configuration file exists, indicating Lampp is likely installed.
* @throws {Error} If the configuration file does not exist.
* @memberof LamppService
*/
enabled() {
return fs.existsSync('/opt/lampp/etc/httpd.conf');
}
/**
* Initializes or restarts the Lampp Apache service.
* This method configures virtual hosts, disables default ports (80/443) in the main config
* to avoid conflicts, and starts or stops the service using shell commands.
*
* @method initService
* @param {object} [options={daemon: false}] - Options for service initialization.
* @param {boolean} [options.daemon=false] - Flag to indicate if the service should be run as a daemon (currently unused in logic).
* @returns {Promise<void>}
* @memberof LamppService
*/
async initService(options = { daemon: false }) {
let cmd;
// 1. Write the current virtual host router configuration
fs.writeFileSync(`/opt/lampp/etc/extra/httpd-vhosts.conf`, this.router || '', 'utf8');
// 2. Ensure the vhosts file is included in the main httpd.conf
const httpdConfPath = `/opt/lampp/etc/httpd.conf`;
fs.writeFileSync(
httpdConfPath,
fs
.readFileSync(httpdConfPath, 'utf8')
.replace(`#Include etc/extra/httpd-vhosts.conf`, `Include etc/extra/httpd-vhosts.conf`),
'utf8',
);
// 3. Stop the service before making port changes
cmd = `sudo /opt/lampp/lampp stop`;
shellExec(cmd);
// 4. Comment out default port Listen directives (80 and 443) to prevent conflicts
// Modify httpd.conf (port 80)
if (!fs.readFileSync(httpdConfPath, 'utf8').match(/# Listen 80/))
fs.writeFileSync(
httpdConfPath,
fs.readFileSync(httpdConfPath, 'utf8').replace(`Listen 80`, `# Listen 80`),
'utf8',
);
// Modify httpd-ssl.conf (port 443)
const httpdSslConfPath = `/opt/lampp/etc/extra/httpd-ssl.conf`;
if (fs.existsSync(httpdSslConfPath) && !fs.readFileSync(httpdSslConfPath, 'utf8').match(/# Listen 443/))
fs.writeFileSync(
httpdSslConfPath,
fs.readFileSync(httpdSslConfPath, 'utf8').replace(`Listen 443`, `# Listen 443`),
'utf8',
);
// 5. Modify the lampp startup script to bypass port checking for 80 and 443
const lamppScriptPath = `/opt/lampp/lampp`;
if (!fs.readFileSync(lamppScriptPath, 'utf8').match(/testport 443 && false/))
fs.writeFileSync(
lamppScriptPath,
fs.readFileSync(lamppScriptPath, 'utf8').replace(`testport 443`, `testport 443 && false`),
'utf8',
);
if (!fs.readFileSync(lamppScriptPath, 'utf8').match(/testport 80 && false/))
fs.writeFileSync(
lamppScriptPath,
fs.readFileSync(lamppScriptPath, 'utf8').replace(`testport 80`, `testport 80 && false`),
'utf8',
);
// 6. Start the service
cmd = `sudo /opt/lampp/lampp start`;
shellExec(cmd);
}
/**
* Appends new Apache VirtualHost configuration content to the internal router string.
* If a router config file exists from a previous run, it loads it first.
*
* @method appendRouter
* @param {string} render - The new VirtualHost configuration string to append.
* @returns {string} The complete, updated router configuration string.
* @memberof LamppService
*/
appendRouter(render) {
if (!this.router) return (this.router = render);
return (this.router += render);
}
/**
* Resets the internal router configuration and removes the temporary configuration file.
*
* @memberof LamppService
* @returns {void}
* @method removeRouter
*/
removeRouter() {
this.router = '';
}
/**
* Installs and configures the Lampp service on Linux.
* This includes downloading the installer, running it, and setting up initial configurations.
* Only runs on the 'linux' platform.
*
* @method install
* @returns {Promise<void>}
* @memberof LamppService
*/
async install() {
if (process.platform === 'linux') {
if (!fs.existsSync(`./engine-private/setup`)) fs.mkdirSync(`./engine-private/setup`, { recursive: true });
shellCd(`./engine-private/setup`);
if (!process.argv.includes(`server`)) {
// Download and run the XAMPP installer (PHP 8.2)
shellExec(
`curl -Lo xampp-linux-installer.run https://sourceforge.net/projects/xampp/files/XAMPP%20Linux/8.2.12/xampp-linux-x64-8.2.12-0-installer.run`,
);
shellExec(`sudo chmod +x xampp-linux-installer.run`);
shellExec(
`sudo ./xampp-linux-installer.run --mode unattended && \\` +
// Create symlink for easier access
`ln -sf /opt/lampp/lampp /usr/bin/lampp && \\` +
// Allow all access to xampp config (security measure override)
`sed -i.bak s'/Require local/Require all granted/g' /opt/lampp/etc/extra/httpd-xampp.conf && \\` +
// Enable display errors in PHP
`sed -i.bak s'/display_errors=Off/display_errors=On/g' /opt/lampp/etc/php.ini && \\` +
// Allow including custom Apache configuration files
`mkdir /opt/lampp/apache2/conf.d && \\` +
`echo "IncludeOptional /opt/lampp/apache2/conf.d/*.conf" >> /opt/lampp/etc/httpd.conf && \\` +
// Create /www directory and symlink it to htdocs
`mkdir /www && \\` +
`ln -s /www /opt/lampp/htdocs`,
);
}
// Copy log files to standard names for easier consumption
if (fs.existsSync(`/opt/lampp/logs/access_log`))
fs.copySync(`/opt/lampp/logs/access_log`, `/opt/lampp/logs/access.log`);
if (fs.existsSync(`/opt/lampp/logs/error_log`))
fs.copySync(`/opt/lampp/logs/error_log`, `/opt/lampp/logs/error.log`);
if (fs.existsSync(`/opt/lampp/logs/php_error_log`))
fs.copySync(`/opt/lampp/logs/php_error_log`, `/opt/lampp/logs/php_error.log`);
if (fs.existsSync(`/opt/lampp/logs/ssl_request_log`))
fs.copySync(`/opt/lampp/logs/ssl_request_log`, `/opt/lampp/logs/ssl_request.log`);
// Initialize the service after installation
await this.initService({ daemon: true });
}
}
/**
* Creates and appends a new Apache VirtualHost entry to the router configuration for a web application.
* The router is then applied by calling {@link LamppService#initService}.
*
* @method createApp
* @param {object} options - Configuration options for the new web application.
* @param {number} options.port - The port the VirtualHost should listen on.
* @param {string} options.host - The ServerName/host for the VirtualHost.
* @param {string} options.path - The base path for error documents (e.g., '/app').
* @param {string} [options.directory] - Optional absolute path to the document root.
* @param {string} [options.rootHostPath] - Relative path from the root directory to the document root, used if `directory` is not provided.
* @param {boolean} [options.redirect] - If true, enables RewriteEngine for redirection.
* @param {string} [options.redirectTarget] - The target URL for redirection.
* @param {boolean} [options.resetRouter] - If true, clears the existing router configuration before appending the new one.
* @returns {{disabled: boolean}} An object indicating if the service is disabled.
* @memberof LamppService
*/
createApp({ port, host, path, directory, rootHostPath, redirect, redirectTarget, resetRouter }) {
if (!this.enabled()) return { disabled: true };
if (!this.ports.includes(port)) this.ports.push(port);
if (resetRouter) this.removeRouter();
const documentRoot = directory ? directory : `${getRootDirectory()}${rootHostPath}`;
// Append the new VirtualHost configuration
this.appendRouter(`
Listen ${port}
<VirtualHost *:${port}>
DocumentRoot "${documentRoot}"
ServerName ${host}
UseCanonicalName Off
ServerSignature Off
<Directory "${documentRoot}">
Options FollowSymLinks MultiViews
AllowOverride All
Require all granted
</Directory>
# Block access to .git directories and files
RedirectMatch 404 /\\.git
# Block access to hidden dotfiles (.env, .htpasswd, etc.)
<FilesMatch "^\\.(env|htpasswd|htaccess\\.bak|DS_Store)">
Require all denied
</FilesMatch>
# Block access to sensitive engine files
<FilesMatch "(wp-config\\.php\\.bak|wp-config-sample\\.php|\\.sql|\\.sql\\.gz)$">
Require all denied
</FilesMatch>
# Security headers
<IfModule mod_headers.c>
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always unset X-Powered-By
</IfModule>
${
redirect
? `
RewriteEngine on
# Exclude the ACME challenge path for certificate renewals
RewriteCond %{REQUEST_URI} !^/.well-known/acme-challenge
RewriteRule ^(.*)$ ${redirectTarget}%{REQUEST_URI} [R=302,L]
`
: ''
}
# Custom Error Documents
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>
`);
return { disabled: false };
}
}
/**
* @description Exported singleton instance of the LamppService class.
* This object is used to interact with the Lampp configuration and service.
* @memberof LamppService
* @type {LamppService}
*/
const Lampp = new LamppService();
export { Lampp };
// -- helper info --
// 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} ^example.com [NC]
// RewriteRule ^(.*)$ http://example.com/$1 [R=301,L]
// Redirect http to https with htaccess rewrite
// RewriteEngine On
// RewriteCond %{SERVER_PORT} 80
// RewriteRule ^(.*)$ https://www.example.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]