chore: fixes
Some checks are pending
📝 Update Documentation / update-docs (push) Waiting to run

This commit is contained in:
MattTheTekie 2026-04-18 15:53:44 -04:00
commit eb954e7470
307 changed files with 24443 additions and 14699 deletions

257
services/app.js Normal file
View file

@ -0,0 +1,257 @@
/**
* Creates Express app, for all the server-side routes + middleware
* Which gets imported by the server.js in the root
* */
/* Import built-in Node server modules */
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
/* Project root — one level up from services/ */
const rootDir = path.join(__dirname, '..');
/* Import NPM dependencies */
const yaml = require('js-yaml');
/* Import Express + middleware functions */
const express = require('express');
const basicAuth = require('express-basic-auth');
const history = require('connect-history-api-fallback');
/* Kick of some basic checks */
require('./update-checker'); // Checks if there are any updates available, prints message
let config = require('./config-validator'); // Validate config file and load result
/* Include route handlers for API endpoints */
const statusCheck = require('./status-check'); // Used by the status check feature, uses GET
const saveConfig = require('./save-config'); // Saves users new conf.yml to file-system
const rebuild = require('./rebuild-app'); // A script to programmatically trigger a build
const systemInfo = require('./system-info'); // Basic system info, for resource widget
const sslServer = require('./ssl-server'); // TLS-enabled web server
const corsProxy = require('./cors-proxy'); // Enables API requests to CORS-blocked services
const getUser = require('./get-user'); // Enables server side user lookup
/* Helper functions, and default config */
const ENDPOINTS = require('../src/utils/defaults').serviceEndpoints; // API endpoint URL paths
/* Indicates for the webpack config, that running as a server */
process.env.IS_SERVER = 'True';
/* Just console.warns an error */
const printWarning = (msg, error) => {
console.warn(`\x1b[103m\x1b[34m${msg}\x1b[0m\n`, error || ''); // eslint-disable-line no-console
};
/* Send a response body if the stream is already closed, with optional status */
const safeEnd = (res, body, status) => {
if (res.headersSent) return;
try {
if (status) res.status(status);
res.end(body);
} catch (e) { /* response stream gone */ }
};
/* Build a serialized JSON error body */
const errBody = (e) => JSON.stringify({
success: false,
message: String(e && e.message ? e.message : e),
});
/* Catch any possible unhandled error. Shouldn't ever happen! */
process.on('unhandledRejection', (reason) => {
printWarning('Unhandled promise rejection in server', reason);
});
/* Load appConfig.auth from config (if present) for authorization purposes */
function loadAuthConfig() {
try {
const filePath = path.join(rootDir, process.env.USER_DATA_DIR || 'user-data', 'conf.yml');
const fileContents = fs.readFileSync(filePath, 'utf8');
const data = yaml.load(fileContents);
return data?.appConfig?.auth || {};
} catch (e) {
return {};
}
}
function loadUserConfig() {
return loadAuthConfig().users || null;
}
/* Authorizer for ENABLE_HTTP_AUTH: validates credentials against conf.yml users */
function customAuthorizer(username, password) {
const sha256 = (input) => crypto.createHash('sha256').update(input).digest('hex').toUpperCase();
const generateUserToken = (user) => {
if (!user.user || (!user.hash && !user.password)) return '';
const strAndUpper = (input) => input.toString().toUpperCase();
const passwordHash = user.hash || sha256(process.env[user.password]);
const sha = sha256(strAndUpper(user.user) + strAndUpper(passwordHash));
return strAndUpper(sha);
};
if (password.startsWith('Bearer ')) {
const token = password.slice('Bearer '.length);
const users = loadUserConfig();
return users.some(user => (
user.user.toLowerCase() === username.toLowerCase() && generateUserToken(user) === token
));
} else {
const users = loadUserConfig();
const userHash = sha256(password);
return users.some(user => (
user.user.toLowerCase() === username.toLowerCase() && user.hash.toUpperCase() === userHash
));
}
}
/* If auth is enabled, setup auth for config access, otherwise skip */
function getBasicAuthMiddleware() {
const authConfig = loadAuthConfig();
const confUsers = authConfig.users || null;
const hasConfUsers = confUsers && confUsers.length > 0;
const useConfAuth = process.env.ENABLE_HTTP_AUTH && hasConfUsers;
const { BASIC_AUTH_USERNAME, BASIC_AUTH_PASSWORD } = process.env;
const hasStaticCreds = BASIC_AUTH_USERNAME && BASIC_AUTH_PASSWORD;
// Warn if both auth methods are configured - they don't work together
if (hasStaticCreds && hasConfUsers) {
printWarning(useConfAuth
? 'BASIC_AUTH env vars are ignored because ENABLE_HTTP_AUTH is active with conf.yml users.'
: 'BASIC_AUTH env vars and appConfig.auth.users are both set but use different credentials.'
+ ' This will cause auth failures. Set ENABLE_HTTP_AUTH=true, or remove users from conf.yml.');
}
if (useConfAuth) {
return basicAuth({
authorizer: customAuthorizer,
challenge: true,
unauthorizedResponse: () => 'Unauthorized - Incorrect token',
});
} else if (hasStaticCreds) {
return basicAuth({
users: { [BASIC_AUTH_USERNAME]: BASIC_AUTH_PASSWORD },
challenge: true,
unauthorizedResponse: () => 'Unauthorized - Incorrect username or password',
});
} else if (authConfig.enableHeaderAuth && authConfig.headerAuth) {
const { userHeader = 'Remote-User', proxyWhitelist = [] } = authConfig.headerAuth;
return (req, res, next) => {
if (!proxyWhitelist.includes(req.socket.remoteAddress)) {
return res.status(401).json({ success: false, message: 'Unauthorized - not from trusted proxy' });
}
const user = req.headers[userHeader.toLowerCase()];
if (!user) {
return res.status(401).json({ success: false, message: 'Unauthorized - missing user header' });
}
req.auth = { user };
return next();
};
}
return (req, res, next) => next();
}
const protectConfig = getBasicAuthMiddleware();
/* Middleware to restrict write endpoints to admin users only */
function requireAdmin(req, res, next) {
if (!req.auth) return next();
const users = loadUserConfig();
if (!users || users.length === 0) return next();
const user = users.find(u => u.user.toLowerCase() === req.auth.user.toLowerCase());
if (user && user.type === 'admin') return next();
return res.status(403).json({ success: false, message: 'Forbidden - Admin access required' });
}
/* A middleware function for Connect, that filters requests based on method type */
const method = (m, mw) => (req, res, next) => (req.method === m ? mw(req, res, next) : next());
const app = express()
// Load SSL redirection middleware
.use(sslServer.middleware)
// Load middlewares for parsing JSON, and supporting HTML5 history routing
.use(express.json({ limit: '1mb' }))
// GET endpoint to run status of a given URL with GET request
.use(ENDPOINTS.statusCheck, protectConfig, method('GET', (req, res) => {
try {
statusCheck(req.url, (results) => {
if (!res.headersSent) {
res.setHeader('Content-Type', 'application/json');
res.end(results);
}
});
} catch (e) {
printWarning(`Error running status check for ${req.url}\n`, e);
if (!res.headersSent) {
res.status(500).end(JSON.stringify({ successStatus: false, message: '❌ Status check failed badly' }));
}
}
}))
// POST Endpoint used to save config, by writing config file to disk
.use(ENDPOINTS.save, protectConfig, requireAdmin, method('POST', (req, res) => {
let responded = false;
const respond = (jsonBody) => {
if (responded || res.headersSent) return;
responded = true;
try { // Only update in-memory config when disk write succeeds
if (JSON.parse(jsonBody).success === true) config = req.body.config;
} catch (e) { /* unparseable body, config is unchanged */ }
try { res.end(jsonBody); } catch (e) { /* response stream gone */ }
};
saveConfig(req.body, respond).catch((e) => {
printWarning('Error writing config file to disk', e);
respond(JSON.stringify({ success: false, message: String(e) }));
});
}))
// GET endpoint to trigger a build, and respond with success status and output
.use(ENDPOINTS.rebuild, protectConfig, requireAdmin, method('GET', (req, res) => {
rebuild()
.then((response) => safeEnd(res, JSON.stringify(response)))
.catch((e) => safeEnd(res, errBody(e)));
}))
// GET endpoint to return system info, for widget
.use(ENDPOINTS.systemInfo, protectConfig, method('GET', (req, res) => {
try {
safeEnd(res, JSON.stringify(systemInfo()));
} catch (e) {
safeEnd(res, errBody(e));
}
}))
// GET for accessing non-CORS API services
.use(ENDPOINTS.corsProxy, protectConfig, (req, res) => {
try {
corsProxy(req, res);
} catch (e) {
safeEnd(res, errBody(e));
}
})
// GET endpoint to return user info
.use(ENDPOINTS.getUser, protectConfig, method('GET', (req, res) => {
try {
safeEnd(res, JSON.stringify(getUser(config, req)));
} catch (e) {
safeEnd(res, errBody(e));
}
}))
// Middleware to serve any .yml files in USER_DATA_DIR with optional protection
.get('/*.yml', protectConfig, (req, res) => {
const ymlFile = req.path.split('/').pop();
const filePath = path.join(rootDir, process.env.USER_DATA_DIR || 'user-data', ymlFile);
res.sendFile(filePath, (err) => {
if (err) safeEnd(res, errBody(`Could not read ${ymlFile}`), 404);
});
})
// Serves up static files
.use(express.static(path.join(rootDir, process.env.USER_DATA_DIR || 'user-data')))
.use(express.static(path.join(rootDir, 'dist')))
.use(express.static(path.join(rootDir, 'public'), { index: 'initialization.html' }))
.use(history())
// If no other route is matched, serve up the index.html with a 404 status
.use((req, res) => {
res.status(404).sendFile(path.join(rootDir, 'dist', 'index.html'), (err) => {
if (err) safeEnd(res, errBody('Not Found'));
});
});
module.exports = app;

