@nasriya/hypercloud
Version:
Nasriya HyperCloud is a lightweight Node.js HTTP2 framework.
992 lines • 79.8 kB
JavaScript
import path from 'path';
import helpers from '../../../utils/helpers.js';
import Renderer from '../../renderer/renderer.js';
import Cookies from './cookies.js';
import fs from 'fs';
import ms from 'ms';
const _dirname = import.meta.dirname;
const mimes = helpers.loadJSON(path.resolve(_dirname, '../../../data/mimes.json'));
const extensions = helpers.loadJSON(path.resolve(_dirname, '../../../data/extensions.json'));
/**
* TODO: Change all the server examples to use my own server class
*/
/**This class is used internally, not by the user */
export class HyperCloudResponse {
#_server;
#_req;
#_res;
#_cookies;
#_preservedHeaders = ['x-server', 'x-request-id'];
#_encodings = Object.freeze([
"ascii",
"utf8",
"utf-8",
"utf16le",
"utf-16le",
"ucs2",
"ucs-2",
"base64",
"base64url",
"latin1",
"binary",
"hex"
]);
#_status = Object.seal({
closed: false
});
#_next = undefined;
constructor(server, req, res) {
this.#_server = server;
this.#_req = req;
this.#_res = res;
this.#_cookies = new Cookies(this);
}
get pages() {
return Object.freeze({
/**
* Return a not found `404` response.
*
* By default, **HyperCloud** returns its own `404` page. To return your
* own page use the {@link HyperCloudServer.setHandler} method.
* @example
* // Use the default 404 page
* response.pages.notFound({
* locals: {
* title: '404 - Not Found',
* subtitle: 'This page cannot be found',
* home: 'Home'
* }
* });
*
* // All options are "optional" and can be omitted
* response.pages.notFound(); // Renders the default 404 page
* @example
* // Setting your own handler
* server.handlers.notFound((request, response, next) => {
* // Decide what to do here
* })
* @param {NotFoundResponseOptions} [options] Rendering options
*/
notFound: async (options) => {
try {
if (typeof this.#_server._handlers.notFound === 'function') {
try {
// Run the user defined handler for not-found resources
this.#_server._handlers.notFound(this.#_req, this, this._next);
}
catch (error) {
this.pages.serverError({ error: error });
}
}
else {
const viewName = 'hypercloud_404';
const page = this.server.rendering.pages.storage[viewName];
const locals = page.locals.get(this.req.language);
const renderOptions = {
locals: {
title: helpers.is.validString(options?.locals?.title) ? options?.locals?.title : locals.title,
subtitle: helpers.is.validString(options?.locals?.subtitle) ? options?.locals?.subtitle : locals.subtitle,
homeBtnLabel: helpers.is.validString(options?.locals?.homeBtnLabel) ? options?.locals?.homeBtnLabel : locals.homeBtnLabel
},
httpOptions: {
cacheControl: false,
statusCode: 404,
}
};
return this.render(viewName, renderOptions);
}
}
catch (error) {
console.error(error);
return this.pages.serverError();
}
},
/**
* Return an unauthorized `401` response.
*
* By default, **HyperCloud** returns its own `401` page. To return your
* own page use the {@link HyperCloudServer.setHandler} method.
* @example
* // Use the default 401 page
* response.pages.unauthorized({
* locals: {
* title: '401 - Unauthorized',
* commands: {
* code: 'ERROR CODE',
* description: 'ERROR DESCRIPTION',
* cause: 'ERROR POSSIBLY CAUSED BY',
* allowed: 'SOME PAGES ON THIS SERVER THAT YOU DO HAVE PERMISSION TO ACCESS',
* regards: 'HAVE A NICE DAY :-)'
* },
* content: {
* code: 'HTTP 401 Unauthorized',
* description: 'Access Denied. You Do Not Have The Permission To Access This Page',
* cause: 'execute access unauthorized, read access unauthorized, write access unauthorized',
* allowed: [{ label: 'Home', link: '/' }, { label: 'About Us', link: '/about' }, { label: 'Contact Us', link: '/support/contact' }],
* }
* }
* });
*
* // All options are "optional" and can be omitted
* response.pages.unauthorized(); // Renders the default 401 page
* @example
* // Setting your own handler
* server.handlers.unauthorized((request, response, next) => {
* // Decide what to do here
* })
* @param {ForbiddenAndUnauthorizedOptions} [options]
*/
unauthorized: async (options) => {
try {
if (typeof this.#_server._handlers.unauthorized === 'function') {
try {
// Run the user defined handler for not-found resources
this.#_server._handlers.unauthorized(this.#_req, this, this._next);
}
catch (error) {
this.pages.serverError({ error: error });
}
}
else {
const viewName = 'hypercloud_401';
const page = this.server.rendering.pages.storage[viewName];
const locals = page.locals.get(this.req.language);
const renderOptions = {
locals: {
title: helpers.is.validString(options?.locals?.title) ? options?.locals?.title : locals.title,
code: locals.code,
description: locals.description,
commands: {
code: helpers.is.validString(options?.locals?.commands?.code) ? options?.locals?.commands?.code : locals.commands.code,
description: helpers.is.validString(options?.locals?.commands?.description) ? options?.locals?.commands?.description : locals.commands.description,
cause: helpers.is.validString(options?.locals?.commands?.cause) ? options?.locals?.commands?.cause : locals.commands.cause,
allowed: helpers.is.validString(options?.locals?.commands?.allowed) ? options?.locals?.commands?.allowed : locals.commands.allowed,
regards: helpers.is.validString(options?.locals?.commands?.regards) ? options?.locals?.commands?.regards : locals.commands.regards,
},
content: {
code: helpers.is.validString(options?.locals?.content?.code) ? options?.locals?.content?.code : locals.content.code,
description: helpers.is.validString(options?.locals?.content?.description) ? options?.locals?.content?.description : locals.content.description,
cause: helpers.is.validString(options?.locals?.content?.cause) ? options?.locals?.content?.cause : locals.content.cause,
allowed: Array.isArray(options?.locals?.content?.allowed) ? options?.locals?.commands?.allowed : locals.commands.allowed,
}
},
httpOptions: {
cacheControl: false,
statusCode: 401,
}
};
return this.render(viewName, renderOptions);
}
}
catch (error) {
console.error(error);
return this.pages.serverError();
}
},
/**
* Return a forbidden `403` response.
*
* By default, **HyperCloud** returns its own `403` page. To return your
* own page use the {@link HyperCloudServer.setHandler} method.
* @example
* // Use the default 403 page
* response.pages.forbidden({
* locals: {
* title: '403 - Forbidden',
* commands: {
* code: 'ERROR CODE',
* description: 'ERROR DESCRIPTION',
* cause: 'ERROR POSSIBLY CAUSED BY',
* allowed: 'SOME PAGES ON THIS SERVER THAT YOU DO HAVE PERMISSION TO ACCESS',
* regards: 'HAVE A NICE DAY :-)'
* },
* content: {
* code: 'HTTP 403 Forbidden',
* description: 'Access Denied. You Do Not Have The Permission To Access This Page',
* cause: 'execute access forbidden, read access forbidden, write access forbidden',
* allowed: [{ label: 'Home', link: '/' }, { label: 'About Us', link: '/about' }, { label: 'Contact Us', link: '/support/contact' }],
* }
* }
* });
*
* // All options are "optional" and can be omitted
* response.pages.forbidden(); // Renders the default 403 page
* @example
* // Setting your own handler
* server.handlers.forbidden((request, response, next) => {
* // Decide what to do here
* })
* @param {ForbiddenAndUnauthorizedOptions} options
*/
forbidden: async (options) => {
try {
if (typeof this.#_server._handlers.forbidden === 'function') {
try {
// Run the user defined handler for not-found resources
this.#_server._handlers.forbidden(this.#_req, this, this._next);
}
catch (error) {
this.pages.serverError({ error: error });
}
}
else {
const viewName = 'hypercloud_403';
const page = this.server.rendering.pages.storage[viewName];
const locals = page.locals.get(this.req.language);
const renderOptions = {
locals: {
title: helpers.is.validString(options?.locals?.title) ? options?.locals?.title : locals.title,
code: locals.code,
description: locals.description,
commands: {
code: helpers.is.validString(options?.locals?.commands?.code) ? options?.locals?.commands?.code : locals.commands.code,
description: helpers.is.validString(options?.locals?.commands?.description) ? options?.locals?.commands?.description : locals.commands.description,
cause: helpers.is.validString(options?.locals?.commands?.cause) ? options?.locals?.commands?.cause : locals.commands.cause,
allowed: helpers.is.validString(options?.locals?.commands?.allowed) ? options?.locals?.commands?.allowed : locals.commands.allowed,
regards: helpers.is.validString(options?.locals?.commands?.regards) ? options?.locals?.commands?.regards : locals.commands.regards,
},
content: {
code: helpers.is.validString(options?.locals?.content?.code) ? options?.locals?.content?.code : locals.content.code,
description: helpers.is.validString(options?.locals?.content?.description) ? options?.locals?.content?.description : locals.content.description,
cause: helpers.is.validString(options?.locals?.content?.cause) ? options?.locals?.content?.cause : locals.content.cause,
allowed: Array.isArray(options?.locals?.content?.allowed) ? options?.locals?.commands?.allowed : locals.commands.allowed,
}
},
httpOptions: {
cacheControl: false,
statusCode: 403,
}
};
return this.render(viewName, renderOptions);
}
}
catch (error) {
console.error(error);
return this.pages.serverError();
}
},
/**
* Return a server error `500` response.
*
* By default, **HyperCloud** returns its own `500` page. To return your
* own page use the {@link HyperCloudServer.setHandler} method.
* @example
* // Use the default 500 page
* response.pages.serverError({
* locals: {
* title: '500 - Server Error',
* subtitle: 'Internal <code>Server error<span>!</span></code>',
* message: '<p> We\'re sorry, but something went wrong on our end. </p>'
* },
* error: new Error('Something went wrong')
* });
*
* // All options are "optional" and can be omitted
* response.pages.serverError(); // Renders the default 500 page
* @example
* // Setting your own handler
* server.handlers.serverError((request, response, next) => {
* // Decide what to do here
* })
* @param {ServerErrorOptions} options
*/
serverError: async (options) => {
try {
if (options && 'error' in options) {
const dashLine = '#'.repeat(50);
const diver = `${dashLine}\n${dashLine}`;
helpers.printConsole(diver);
console.error(`A server error has occurred`);
helpers.printConsole(`${new Date().toUTCString()} - Page Load Error - Request ID: ${this.#_req.id}`);
helpers.printConsole(`Request:\n${this.#_req._toString()}`);
helpers.printConsole(options.error);
helpers.printConsole(diver);
}
if (typeof this.#_server._handlers.serverError === 'function' && options?.bypassHandler !== true) {
try {
// Run the user defined handler for not-found resources
this.#_server._handlers.serverError(this.#_req, this, this._next);
}
catch (error) {
this.pages.serverError({ bypassHandler: true });
}
}
else {
const viewName = 'hypercloud_500';
const page = this.server.rendering.pages.storage[viewName];
const locals = page.locals.get(this.req.language);
const renderOptions = {
locals: {
title: helpers.is.validString(options?.locals?.title) ? options?.locals?.title : locals.title,
subtitle: helpers.is.validString(options?.locals?.subtitle) ? options?.locals?.subtitle : locals.subtitle,
message: helpers.is.validString(options?.locals?.message) ? options?.locals?.message : locals.message,
},
httpOptions: {
cacheControl: false,
statusCode: 500,
}
};
return this.render(viewName, renderOptions);
}
}
catch (error) {
console.error(error);
return this.status(500).json({ message: 'A serious server error has occurred. Please report this issue to the framework repo.' });
}
}
});
}
/**
* HyperCloud's next() function
* @private
*/
get _next() { return this.#_next; }
set _next(value) {
if (typeof value === 'function') {
this.#_next = value;
}
}
/**
* Redirect the client to a new location
* @param {string} url A relative or full path URL.
* @param {RedirectCode} [code] A redirect code. Default `307`. Learn more about [redirections in HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections).
*/
redirect(url, code = 307) {
try {
if (typeof url !== 'string') {
throw new TypeError(`The redirect URL should be a string, but instead got ${typeof url}`);
}
if (typeof code === 'number' || typeof code === 'string') {
if (typeof code === 'string') {
try {
code = Number.parseInt(code);
}
catch (error) {
throw new TypeError(`The redirect code should be a number, instead got ${typeof code}`);
}
}
const codes = [300, 301, 302, 303, 304, 307, 308];
if (!codes.includes(code)) {
throw new RangeError(`Invalid redirect code: ${code}. Learn more about redirections at: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections`);
}
this.status(code).setHeader('Location', url);
return this.end();
}
else {
throw new TypeError(`The redirect code should be a number, instead got ${typeof code}`);
}
}
catch (error) {
if (typeof error === 'string') {
error = `Unable to redirect: ${error}`;
}
if (error instanceof Error) {
error.message = `Unable to redirect: ${error.message}`;
}
throw error;
}
}
/**
* Render a page template with the provided options.
* @param {string} name A defined `Page` name
* @param {PageRenderingOptions} options
* @returns {HyperCloudResponse}
*/
async render(name, options) {
try {
const renderer = new Renderer(this.#_req, name);
const html = await renderer.render(options);
this.setHeader('Content-Type', 'text/html');
if (options && 'httpOptions' in options) {
if (options && options.httpOptions && helpers.is.realObject(options.httpOptions)) {
if ('cacheControl' in options.httpOptions) {
if (typeof options.httpOptions.cacheControl !== 'boolean') {
throw new TypeError(`The "cacheControl" option in response.render expected a boolean value but instead got ${typeof options.httpOptions.cacheControl}`);
}
if (options.httpOptions.cacheControl === true) {
const ONEYEAR = 31_536_000_000; // in ms
const cache = { maxAge: 0, immutable: false };
if (!('maxAge' in options.httpOptions)) {
throw new SyntaxError('The render cache-control was enabled without providing the maxAge');
}
if (!(typeof options.httpOptions.maxAge === 'number' || typeof options.httpOptions.maxAge === 'string')) {
throw new TypeError(`The maxAge property should be either a number or string, but instead got ${typeof options.httpOptions.maxAge}`);
}
if (typeof options.httpOptions.maxAge === 'number') {
cache.maxAge = options.httpOptions.maxAge;
}
if (typeof options.httpOptions.maxAge === 'string') {
const maxAgeStr = options.httpOptions.maxAge.trim();
if (maxAgeStr.length === 0) {
throw new SyntaxError(`The maxAge string value cannot be empty`);
}
const value = ms(maxAgeStr);
if (typeof value !== 'number') {
throw new SyntaxError(`${options.httpOptions.maxAge} is not a valid maxAge value`);
}
cache.maxAge = value;
}
if (options.httpOptions.maxAge < 0) {
throw new RangeError(`The maxAge cannot be a negative value`);
}
if ((options.httpOptions.maxAge) > ONEYEAR) {
throw new RangeError(`The maxAge value should not be more than one year`);
}
if ('immutable' in options.httpOptions) {
if (typeof options.httpOptions.immutable !== 'boolean') {
throw new TypeError(`The immutable property only accepts boolean values, but instead got ${typeof options.httpOptions.immutable}`);
}
cache.immutable = true;
}
const expiryDate = new Date(Date.now() + cache.maxAge).toUTCString();
this.setHeader('Cache-Control', `public, max-age=${cache.maxAge}${cache.immutable ? ', immutable' : ''}`);
this.setHeader('Expires', expiryDate);
}
else {
this.setHeader('Cache-Control', 'no-cache');
}
}
else {
this.setHeader('Cache-Control', 'no-cache');
}
if ('statusCode' in options.httpOptions) {
if (typeof options.httpOptions.statusCode !== 'number') {
throw new TypeError(`The "statusCode" option in response.render expected a number value but instead got ${typeof options.httpOptions.statusCode}`);
}
this.status(options.httpOptions.statusCode);
}
if ('eTag' in options.httpOptions && options.httpOptions.eTag) {
if (typeof options.httpOptions.eTag !== 'string') {
throw new TypeError(`The "eTag" option in response.render expected a string value but got ${typeof options.httpOptions.eTag}`);
}
this.setHeader('etag', options.httpOptions.eTag);
}
}
}
this.write({ chunk: html, encoding: 'utf-8' });
return this.end();
}
catch (error) {
if (typeof error === 'string') {
error = `Unable to render page: ${error}`;
}
if (error instanceof Error) {
error.message = `Unable to render page: ${error.message}`;
}
throw error;
}
}
/**
* Download a file using the `response.downloadFile` method.
* @param {string} filePath The file path (relative/absolute). When providing a relative path, you must specify the `root` in the `options` argument
* @param {DownloadFileOptions} options Options for sending the file
* @returns {http2.Http2ServerResponse|undefined}
*/
downloadFile(filePath, options) {
const sendOptions = helpers.is.realObject(options) ? { ...options, download: true } : { download: true };
return this.sendFile(filePath, sendOptions);
}
/**
* Send a file back to the client
* @param {string} filePath The file path (relative/absolute). When providing a relative path, you must specify the `root` in the `options` argument
* @param {SendFileOptions} [options] Options for sending the file
* @returns {http2.Http2ServerResponse|undefined}
*/
sendFile(filePath, options) {
const root = process.cwd();
try {
// Basic filePath validations
if (typeof filePath !== 'string') {
throw new TypeError(`The sendFile expected a file path to be passed as its first argument, but instead got ${typeof filePath}`);
}
if (filePath.length === 0) {
throw new SyntaxError('The file path cannot be an empty string');
}
// Validating the root path if provided
if (options && 'root' in options) {
if (typeof options.root !== 'string') {
throw new TypeError(`The root path of the file should be of type string, but instead got ${typeof options.root}`);
}
if (options.root.length === 0) {
throw new SyntaxError(`The root path cannot be an empty string`);
}
const rootAvail = helpers.checkPathAccessibility(options.root);
if (!rootAvail.valid) {
if (rootAvail.errors.doesntExist) {
throw new Error(`The provided root path (${options.root}) doesn't exist`);
}
if (rootAvail.errors.doesntExist) {
throw new Error(`You don't have enough permissions to access the root path: ${options.root}`);
}
}
filePath = path.resolve(options.root, filePath);
if (!filePath.startsWith(options.root)) {
throw new RangeError(`When providing a relative filePath, the relative path must not escape the provided root directory`);
}
}
// Validating the file path
const fileAvail = helpers.checkPathAccessibility(filePath);
if (!fileAvail.valid) {
if (fileAvail.errors.doesntExist) {
throw new Error(`The provided filePath (${filePath}) doesn't exist`);
}
if (fileAvail.errors.notAccessible) {
throw new Error(`You don't have enough permissions to access the file path: ${filePath}`);
}
}
const fileName = (() => {
if (options && 'fileName' in options) {
if (helpers.isNot.validString(options.fileName)) {
throw new TypeError(`The procided filename is not a string but a ${typeof options.fileName}`);
}
return options.fileName;
}
else {
const paths = filePath.split('\\');
return paths[paths.length - 1];
}
})();
// Handling dotFiles
if (fileName.startsWith('.')) {
if (options && 'dotfiles' in options) {
const allowed = ['allow', 'deny', 'ignore'];
if (!allowed.includes(options.dotfiles || '')) {
throw new TypeError(`The dotfiles property was provided with an unsupported value. Only "allow", "deny", and "ignore" are supported`);
}
const choice = options.dotfiles;
if (choice === 'ignore') {
if ('notFoundFile' in options) {
const notFoundAvail = helpers.checkPathAccessibility(options.notFoundFile);
if (!notFoundAvail.valid) {
if (notFoundAvail.errors.notString) {
throw new Error(`The notFoundFile path should be a string, instead got ${typeof options.notFoundFile}`);
}
if (notFoundAvail.errors.doesntExist) {
throw new Error(`The notFoundFile path (${options.notFoundFile}) doesn't exist`);
}
if (notFoundAvail.errors.notAccessible) {
throw new Error(`You don't have enough permissions to access the notFoundFile path: ${options.notFoundFile}`);
}
}
if (!options.notFoundFile?.toLowerCase().startsWith(root.toLowerCase())) {
throw new RangeError(`The not 404 file (${options.notFoundFile}) is not in your root directory.`);
}
this.setHeader('Content-Type', 'text/html');
this.write({ chunk: fs.readFileSync(options.notFoundFile) });
this.end();
return;
}
else {
this.status(404).json({ message: 'File not found', code: 404 });
return;
}
}
if (choice === 'deny') {
if ('unauthorizedFile' in options) {
const unAuthAvail = helpers.checkPathAccessibility(options.unauthorizedFile);
if (!unAuthAvail.valid) {
if (unAuthAvail.errors.notString) {
throw new Error(`The unauthorizedFile path should be a string, instead got ${typeof options.unauthorizedFile}`);
}
if (unAuthAvail.errors.doesntExist) {
throw new Error(`The unauthorizedFile path (${options.unauthorizedFile}) doesn't exist`);
}
if (unAuthAvail.errors.notAccessible) {
throw new Error(`You don't have enough permissions to access the unauthorizedFile path: ${options.unauthorizedFile}`);
}
}
if (!options.unauthorizedFile?.toLowerCase().startsWith(root.toLowerCase())) {
throw new RangeError(`The not 401 file (${options.unauthorizedFile}) is not in your root directory.`);
}
this.setHeader('Content-Type', 'text/html');
this.write({ chunk: fs.readFileSync(options.unauthorizedFile) });
this.end();
return;
}
else {
this.status(401).json({ message: 'Unauthorized', code: 401 });
return;
}
}
}
}
// Validate the modification property (if provided)
if (options && 'lastModified' in options) {
if (typeof options.lastModified !== 'boolean') {
throw new TypeError(`The lastModified option can only be a boolean type, but instead got ${typeof options.lastModified}`);
}
}
// Get the file stats
const stats = fs.statSync(filePath);
// Set the modification time
if (!options || options?.lastModified !== false) {
this.setHeader('Last-Modified', stats.mtime.toUTCString());
}
// Checking the cache-control
if (options && 'cacheControl' in options) {
if (typeof options.cacheControl !== 'boolean') {
throw new TypeError(`The cacheControl option can only be a boolean type, but instead got ${typeof options.cacheControl}`);
}
if (options.cacheControl) {
const ONEYEAR = 31_536_000_000; // in ms
const cache = { maxAge: 0, immutable: false };
if (!('maxAge' in options)) {
throw new SyntaxError('The sendFile cache-control was enabled without providing the maxAge');
}
if (!(typeof options.maxAge === 'number' || typeof options.maxAge === 'string')) {
throw new TypeError(`The maxAge property should be either a number or string, but instead got ${typeof options.maxAge}`);
}
if (typeof options.maxAge === 'number') {
cache.maxAge = options.maxAge;
}
if (typeof options.maxAge === 'string') {
const maxAge = options.maxAge.trim();
if (maxAge.length === 0) {
throw new SyntaxError(`The maxAge string value cannot be empty`);
}
const value = ms(maxAge);
if (typeof value !== 'number') {
throw new SyntaxError(`${options.maxAge} is not a valid maxAge value`);
}
cache.maxAge = value;
}
if (options.maxAge < 0) {
throw new RangeError(`The maxAge cannot be a negative value`);
}
if (options.maxAge > ONEYEAR) {
throw new RangeError(`The maxAge value should not be more than one year`);
}
if ('immutable' in options) {
if (typeof options.immutable !== 'boolean') {
throw new TypeError(`The immutable property only accepts boolean values, but instead got ${typeof options.immutable}`);
}
cache.immutable = true;
}
const expiryDate = new Date(Date.now() + cache.maxAge).toUTCString();
this.setHeader('Cache-Control', `public, max-age=${cache.maxAge}${cache.immutable ? ', immutable' : ''}`);
this.setHeader('Expires', expiryDate);
}
}
// Applying the headers
if (options && 'headers' in options) {
const headers = Object.keys(options.headers || {});
if (typeof options.headers === 'object' && headers.length > 0) {
const preserved = [...this.#_preservedHeaders, 'last-modified', 'cache-control', 'expires'];
const headersUsed = [...preserved];
for (const headerInput of headers) {
const headerName = headerInput.toLowerCase();
if (!headersUsed.includes(headerName)) {
headersUsed.push(headerName);
this.setHeader(headerName, options.headers[headerInput]);
}
}
}
}
if (options && 'eTag' in options && options.eTag) {
if (typeof options.eTag !== 'string') {
throw new TypeError(`The "eTag" option in response.sendFile expected a string value but got ${typeof options.eTag}`);
}
this.setHeader('etag', options.eTag);
}
// Preparing the mime-type
const exts = fileName.split('.').filter(i => i.length > 0);
const extension = `.${exts[exts.length - 1]}`;
const mime = extensions.find(i => i.extension.includes(extension))?.mime;
// Check if the download option is triggered or not
if (options && 'download' in options) {
if (typeof options.download !== 'boolean') {
throw new TypeError(`The download property should be a boolean value, but instead got ${typeof options.download}`);
}
if (options.download === true) {
this.setHeader('Content-Type', 'application/octet-stream');
this.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
}
else {
this.setHeader('Content-Type', mime);
}
}
else {
this.setHeader('Content-Type', mime);
}
// Checking the range settings
if (options && 'acceptRanges' in options) {
if (typeof options.acceptRanges !== 'boolean') {
throw new TypeError(`The acceptRanges option only accepts boolean values, but instead got ${typeof options.acceptRanges}`);
}
const range = this.req.headers.range;
// Check if the request has ranges
if (range) {
// Function to parse the Range header
const parseRangeHeader = (range, size) => {
const [start, end] = range.replace(/bytes=/, '').split('-');
const parsedStart = parseInt(start, 10);
const parsedEnd = parseInt(end, 10);
const validStart = isNaN(parsedStart) ? 0 : Math.max(0, parsedStart);
const validEnd = isNaN(parsedEnd) ? size - 1 : Math.min(size - 1, parsedEnd);
return [validStart, validEnd];
};
const totalSize = stats.size;
const [start, end] = parseRangeHeader(range, totalSize);
const chunkSize = (end - start) + 1;
this.status(206);
this.setHeader('Content-Range', `bytes ${start}-${end}/${totalSize}`);
this.setHeader('Accept-Ranges', 'bytes');
this.setHeader('Content-Length', chunkSize);
const fileStream = fs.createReadStream(filePath, { start, end });
return fileStream.pipe(this.#_res);
}
}
this.status(200).setHeader('Content-Length', stats.size);
const fileStream = fs.createReadStream(filePath);
return fileStream.pipe(this.#_res);
}
catch (error) {
if (options && 'serverErrorFile' in options) {
const errValidity = helpers.checkPathAccessibility(options.serverErrorFile);
if (!errValidity.valid) {
if (errValidity.errors.notString) {
throw new Error(`The serverErrorFile path should be a string, instead got ${typeof options.serverErrorFile}`);
}
if (errValidity.errors.doesntExist) {
throw new Error(`The serverErrorFile path (${options.serverErrorFile}) doesn't exist`);
}
if (errValidity.errors.notAccessible) {
throw new Error(`You don't have enough permissions to access the errValidity path: ${options.serverErrorFile}`);
}
}
if (!options.serverErrorFile?.toLowerCase().startsWith(root.toLowerCase())) {
throw new RangeError(`The not 500 file (${options.serverErrorFile}) is not in your root directory.`);
}
this.setHeader('Content-Type', 'text/html');
this.write({ chunk: fs.readFileSync(options.serverErrorFile) });
console.error(error);
this.status(500).end();
return;
}
if (error instanceof Error) {
error.message = `Unable to send file: ${error.message}`;
}
throw error;
}
}
/**
* Send a response.
*
* Examples:
* @example
* // Send buffer
* response.send(Buffer.from('wahoo'));
* // Send JSON
* response.send({ some: 'json' });
* //Send HTML content
* response.send('<p>some html</p>');
* // Sending plain text
* response.status(404).send('Sorry, cant find that');
* // Sending a file
* const fs = require('fs');
* response.status(200).send(fs.readFileSync('./style.css', { encoding: 'utf8' }), 'text/css');
* @param {string|object|Buffer} data The data to be sent
* @param {MimeType} [contentType] Specify the type of content
*/
send(data, contentType) {
let type = null;
if (typeof data === 'string') {
if (typeof contentType === 'string' && mimes.includes(contentType.toLowerCase())) {
type = contentType;
}
else if (helpers.is.html(data)) {
type = 'text/html';
}
else {
type = 'text/plain';
}
}
else if (Buffer.isBuffer(data)) {
if (typeof contentType === 'string' && mimes.includes(contentType.toLowerCase())) {
type = contentType;
}
else {
type = 'application/octet-stream';
}
}
else if (Array.isArray(data) || (typeof data === 'object' && data !== null)) {
data = JSON.stringify(data);
if (typeof contentType === 'string' && mimes.includes(contentType.toLowerCase())) {
type = contentType;
}
else {
type = 'application/json';
}
}
else {
throw new TypeError(`${typeof data} is not a valid data type. Expected an Object, String, or Buffer, but instead got ${typeof data}`);
}
this.setHeader('Content-Type', type);
this.write({ chunk: data });
return this.end();
}
/**
* Send JSON response.
* *Examples:*
* @example
* response.json(null);
* response.json({ user: 'tj' });
* response.status(500).json('oh noes!');
* response.status(404).json('I dont have that');
* @param data
*/
json(data) {
const chunk = Array.isArray(data) || helpers.is.realObject(data) ? JSON.stringify(data) : String(data);
this.setHeader('Content-Type', 'application/json');
this.write({ chunk });
return this.end();
}
/**
* When using implicit headers (not calling `response.writeHead()` explicitly),
* this method controls the status code that will be sent to the client when
* the headers get flushed.
*
* ```js
* response.status(404);
* ```
* @param {number} statusCode The status code of the request
* @returns {this}
*/
status(statusCode) {
try {
this.statusCode = statusCode;
}
catch (error) {
throw error;
}
return this;
}
/**
* Add an event handler
* @param {EventConfig} config
*/
addListener(config) {
const events = ['close', 'drain', 'error', 'finish', 'pipe', 'unpipe'];
if (events.includes(config?.event)) {
throw `${config.event} is not a valid response event`;
}
if (typeof config?.listener !== 'function') {
throw 'The event listener must be a function';
}
this.#_res.addListener(config.event, config.listener);
return this;
}
/**
* Returns a copy of the array of listeners for the event named eventName.
*
* ```js
* server.on('connection', (stream) => {
* console.log('someone connected!');
* });
*
* console.log(util.inspect(server.listeners('connection')));
* // Prints: [ [Function] ]
* ```
* @param {string|symbol} eventName The event name
*/
listeners(eventName) {
return this.#_res.listeners(eventName);
}
/**
* This method adds HTTP trailing headers (a header but at the end of the message) to the response.
*
* Attempting to set a header field name or value that contains invalid characters will result in a ```TypeError``` being thrown.
* @param {http2.OutgoingHttpHeaders} trailers
*/
addTrailers(trailers) {
this.#_res.addTrailers(trailers);
}
/**
* This method signals to the server that all of
* the response headers and body have been sent;
* that server should consider this message complete.
* The method, ```response.end()```, MUST be called on each response.
*
* If data is specified, it is equivalent to calling
* ```response.write(data, encoding)``` followed by ```response.end(callback)```.
*
* If ```callback``` is specified, it will be called when the response stream is finished.
* @param {ResponseEndOptions} [options] End stream options
* @returns {this}
*/
end(options) {
if (helpers.is.undefined(options) || helpers.isNot.realObject(options)) {
this.#_res.end();
return this;
}
const params = {
data: options && 'data' in options && options.data ? options.data : null,
callback: options && 'callback' in options && typeof options.callback === 'function' ? options.callback : null,
encoding: options && 'encoding' in options && typeof options.encoding === 'string' && this.#_encodings.includes(options.encoding) ? options.encoding : null
};
if (params.data) {
if (params.encoding && params.callback) {
this.#_res.end(params.data, params.encoding, params.callback);
}
else if (params.callback) {
this.#_res.end(params.data, params.callback);
}
else {
this.#_res.end(params.data);
}
}
else if (params.callback) {
this.#_res.end(params.callback);
}
else {
this.#_res.end();
}
return this;
}
/**
* Reads out a header that has already been queued but not sent to the client. The name is case-insensitive.
*
* ```js
* const contentType = response.getHeader('content-type');
* ```
* @param {string} name The header name
* @returns {string} The header value
*/
getHeader(name) {
return this.#_res.getHeader(name);
}
/**
* Returns an array containing the unique names of the current outgoing headers. All header names are lowercase.
*
* ```js
* response.setHeader('Foo', 'bar');
* response.setHeader('Set-Cookie', ['foo=bar', 'bar=baz']);
*
* const headerNames = response.getHeaderNames();
* // headerNames === ['foo', 'set-cookie']
* ```
* @returns {string[]} The names of the provided headers
*/
getHeaderNames() {
return this.#_res.getHeaderNames();
}
/**
* Returns a shallow copy of the current outgoing headers.
* Since a shallow copy is used, array values may be mutated
* without additional calls to various header-related http
* module methods. The keys of the returned object are the
* header names and the values are the respective header values.
* All header names are lowercase.
*
* The object returned by the ```response.getHeaders()``` method *does
* not* prototypically inherit from the JavaScript ```Object```. This means
* that typicalObject methods such as ```obj.toString()```, ```obj.hasOwnProperty()```,
* and others are not defined and *will not work*.
* @returns {http2.OutgoingHttpHeaders}
*/
getHeaders() {
return this.#_res.getHeaders();
}
/**
* Returns ```true``` if the header identified by name is currently
* set in the outgoing hea