@nasriya/hypercloud
Version:
Nasriya HyperCloud is a lightweight Node.js HTTP2 framework.
652 lines (651 loc) • 24.7 kB
JavaScript
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
const _dirname = import.meta.dirname;
class Helpers {
#_currencies = [];
constructor() {
this.#_currencies = this.loadJSON(path.resolve(_dirname, '../data/currencies.json'));
}
/**
* Load a `JSON` file
* @param filePath The absolute path of the `JSON` file
*/
loadJSON(filePath) {
try {
const validity = this.checkPathAccessibility(filePath);
if (!validity.valid) {
if (validity.errors.notString) {
throw new Error(`The filePath should be string, instead got ${typeof filePath}`);
}
if (validity.errors.doesntExist) {
throw new Error(`The path ${filePath} doesn't exist`);
}
if (validity.errors.doesntExist) {
throw new Error(`You don't have enough permissions to access this path: ${filePath}`);
}
}
if (!filePath.toLowerCase().endsWith('.json')) {
throw new Error(`${path.basename(filePath)} is not a JSON file.`);
}
const strContent = fs.readFileSync(filePath, { encoding: 'utf-8' });
try {
const file = JSON.parse(strContent);
return file;
}
catch (error) {
throw new Error(`The default configuration file is damaged, corrupteed, or not a valid JSON file.`);
}
}
catch (error) {
if (error instanceof Error) {
error.message = `Unable to load JSON file: ${error.message}`;
}
throw error;
}
}
/**
* Deep freeze an object or an array
* @param obj The object or array you want to freeze
*/
deepFreeze(obj) {
if (!this.is.freezable(obj)) {
throw new Error(`${typeof obj} is not freezable`);
}
if (Array.isArray(obj)) {
for (const item of obj) {
this.deepFreeze(item);
}
return Object.freeze(obj);
}
if (this.is.realObject(obj)) {
for (const key in obj) {
if (this.is.freezable(obj[key])) {
// @ts-ignore
obj[key] = this.deepFreeze(obj[key]);
}
}
return Object.freeze(obj);
}
return obj;
}
/**Get the name if this package (project) from the `package.json` file */
getProjectName() {
// Read package.json file
const packageJson = fs.readFileSync('package.json', 'utf8');
// Parse package.json as JSON
const packageData = JSON.parse(packageJson);
// Extract project name
return packageData.name;
}
/**
* Calculate the hash value if a file
* @param {string} filePath The file path
* @returns {Promise<string>} The hashed value
*/
calculateHash(filePath) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('sha256'); // You can use other hash algorithms like 'md5', 'sha1', etc.
const stream = fs.createReadStream(filePath);
stream.on('data', data => {
hash.update(data);
});
stream.on('end', () => {
const fileHash = hash.digest('hex');
resolve(fileHash);
});
stream.on('error', err => {
reject(err);
});
});
}
/**
* Print something on the debug level
* @param {string|any} message
* @returns {void}
*/
printConsole(message) {
if (process.env.HYPERCLOUD_SERVER_VERBOSE === 'TRUE') {
console.debug(message);
}
}
validate = {
/**
* Validate a currency (regardless of letter case)
* @param {string} currency A currency to validate
* @returns {boolean}
*/
currency: (currency) => {
if (typeof currency === 'string') {
currency = currency.toUpperCase();
return this.#_currencies.includes(currency);
}
else {
return false;
}
},
/**
* Validate a locale
* @param {string} locale A locale to validate
* @returns {boolean}
*/
locale: (locale) => {
const languageTagPattern = /^[a-zA-Z]{2,3}(?:-[a-zA-Z]{3})?(?:-[a-zA-Z]{4})?(?:-[a-zA-Z]{2})?(?:-[a-zA-Z]{2})?$/;
return languageTagPattern.test(locale);
},
/**
* Validate an IPv4 or IPv6 address
* @example
* // Example usage:
* console.log(validate.ipAddress('192.168.0.1')); // true
* console.log(validate.ipAddress('2001:0db8:85a3:0000:0000:8a2e:0370:7334')); // true
* console.log(validate.ipAddress('invalid')); // false
* @param {string} ip The IP address to validate
* @returns {boolean}
*/
ipAddress: (ip) => {
const ipPattern = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|^([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:){1,7}:$|^([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}$|^([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}$|^([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}$|^([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}$|^[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})$|:^:$/;
return ipPattern.test(ip);
},
/**
* Pass domain(s) to check whether they're valid to be used for the SSL certificate
* @param {string|string[]} toCheck The domain(s) to check
* @returns {boolean}
*/
domains: (toCheck) => {
const regex = /^(\*\.)?([\w-]+\.)+[\w-]+$/;
if (typeof toCheck === 'string') {
return regex.test(toCheck);
}
else if (Array.isArray(toCheck)) {
/**@type {string[]} */
const invalidDomains = [];
for (const domain of toCheck) {
if (!regex.test(domain)) {
invalidDomains.push(domain);
}
}
if (invalidDomains.length === 0) {
return true;
}
else {
this.printConsole(`You have used invalid domains for the SSL certificate, the domains are: ${invalidDomains.toString()}.`);
return false;
}
}
else {
throw new Error(`The value that was passed on the "validate.domains()" method is invalid. Expected a string or an array of strings but instead got ${typeof toCheck}`);
}
},
/**
* Check the syntax validity of an email address
* @param {string} email The email address to check
* @returns {boolean}
*/
email: (email) => {
const regex = /^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/;
if (regex.test(email)) {
return true;
}
return false;
},
/**
* @param {string} certbotPath
* @returns {boolean}
*/
certbotPath: (certbotPath) => {
const validity = this.checkPathAccessibility(certbotPath);
if (validity.valid) {
return true;
}
if (validity.errors.notString) {
this.printConsole(`The certbot path should've been a string, but instead got ${typeof certbotPath}`);
throw new Error(`The certbot path that was provided is invalid`);
}
if (validity.errors.doesntExist) {
this.printConsole(`The cerbot path you provided (${certbotPath}) does not exist`);
}
if (validity.errors.doesntExist) {
this.printConsole(`Certbot path error: You do not have permissions to read path: ${certbotPath}`);
}
return false;
},
/**
* Validate the project path
* @param {string} projectPath
* @returns {boolean}
*/
projectPath: (projectPath) => {
const validity = this.checkPathAccessibility(projectPath);
if (validity.valid) {
const dirs = fs.readdirSync(projectPath);
return dirs.includes('package.json');
}
else {
if (validity.errors.notString) {
this.printConsole(`The project path should've been a string, but instead got ${typeof projectPath}`);
throw new Error(`The project path that was provided is invalid`);
}
if (validity.errors.doesntExist) {
this.printConsole(`The project path you provided (${projectPath}) does not exist`);
}
if (validity.errors.notAccessible) {
this.printConsole(`Project path path error: You do not have permissions to read path: ${projectPath}`);
}
return false;
}
}
};
/**
* Check the accessibility of a directory. This also checks whether the directory exists or not.
* @param {string} path The path to check
*/
checkPathAccessibility(path) {
const errors = Object.seal({
notString: typeof path !== 'string',
doesntExist: false,
notAccessible: false
});
if (errors.notString) {
return { valid: false, errors };
}
errors.doesntExist = !fs.existsSync(path);
if (errors.doesntExist) {
return { valid: false, errors };
}
try {
fs.accessSync(path, fs.constants.R_OK | fs.constants.W_OK);
}
catch (error) {
errors.notAccessible = true;
return { valid: false, errors };
}
return { valid: true };
}
/**
* Add a message/comment to a bat file
* @param {string} batStr The original BAT string
* @param {string} msg The message you want to add to the BAT string
* @returns {string} The updated BAT string
*/
addBatMessage(batStr, msg) {
if (!batStr.includes('@echo off')) {
batStr = `@echo off\n${batStr}`;
}
batStr += `echo ${msg}\n`;
return batStr;
}
/**
* Parse the request's cookies header
* @param {string} cookiesHeader The cookies header from a request
* @returns The cookies as an object
*/
parseCookies(cookiesHeader) {
const cookies = {};
if (typeof cookiesHeader === 'string') {
cookiesHeader.split(';').forEach(cookie => {
const [key, value] = cookie.trim().split('=');
//@ts-ignore
cookies[key] = value;
});
}
return cookies;
}
/**
* Verify whether your Node.js process is running in CommonJS `cjs` or ECMAScript modules `esm`
* @returns {'commonjs'|'module'}
*/
getNodeEnv() {
return typeof module !== 'undefined' && module.exports ? 'commonjs' : 'module';
}
/**
* Load a module (either a file or a package)
* @param {string} name The name of the module to load
* @param {Object} [options] Additional options
* @param {boolean} [options.isFile] Whether the name is a file path
* @returns {Promise<any>} A promise that resolves with the loaded module
* @throws {Error} If the module couldn't be loaded
*/
async loadModule(name, options) {
return new Promise((resolve, reject) => {
try {
const isFile = options?.isFile ?? false;
const nodeEnv = this.getNodeEnv();
if (nodeEnv === 'commonjs') {
const mod = require(name);
resolve('default' in mod ? mod.default : mod);
}
else {
// @ts-ignore
import(isFile ? `file://${name}` : name).then(mod => resolve('default' in mod ? mod.default : mod));
}
}
catch (error) {
if (error instanceof Error) {
error.message = `Unable to load module (${name}): ${error.message}`;
}
reject(error);
}
});
}
/**
* Load a module from a file
* @param {string} filePath The path to the file
* @returns {Promise<any>} The module
*/
async loadFileModule(filePath) {
return this.loadModule(filePath, { isFile: true });
}
/**
* Get the local IP address of the server
* @returns {string[]} An array of local IPs
*/
async getLocalIPs() {
const os = await this.loadModule('os');
const nets = os.networkInterfaces();
const interfaces = {};
for (const name of Object.keys(nets)) {
for (const net of nets[name]) {
// Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses
// 'IPv4' is in Node <= 17, from 18 it's a number 4 or 6
const familyV4Value = typeof net.family === 'string' ? 'IPv4' : 4;
if (net.family === familyV4Value && !net.internal) {
if (!interfaces[name]) {
interfaces[name] = [];
}
interfaces[name].push(net.address);
}
}
}
const interfacesArr = Object.entries(interfaces).map(entry => {
return { name: entry[0], ips: entry[1] };
});
interfacesArr.sort((int1, int2) => {
if (int1.name === 'Ethernet' && int2.name === 'Ethernet') {
return 0;
}
if (int1.name === 'Ethernet') {
return -1;
}
if (int2.name === 'Ethernet') {
return 1;
}
if (int1.name === 'vEthernet' && int2.name === 'vEthernet') {
return 0;
}
if (int1.name === 'vEthernet') {
return -1;
}
if (int2.name === 'vEthernet') {
return 1;
}
return 0;
});
const local_ips = interfacesArr.map(i => i.ips).flat(3);
return [...new Set(local_ips)];
}
/**
* Generate a random text
* @param length The length of the text. Minimum of `4`
* @param [options] Options for generating the text
* @returns
*/
generateRandom(length, options = {}) {
const { includeNumbers = true, includeLetters = true, includeSymbols = true, includeLowerCaseChars = true, includeUpperCaseChars = true, beginWithLetter = true, noSimilarChars = true, noDuplicateChars = false, noSequentialChars = true } = options;
let chars = '';
let text = '';
if (includeNumbers)
chars += '0123456789';
if (includeLetters) {
if (includeLowerCaseChars)
chars += 'abcdefghijklmnopqrstuvwxyz';
if (includeUpperCaseChars)
chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
}
if (includeSymbols)
chars += '!";#$%&\'()*+,-./:;<=>?@[]^_`{|}~';
if (beginWithLetter && (includeLetters || includeNumbers || includeSymbols)) {
const validChars = includeLetters && includeNumbers && includeSymbols ? chars : chars.slice(10);
text += validChars.charAt(Math.floor(Math.random() * validChars.length));
}
while (text.length < length) {
const randomIndex = Math.floor(Math.random() * chars.length);
const char = chars[randomIndex];
if ((noSimilarChars && /[il1LoO]/.test(char)) ||
(noDuplicateChars && text.includes(char)) ||
(noSequentialChars && text.length > 0 && text[text.length - 1].charCodeAt(0) + 1 === char.charCodeAt(0))) {
continue;
}
text += char;
}
return text;
}
is = {
/**
* Check if a given value is a number.
* @param {any} value The value to check.
* @returns {value is number}
*/
number: (value) => {
return typeof value === 'number' && !Number.isNaN(value);
},
/**
* Check if a given value is a string.
* @param {any} value The value to check.
* @returns {value is string}
*/
string: (value) => {
return typeof value === 'string';
},
/**
* Check if a given MIME type is valid.
* @param {any} mime The MIME type to check.
* @returns {boolean}
*/
validMime: (mime) => {
if (typeof mime !== 'string') {
return false;
}
const mimes = [
"audio/aac", "application/x-abiword", "application/x-freearc", "image/avif",
"video/x-msvideo", "application/vnd.amazon.ebook", "application/octet-stream",
"image/bmp", "application/x-bzip", "application/x-bzip2", "application/x-cdf",
"application/x-csh", "text/calendar", "text/css", "text/plain", "text/csv", "application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-fontobject", "application/epub+zip", "application/gzip",
"image/gif", "text/html", "image/vnd.microsoft.icon", "text/calendar", "application/java-archive",
"image/jpeg", "text/javascript", "application/json", "application/ld+json", "audio/midi",
"audio/x-midi", "audio/mpeg", "video/mp4", "video/mpeg", "application/vnd.apple.installer+xml",
"application/vnd.oasis.opendocument.presentation", "application/vnd.oasis.opendocument.spreadsheet",
"application/vnd.oasis.opendocument.text", "audio/ogg", "video/ogg", "application/ogg",
"audio/opus", "font/otf", "image/png", "application/pdf", "application/x-httpd-php",
"application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.rar", "application/rtf", "application/x-sh", "image/svg+xml",
"application/x-tar", "image/tiff"
];
return mimes.includes(mime);
},
/**
* Check if a given value is a valid URL.
* @param {any} str The value to check.
* @returns {boolean}
*/
validURL: (str) => {
try {
new URL(str);
return true;
}
catch (_) {
return false;
}
},
/**
* Check if a given value is path-like.
* @param {any} value The value to check.
* @returns {value is fs.PathLike}
*/
pathLike: (value) => {
if (typeof value === 'string' || value instanceof Buffer) {
return true;
}
try {
new URL(value);
return true;
}
catch (error) {
return false;
}
},
/**
* Check if a given value can be frozen (i.e., is an object or array).
* @param {any} value The value to check.
* @returns {boolean}
*/
freezable: (value) => {
return this.is.realObject(value) || Array.isArray(value);
},
/**
* Check if a given string is valid HTML code.
* @param {string} string The string to check.
* @returns {boolean}
*/
html: (string) => {
const regex = /<([A-Za-z][A-Za-z0-9]*)\b[^>]*>(.*?)<\/\1>/;
return regex.test(string);
},
/**
* Check if a given value is a real object (i.e., not null or an array).
* @param {any} obj The value to check.
* @returns {boolean}
*/
realObject: (obj) => {
return typeof obj === 'object' && obj !== null && !Array.isArray(obj);
},
/**
* Check if a given value is a valid string.
* @param {any} str The value to check.
* @returns {boolean}
*/
validString: (str) => {
return typeof str === 'string' && str.trim().length > 0;
},
/**
* Check if a given value is undefined.
* @param {any} arg The value to check.
* @returns {arg is undefined}
*/
undefined: (arg) => {
return typeof arg === 'undefined';
},
/**
* Check if a given value is an integer.
* @param {any} value The value to check.
* @returns {boolean}
*/
integer: (value) => {
return this.is.number(value) && Number.isInteger(value);
}
};
isNot = {
/**
* Check if a given value is not a number.
* @param {any} value The value to check.
* @returns {value is Exclude<any, number>}
*/
number: (value) => {
return typeof value !== 'number';
},
/**
* Check if a given value is not a string.
* @param {any} value The value to check.
* @returns {value is Exclude<any, string>}
*/
string: (value) => {
return typeof value !== 'string';
},
/**
* Check if a given MIME type is not valid.
* @param {any} mime The MIME type to check.
* @returns {boolean}
*/
validMime: (mime) => {
return !this.is.validMime(mime);
},
/**
* Check if a given value is not a valid URL.
* @param {any} str The value to check.
* @returns {boolean}
*/
validURL: (str) => {
return !this.is.validURL(str);
},
/**
* Check if a given value is not path-like.
* @param {any} value The value to check.
* @returns {value is Exclude<fs.PathLike, string | Buffer | URL>}
*/
pathLike: (value) => {
return !this.is.pathLike(value);
},
/**
* Check if a given value is not freezable.
* @param {any} value The value to check.
* @returns {boolean}
*/
freezable: (value) => {
return !this.is.freezable(value);
},
/**
* Check if a given string is not valid HTML code.
* @param {string} string The string to check.
* @returns {boolean}
*/
html: (string) => {
return !this.is.html(string);
},
/**
* Check if a given value is not a real object.
* @param {any} obj The value to check.
* @returns {boolean}
*/
realObject: (obj) => {
return !this.is.realObject(obj);
},
/**
* Check if a given value is not a valid string.
* @param {any} str The value to check.
* @returns {boolean}
*/
validString: (str) => {
return !this.is.validString(str);
},
/**
* Check if a given value is not undefined.
* @param {any} arg The value to check.
* @returns {arg is Exclude<any, undefined>}
*/
undefined: (arg) => {
return typeof arg !== 'undefined';
},
/**
* Check if a given value is not an integer.
* @param {any} value The value to check.
* @returns {boolean}
*/
integer: (value) => {
return !this.is.integer(value);
}
};
/**
* Checks if the given object has the specified property as its own property.
* This method does not check properties inherited through the prototype chain.
*
* @param obj - The object to check for the property.
* @param prop - The name of the property to check for.
* @returns A boolean indicating whether the object has the specified property as its own property.
*/
hasOwnProperty(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
}
}
export default new Helpers;