View file

@ -11,7 +11,7 @@ const schema = require('../src/utils/ConfigSchema.json');
/* Tell AJV to use strict mode, and report all errors */
const validatorOptions = {
strict: true,
strict: false,
allowUnionTypes: true,
allErrors: true,
};
@ -98,11 +98,14 @@ const printFileReadError = (e) => {
}
};
let config = {};
try { // Try to open and parse the YAML file
const config = yaml.load(fs.readFileSync('./public/conf.yml', 'utf8'));
config = yaml.load(fs.readFileSync(`./${process.env.USER_DATA_DIR || 'user-data'}/conf.yml`, 'utf8'));
validate(config);
} catch (e) { // Something went very wrong...
setIsValidVariable(false);
logToConsole(bigError());
printFileReadError(e);
}
module.exports = config;

View file

@ -4,7 +4,45 @@
* makes request to endpoint, then responds to the frontend with the response
*/
const axios = require('axios');
const request = require('./request');
// List of hosts to disallow by default, for cloud instances
// Covers AWS, Azure, GCP, DO, Hetzner, Oracle, etc on IPv4/6
const BLOCKED_HOSTS = new Set([
'169.254.169.254',
'::ffff:a9fe:a9fe',
'fd00:ec2::254',
'metadata.google.internal',
'100.100.100.200',
]);
// Operator escape hatch, set this env var to bypass all proxy restrictions
const restrictionsDisabled = !!process.env.DANGEROUSLY_DISABLE_PROXY_RESTRICTIONS;
// Validate the target URL against scheme and host policies
// Returns { ok: true } on success, or { ok: false, status, error } on rejection
const validateTargetUrl = (raw) => {
if (restrictionsDisabled) return { ok: true };
let url;
try { url = new URL(raw); } catch (e) {
return { ok: false, status: 400, error: 'Target-URL is not a valid URL' };
}
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
return { ok: false, status: 400, error: 'Target-URL must use http:// or https://' };
}
// url.hostname includes brackets for IPv6 (e.g. '[fd00:ec2::254]') - strip em
const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, '');
if (BLOCKED_HOSTS.has(host)) {
return {
ok: false,
status: 403,
error: `Target-URL host '${host}' is blocked by the CORS proxy. `
+ 'This address is reserved for cloud instance metadata services. '
+ 'To bypass, set DANGEROUSLY_DISABLE_PROXY_RESTRICTIONS=true.',
};
}
return { ok: true };
};
module.exports = (req, res) => {
// Apply allow-all response headers
@ -20,28 +58,47 @@ module.exports = (req, res) => {
return;
}
// Get desired URL, from Target-URL header
// Get desired URL, from Target-URL header, and validate it against the policy
const targetURL = req.header('Target-URL');
if (!targetURL) {
res.status(500).send({ error: 'There is no Target-Endpoint header in the request' });
res.status(400).send({ error: 'Missing required Target-URL header' });
return;
}
const validation = validateTargetUrl(targetURL);
if (!validation.ok) {
res.status(validation.status).send({ error: validation.error });
return;
}
// Apply any custom headers, if needed
const headers = req.header('CustomHeaders') ? JSON.parse(req.header('CustomHeaders')) : {};
let headers = {};
const rawCustomHeaders = req.header('CustomHeaders');
if (rawCustomHeaders) {
try {
headers = JSON.parse(rawCustomHeaders);
} catch (e) {
res.status(400).send({ error: 'CustomHeaders header contains malformed JSON' });
return;
}
}
// Prepare the request
const requestConfig = {
method: req.method,
url: targetURL,
json: req.body,
data: req.body,
headers,
timeout: 30000,
maxResponseSize: 10 * 1024 * 1024, // 10 MB
};
// Make the request, and respond with result
axios.request(requestConfig)
.then((response) => {
res.status(200).send(response.data);
}).catch((error) => {
res.status(500).send({ error });
});
const send = (status, body) => {
if (res.headersSent) return;
try { res.status(status).send(body); } catch (e) { /* response stream gone */ }
};
request(requestConfig).then(
(response) => send(200, response.data),
(error) => send(500, { error }),
);
};

15
services/get-user.js Normal file
View file

@ -0,0 +1,15 @@
module.exports = (config, req) => {
try {
if (config.appConfig?.auth?.enableHeaderAuth) {
const { userHeader } = config.appConfig.auth.headerAuth;
const { proxyWhitelist } = config.appConfig.auth.headerAuth;
if (proxyWhitelist.includes(req.socket.remoteAddress)) {
return { success: true, user: req.headers[userHeader.toLowerCase()] };
}
}
return {};
} catch (e) {
console.warn('Error get-user: ', e);
return { success: false };
}
};

View file

@ -4,13 +4,27 @@
* Note that exiting with code 1 indicates failure, and 0 is success
*/
const isSsl = !!process.env.SSL_PRIV_KEY_PATH && !!process.env.SSL_PUB_KEY_PATH;
const fs = require('fs');
/* setting default paths for public and pvt keys to match of ssl-server.js */
const httpsCerts = {
private: process.env.SSL_PRIV_KEY_PATH || '/etc/ssl/certs/dashy-priv.key',
public: process.env.SSL_PUB_KEY_PATH || '/etc/ssl/certs/dashy-pub.pem',
};
/* Check if either if simular conditions exist that would of had ssl-server.js to enable ssl */
const isSsl = !!fs.existsSync(httpsCerts.private) && !!fs.existsSync(httpsCerts.public);
// eslint-disable-next-line import/no-dynamic-require
const http = require(isSsl ? 'https' : 'http');
/* Location of the server to test */
const isDocker = !!process.env.IS_DOCKER;
const port = isSsl ? (process.env.SSL_PORT || (isDocker ? 443 : 4001)) : (process.env.PORT || isDocker ? 80 : 4000);
/* Get the port to use (depending on, if docker, if SSL) */
const sslPort = process.env.SSL_PORT || (isDocker ? 443 : 4001);
const normalPort = process.env.PORT || (isDocker ? 8080 : 4000);
const port = isSsl ? sslPort : normalPort;
const host = process.env.HOST || '0.0.0.0';
const timeout = 2000;
@ -18,7 +32,9 @@ const agent = new http.Agent({
rejectUnauthorized: false, // Allow self-signed certificates
});
const requestOptions = { host, port, timeout, agent };
const requestOptions = {
host, port, timeout, agent,
};
const startTime = new Date(); // Initialize timestamp to calculate time taken

View file

@ -32,7 +32,7 @@ module.exports = (ip, port, isDocker) => {
} else {
// Prepare message for users running app on bare metal
msg = `${chars.GREEN}${line(75)}${chars.BR}`
+ `${chars.CYAN}Welcome to Dashy! 🚀${blanks(55)}${chars.GREEN}${chars.BR}`
+ `${chars.CYAN}Welcome to Dashy! 🚀${blanks(54)}${chars.GREEN}${chars.BR}`
+ `${chars.CYAN}Your new dashboard is now up and running at ${chars.BRIGHT}`
+ `http://${ip}:${port}${chars.RESET}${blanks(18 - ip.length)}${chars.GREEN}${chars.BR}`
+ `${line(75)}${chars.BR}${chars.BR}${chars.RESET}`;

View file

@ -5,7 +5,7 @@
const { exec } = require('child_process');
module.exports = () => new Promise((resolve, reject) => {
const buildProcess = exec('npm run build'); // Trigger the build command
const buildProcess = exec('NODE_OPTIONS="--max-old-space-size=512" npm run build'); // Trigger the build command
let output = ''; // Will store console output

234
services/request.js Normal file
View file

@ -0,0 +1,234 @@
/**
* Lightweight HTTP client for server-side (Node.js) code.
* Uses built-in http/https modules - no external dependencies.
* Replaces axios for all server-side requests.
*
* Supports: .get(), .request(), custom httpsAgent options, maxRedirects,
* gzip/deflate/brotli decompression, optional timeout,
* and exposes the raw socket (needed by status-check.js for servername).
*/
const http = require('http');
const https = require('https');
const zlib = require('zlib');
const { URL } = require('url');
class RequestError extends Error {
constructor(message, { response, code, errno } = {}) {
super(message);
this.name = 'RequestError';
this.response = response || undefined;
this.code = code || undefined;
this.errno = errno || undefined;
}
// Return a JSON-safe summary, to prevent the any circular references
toJSON() {
return {
name: this.name,
message: this.message,
code: this.code,
errno: this.errno,
status: this.response && this.response.status,
statusText: this.response && this.response.statusText,
data: this.response && this.response.data,
};
}
}
/**
* Core request function.
* @param {Object} config
* @param {string} config.url
* @param {string} [config.method='GET']
* @param {Object} [config.headers={}]
* @param {*} [config.data] - Request body (object will be JSON-stringified)
* @param {number} [config.maxRedirects=5]
* @param {number} [config.timeout=0] - Request timeout in ms (0 = no timeout)
* @param {Object} [config.httpsAgent] - Options for https.Agent (e.g. { rejectUnauthorized })
* @returns {Promise<{data, status, statusText, headers, request}>}
*/
function request(config) {
const {
url,
method = 'GET',
headers = {},
data,
json,
maxRedirects = 5,
timeout = 0,
maxResponseSize = 0,
httpsAgent,
} = config;
return new Promise((resolve, reject) => {
const makeRequest = (targetUrl, redirectsLeft) => {
let parsed;
try {
parsed = new URL(targetUrl);
} catch (e) {
reject(new RequestError(`Invalid URL: ${targetUrl}`));
return;
}
const isHttps = parsed.protocol === 'https:';
const transport = isHttps ? https : http;
const reqOptions = {
method: method.toUpperCase(),
hostname: parsed.hostname,
port: parsed.port || (isHttps ? 443 : 80),
path: parsed.pathname + parsed.search,
headers: { ...headers },
};
// Advertise supported encodings (matching axios behavior)
if (!reqOptions.headers['Accept-Encoding'] && !reqOptions.headers['accept-encoding']) {
reqOptions.headers['Accept-Encoding'] = 'gzip, deflate, br';
}
// Support URL-embedded credentials (e.g. https://user:pass@host)
if (parsed.username && !reqOptions.headers.Authorization) {
const creds = `${decodeURIComponent(parsed.username)}:${decodeURIComponent(parsed.password || '')}`;
reqOptions.headers.Authorization = `Basic ${Buffer.from(creds).toString('base64')}`;
}
// Apply httpsAgent options (e.g. rejectUnauthorized)
if (isHttps && httpsAgent) {
reqOptions.agent = new https.Agent(httpsAgent);
}
// Prepare body
let body = null;
const payload = data || json;
if (payload != null && method.toUpperCase() !== 'GET' && method.toUpperCase() !== 'HEAD') {
body = typeof payload === 'string' ? payload : JSON.stringify(payload);
if (!reqOptions.headers['Content-Type'] && !reqOptions.headers['content-type']) {
reqOptions.headers['Content-Type'] = 'application/json';
}
reqOptions.headers['Content-Length'] = Buffer.byteLength(body);
}
const req = transport.request(reqOptions, (res) => {
// Handle redirects
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
// Drain the response body to free the socket
res.resume();
if (redirectsLeft <= 0) {
reject(new RequestError('Max redirects exceeded'));
return;
}
const redirectUrl = new URL(res.headers.location, targetUrl).href;
makeRequest(redirectUrl, redirectsLeft - 1);
return;
}
// Decompress response based on Content-Encoding (matching axios behavior)
let stream = res;
const encoding = (res.headers['content-encoding'] || '').toLowerCase();
if (encoding === 'gzip' || encoding === 'x-gzip') {
stream = res.pipe(zlib.createGunzip());
} else if (encoding === 'deflate') {
stream = res.pipe(zlib.createInflate());
} else if (encoding === 'br') {
stream = res.pipe(zlib.createBrotliDecompress());
}
const chunks = [];
let totalSize = 0;
let aborted = false;
stream.on('data', (chunk) => {
if (aborted) return;
totalSize += chunk.length;
if (maxResponseSize > 0 && totalSize > maxResponseSize) {
aborted = true;
req.destroy();
reject(new RequestError(
`Response exceeds maximum size of ${maxResponseSize} bytes`,
{ code: 'E_RESPONSE_TOO_LARGE' },
));
return;
}
chunks.push(chunk);
});
stream.on('error', (err) => {
if (aborted) return;
reject(new RequestError(`Decompression failed: ${err.message}`, { code: err.code }));
});
stream.on('end', () => {
if (aborted) return;
const raw = Buffer.concat(chunks).toString('utf8');
let responseData;
try { responseData = JSON.parse(raw); } catch (_) { responseData = raw; }
const response = {
data: responseData,
status: res.statusCode,
statusText: res.statusMessage,
headers: res.headers,
};
// Expose the raw request object for socket access (status-check.js
// needs this). Defined as non-enumerable so JSON.stringify() skips
// it — the http.ClientRequest has circular socket references that
// would otherwise crash any endpoint forwarding the response.
Object.defineProperty(response, 'request', {
value: req,
enumerable: false,
});
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(response);
} else {
reject(new RequestError(
`Request failed with status ${res.statusCode}`,
{ response, code: res.statusCode },
));
}
});
});
// Expose socket events for servername access
req.on('socket', (socket) => {
req.socket = socket;
socket.on('secureConnect', () => {
req.socket = socket;
});
});
req.on('error', (err) => {
reject(new RequestError(
err.message,
{ code: err.code, errno: err.errno },
));
});
// Optional request timeout
if (timeout > 0) {
req.setTimeout(timeout, () => {
req.destroy();
reject(new RequestError(
`timeout of ${timeout}ms exceeded`,
{ code: 'ECONNABORTED' },
));
});
}
if (body) req.write(body);
req.end();
};
makeRequest(url, maxRedirects);
});
}
/** GET shorthand */
request.get = (url, config = {}) => request({ ...config, method: 'GET', url });
/** POST shorthand */
request.post = (url, data, config = {}) => request({ ...config, method: 'POST', url, data });
/** PUT shorthand */
request.put = (url, data, config = {}) => request({ ...config, method: 'PUT', url, data });
module.exports = request;
module.exports.RequestError = RequestError;

View file

@ -5,54 +5,69 @@
* Finally, it will call a function with the status message
*/
const fsPromises = require('fs').promises;
const path = require('path');
const MAX_CONFIG_BYTES = 256 * 1024;
// Disallow paths having path separators, control chars (NUL/CR/LF), or ..
const SAFE_FILENAME = /^(?!\.+$)[^\\/\0\r\n]+\.ya?ml$/i;
module.exports = async (newConfig, render) => {
/* Either returns nothing (if using default path), or strips navigational characters from path */
const makeSafeFileName = (configObj) => {
if (!configObj || !configObj.filename) return undefined;
return configObj.filename.replaceAll('/', '').replaceAll('..', '');
};
const respond = (success, message) => render(JSON.stringify({ success, message }));
const usersFileName = makeSafeFileName(newConfig);
// Validate request body
if (!newConfig || typeof newConfig.config !== 'string' || newConfig.config.length === 0) {
respond(false, "Request body is missing or has an invalid 'config' field");
return;
}
if (newConfig.config.length > MAX_CONFIG_BYTES) {
respond(false, `Config exceeds maximum size of ${MAX_CONFIG_BYTES / 1024} KB`);
return;
}
// Define constants for the config file
const settings = {
defaultLocation: './public/',
defaultFile: 'conf.yml',
filename: 'conf',
backupDenominator: '.backup.yml',
};
// If `filename` (for sub-pages) is specified validate and set it
let usersFileName;
if (typeof newConfig.filename === 'string' && newConfig.filename) {
const base = path.basename(newConfig.filename);
if (!SAFE_FILENAME.test(base)) {
respond(false, 'Invalid filename: must be a basename ending in .yml or .yaml');
return;
}
usersFileName = base;
}
// Make the full file name and path to save the backup config file
const backupFilePath = `${settings.defaultLocation}${usersFileName || settings.filename}-`
+ `${Math.round(new Date() / 1000)}${settings.backupDenominator}`;
// Resolve paths
const userDataDirectory = process.env.USER_DATA_DIR || './user-data/';
const backupLocation = process.env.BACKUP_DIR || path.join(userDataDirectory, 'config-backups');
const targetFile = usersFileName || 'conf.yml';
const targetFilePath = path.join(userDataDirectory, targetFile);
// The path where the main conf.yml should be read and saved to
const defaultFilePath = settings.defaultLocation + (usersFileName || settings.defaultFile);
const backupBase = targetFile.replace(/\.ya?ml$/i, '');
const backupFilePath = path.join(backupLocation, `${backupBase}-${Date.now()}.backup.yml`);
// Returns a string confirming successful job
const getSuccessMessage = () => `Successfully backed up ${settings.defaultFile} to`
+ ` ${backupFilePath}, and updated the contents of ${defaultFilePath}`;
// Backup current config before proceeding
try {
await fsPromises.mkdir(backupLocation, { recursive: true });
await fsPromises.copyFile(targetFilePath, backupFilePath);
} catch (error) {
if (error.code !== 'ENOENT') {
respond(false, `Unable to backup ${targetFile}: ${error}`);
return;
}
}
// Encoding options for writing to conf file
const writeFileOptions = { encoding: 'utf8' };
// Prepare the response returned by the API
const getRenderMessage = (success, errorMsg) => JSON.stringify({
success,
message: !success ? errorMsg : getSuccessMessage(),
});
// Makes a backup of the existing config file
await fsPromises
.copyFile(defaultFilePath, backupFilePath)
.catch((error) => render(getRenderMessage(false, `Unable to backup conf.yml: ${error}`)));
// Writes the new content to the conf.yml file
await fsPromises
.writeFile(defaultFilePath, newConfig.config.toString(), writeFileOptions)
.catch((error) => render(getRenderMessage(false, `Unable to write to conf.yml: ${error}`)));
// Write the new config
try {
await fsPromises.writeFile(targetFilePath, newConfig.config, { encoding: 'utf8' });
} catch (error) {
respond(false, `Unable to write to ${targetFile}: ${error}`);
return;
}
// If successful, then render hasn't yet been called- call it
await render(getRenderMessage(true));
respond(
true,
`Successfully backed up ${targetFile} to ${backupFilePath}, `
+ `and updated the contents of ${targetFilePath}`,
);
};

View file

@ -1,5 +1,5 @@
/* A Netlify cloud function to handle requests to CORS-disabled services */
const axios = require('axios');
const request = require('../request');
exports.handler = (event, context, callback) => {
// Get input data
@ -34,12 +34,12 @@ exports.handler = (event, context, callback) => {
const requestConfig = {
method: 'GET',
url: requestUrl,
json: body,
data: body,
headers: requestHeaders,
};
// Make request
axios.request(requestConfig)
request(requestConfig)
.then((response) => {
callback(null, { statusCode: 200, body: JSON.stringify(response.data) });
}).catch((error) => {

View file

@ -3,8 +3,7 @@
* It accepts a single url parameter, and will make an empty GET request to that
* endpoint, and then resolve the response status code, time taken, and short message
*/
const axios = require('axios').default;
const https = require('https');
const request = require('./request');
/* Determines if successful from the HTTP response code */
const getResponseType = (code, validCodes) => {
@ -34,21 +33,19 @@ const makeRequest = (url, options, render) => {
} = options;
const validCodes = acceptCodes && acceptCodes !== 'null' ? acceptCodes : null;
const startTime = new Date();
const requestMaker = axios.create({
httpsAgent: new https.Agent({
rejectUnauthorized: !enableInsecure,
}),
});
requestMaker.request({
request({
url,
headers,
maxRedirects,
timeout: 10000,
httpsAgent: { rejectUnauthorized: !enableInsecure },
})
.then((response) => {
const statusCode = response.status;
const { statusText } = response;
const successStatus = getResponseType(statusCode, validCodes);
const serverName = response.request.socket.servername;
const serverName = response.request && response.request.socket
? response.request.socket.servername : undefined;
const timeTaken = (new Date() - startTime);
const results = {
statusCode, statusText, serverName, successStatus, timeTaken,
@ -110,7 +107,10 @@ module.exports = (paramStr, render) => {
const maxRedirects = decodeURIComponent(params.get('maxRedirects')) || 0;
const headers = decodeHeaders(params.get('headers'));
const enableInsecure = !!params.get('enableInsecure');
if (!url || url === 'undefined') immediateError(render);
if (!url || url === 'undefined') {
immediateError(render);
return;
}
const options = {
headers, enableInsecure, acceptCodes, maxRedirects,
};

View file

@ -1,4 +1,4 @@
const axios = require('axios').default;
const request = require('./request');
const currentVersion = require('../package.json').version;
@ -22,7 +22,7 @@ const makeMsg = (latestVersion) => {
return msg;
};
axios.get(packageUrl).then((response) => {
request.get(packageUrl).then((response) => {
if (response && response.data && response.data.version) {
logToConsole(`\nUsing Dashy V-${currentVersion}. Update Check Complete`);
logToConsole(makeMsg(response.data.version));