├── .gitignore ├── .gitmodules ├── .npmignore ├── .prettierrc ├── LICENSE ├── README.md ├── benchmarks ├── benchmark.sh ├── configuration.json ├── index.js ├── package-lock.json ├── package.json ├── scenarios │ └── simple_html_page.js ├── setup │ ├── express.js │ ├── fastify.js │ ├── hyperexpress.js │ ├── nanoexpress.js │ └── uwebsockets.js └── utils.js ├── docs ├── Benchmarks.md ├── Examples.md ├── HostManager.md ├── LiveDirectory.md ├── Middlewares.md ├── MultipartField.md ├── Request.md ├── Response.md ├── Router.md ├── SSEventStream.md ├── Server.md ├── Sessions.md └── Websocket.md ├── index.js ├── package-lock.json ├── package.json ├── src ├── components │ ├── Server.js │ ├── compatibility │ │ ├── ExpressRequest.js │ │ ├── ExpressResponse.js │ │ ├── NodeRequest.js │ │ └── NodeResponse.js │ ├── http │ │ ├── Request.js │ │ └── Response.js │ ├── plugins │ │ ├── HostManager.js │ │ ├── LiveFile.js │ │ ├── MultipartField.js │ │ └── SSEventStream.js │ ├── router │ │ ├── Route.js │ │ └── Router.js │ └── ws │ │ ├── Websocket.js │ │ └── WebsocketRoute.js └── shared │ └── operators.js ├── tests ├── components │ ├── Server.js │ ├── features │ │ ├── HostManager.js │ │ └── LiveFile.js │ ├── http │ │ ├── Request.js │ │ ├── Response.js │ │ └── scenarios │ │ │ ├── middleware_double_iteration.js │ │ │ ├── middleware_dynamic_iteration.js │ │ │ ├── middleware_execution_order.js │ │ │ ├── middleware_iteration_error.js │ │ │ ├── middleware_layered_iteration.js │ │ │ ├── middleware_uncaught_async_error.js │ │ │ ├── request_body_echo_test.js │ │ │ ├── request_chunked_json.js │ │ │ ├── request_chunked_stream.js │ │ │ ├── request_multipart.js │ │ │ ├── request_router_paths_test.js │ │ │ ├── request_stream.js │ │ │ ├── request_uncaught_rejections.js │ │ │ ├── response_chunked_write.js │ │ │ ├── response_custom_content_length.js │ │ │ ├── response_custom_status.js │ │ │ ├── response_headers_behavior.js │ │ │ ├── response_hooks.js │ │ │ ├── response_piped.js │ │ │ ├── response_send_no_body.js │ │ │ ├── response_send_status.js │ │ │ ├── response_set_header.js │ │ │ ├── response_sse.js │ │ │ ├── response_stream.js │ │ │ └── response_stream_sync_writes.js │ ├── router │ │ ├── Router.js │ │ └── scenarios │ │ │ └── chainable_routes.js │ └── ws │ │ ├── Websocket.js │ │ ├── WebsocketRoute.js │ │ └── scenarios │ │ ├── stream.js │ │ └── writable.js ├── configuration.js ├── content │ ├── example.txt │ ├── large-image.jpg │ ├── test-body.json │ ├── test.html │ └── written │ │ └── .required ├── index.js ├── local.js ├── middlewares │ ├── hyper-express-body-parser │ │ ├── configuration.json │ │ ├── index.js │ │ └── scenarios │ │ │ ├── parser_compression.js │ │ │ ├── parser_limit.js │ │ │ ├── parser_types.js │ │ │ └── parser_validation.js │ └── hyper-express-session │ │ ├── configuration.json │ │ ├── index.js │ │ ├── scenarios │ │ ├── brute.js │ │ ├── duration.js │ │ ├── properties.js │ │ ├── roll.js │ │ └── visits.js │ │ └── test_engine.js ├── package-lock.json ├── package.json ├── performance.js ├── scripts │ ├── MemoryStore.js │ └── operators.js ├── ssl │ ├── dummy-cert.pem │ └── dummy-key.pem └── types │ └── Router.ts ├── tsconfig.json └── types ├── components ├── Server.d.ts ├── http │ ├── Request.d.ts │ └── Response.d.ts ├── middleware │ ├── MiddlewareHandler.d.ts │ └── MiddlewareNext.d.ts ├── plugins │ ├── HostManager.d.ts │ ├── LiveFile.d.ts │ ├── MultipartField.d.ts │ └── SSEventStream.d.ts ├── router │ ├── Route.d.ts │ └── Router.d.ts └── ws │ ├── Websocket.d.ts │ └── WebsocketRoute.d.ts ├── index.d.ts └── shared └── operators.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .npmrc 2 | .vscode/ 3 | node_modules/ 4 | tests/content/large-files/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "middlewares/hyper-express-session"] 2 | path = middlewares/hyper-express-session 3 | url = git@github.com:kartikk221/hyper-express-session.git 4 | [submodule "middlewares/hyper-express-body-parser"] 5 | path = middlewares/hyper-express-body-parser 6 | url = git@github.com:kartikk221/hyper-express-body-parser.git 7 | [submodule "middlewares/hyper-express-serve-static"] 8 | path = middlewares/hyper-express-serve-static 9 | url = git@github.com:kartikk221/hyper-express-serve-static.git 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tests/ 2 | benchmarks/ 3 | middlewares/ 4 | docs/ 5 | .npmrc -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 4, 4 | "printWidth": 120, 5 | "singleQuote": true, 6 | "bracketSpacing": true 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2021-2021 Kartik Kumar <@kartikk221> 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HyperExpress: High Performance Node.js Webserver 2 | #### Powered by [`uWebSockets.js`](https://github.com/uNetworking/uWebSockets.js/) 3 | 4 |
5 | 6 | [![NPM version](https://img.shields.io/npm/v/hyper-express.svg?style=flat)](https://www.npmjs.com/package/hyper-express) 7 | [![NPM downloads](https://img.shields.io/npm/dm/hyper-express.svg?style=flat)](https://www.npmjs.com/package/hyper-express) 8 | [![GitHub issues](https://img.shields.io/github/issues/kartikk221/hyper-express)](https://github.com/kartikk221/hyper-express/issues) 9 | [![GitHub stars](https://img.shields.io/github/stars/kartikk221/hyper-express)](https://github.com/kartikk221/hyper-express/stargazers) 10 | [![GitHub license](https://img.shields.io/github/license/kartikk221/hyper-express)](https://github.com/kartikk221/hyper-express/blob/master/LICENSE) 11 | 12 |
13 | 14 | ## Motivation 15 | HyperExpress aims to be a simple yet performant HTTP & Websocket Server. Combined with the power of uWebsockets.js, a Node.js binding of uSockets written in C++, HyperExpress allows developers to unlock higher throughput for their web applications with their existing hardware. This can allow many web applications to become much more performant on optimized data serving endpoints without having to scale hardware. 16 | 17 | Some of the prominent highlights are: 18 | - Simplified HTTP & Websocket API 19 | - Server-Sent Events Support 20 | - Multipart File Uploading Support 21 | - Modular Routers & Middlewares Support 22 | - Multiple Host/Domain Support Over SSL 23 | - Limited Express.js API Compatibility Through Shared Methods/Properties 24 | 25 | See [`> [Benchmarks]`](https://web-frameworks-benchmark.netlify.app/result?l=javascript) for **performance metrics** against other webservers in real world deployments. 26 | 27 | ## Documentation 28 | HyperExpress **supports** the latest three LTS (Long-term Support) Node.js versions only and can be installed using Node Package Manager (`npm`). 29 | ``` 30 | npm i hyper-express 31 | ``` 32 | 33 | - See [`> [Examples & Snippets]`](./docs/Examples.md) for small and **easy-to-use snippets** with HyperExpress. 34 | - See [`> [Server]`](./docs/Server.md) for creating a webserver and working with the **Server** component. 35 | - See [`> [Router]`](./docs/Router.md) for working with the modular **Router** component. 36 | - See [`> [Request]`](./docs/Request.md) for working with the **Request** component made available through handlers. 37 | - See [`> [Response]`](./docs/Response.md) for working with the **Response** component made available through handlers. 38 | - See [`> [Websocket]`](./docs/Websocket.md) for working with **Websockets** in HyperExpress. 39 | - See [`> [Middlewares]`](./docs/Middlewares.md) for working with global and route-specific **Middlewares** in HyperExpress. 40 | - See [`> [SSEventStream]`](./docs/SSEventStream.md) for working with **Server-Sent Events** based streaming in HyperExpress. 41 | - See [`> [MultipartField]`](./docs/MultipartField.md) for working with multipart requests and **File Uploading** in HyperExpress. 42 | - See [`> [SessionEngine]`](https://github.com/kartikk221/hyper-express-session) for working with cookie based web **Sessions** in HyperExpress. 43 | - See [`> [LiveDirectory]`](./docs/LiveDirectory.md) for implementing **static file/asset** serving functionality into HyperExpress. 44 | - See [`> [HostManager]`](./docs/HostManager.md) for supporting requests over **muliple hostnames** in HyperExpress. 45 | 46 | ## Encountering Problems? 47 | - HyperExpress is mostly compatible with `Express` but not **100%** therefore you may encounter some middlewares not working out of the box. In this scenario, you must either write your own polyfill or omit the middleware to continue. 48 | - The uWebsockets.js version header is disabled by default. You may opt-out of this behavior by setting an environment variable called `KEEP_UWS_HEADER` to a truthy value such as `1` or `true`. 49 | - Still having problems? Open an [`> [Issue]`](https://github.com/kartikk221/hyper-express/issues) with details about what led up to the problem including error traces, route information etc etc. 50 | 51 | ## Testing Changes 52 | To run HyperExpress functionality tests locally on your machine, you must follow the steps below. 53 | 1. Clone the HyperExpress repository to your machine. 54 | 2. Initialize and pull any submodule(s) which are used throughout the tests. 55 | 3. Run `npm install` in the root directory. 56 | 4. Run `npm install` in the `/tests` directory. 57 | 5. Run `npm test` to run all tests with your local changes. 58 | 59 | ## License 60 | [MIT](./LICENSE) 61 | -------------------------------------------------------------------------------- /benchmarks/benchmark.sh: -------------------------------------------------------------------------------- 1 | # Define Execution Variables 2 | HOST="localhost"; 3 | PORT_START=3000; 4 | PORT_END=3004; 5 | NUM_OF_CONNECTIONS=2500; 6 | DURATION_SECONDS=30; 7 | PIPELINE_FACTOR=4; 8 | 9 | # Ensure "autocannon" is not installed, install it with NPM 10 | if ! [ -x "$(command -v autocannon)" ]; then 11 | echo 'Error: autocannon is not installed. Attempting to install with NPM.'; 12 | npm install autocannon -g; 13 | fi 14 | 15 | # Iterate a for loop from PORT_START to PORT_END 16 | for ((PORT=$PORT_START; PORT<=$PORT_END; PORT++)) 17 | do 18 | # Execute the benchmark 19 | echo "Benchmarking Webserver @ Port: $HOST:$PORT"; 20 | 21 | # Use the autocannon utility to benchmark 22 | autocannon -c $NUM_OF_CONNECTIONS -d $DURATION_SECONDS -p $PIPELINE_FACTOR http://localhost:$PORT/; 23 | 24 | # Append a visual line to separate results 25 | echo "----------------------------------------------------"; 26 | done -------------------------------------------------------------------------------- /benchmarks/configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "hostname": "localhost", 3 | "port_start": 3000, 4 | "multi_core": false 5 | } 6 | -------------------------------------------------------------------------------- /benchmarks/index.js: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | import fs from 'fs'; 3 | import cluster from 'cluster'; 4 | import fetch from 'node-fetch'; 5 | import uWebsocketsJS from 'uWebSockets.js'; 6 | import { log } from './utils.js'; 7 | 8 | // Load the server instances to be benchmarked 9 | import uWebsockets from './setup/uwebsockets.js'; 10 | import NanoExpress from './setup/nanoexpress.js'; 11 | import HyperExpress from './setup/hyperexpress.js'; 12 | import Fastify from './setup/fastify.js'; 13 | import Express from './setup/express.js'; 14 | 15 | // Load the configuration from disk 16 | const configuration = JSON.parse(fs.readFileSync('./configuration.json', 'utf8')); 17 | 18 | // Handle spawning of worker processes from the master process 19 | const numCPUs = configuration.multi_core ? os.cpus().length : 1; 20 | if (numCPUs > 1 && (cluster.isMaster || cluster.isPrimary)) { 21 | for (let i = 0; i < numCPUs; i++) { 22 | cluster.fork(); 23 | } 24 | log(`Forked ${numCPUs} workers for benchmarking on ${os.platform()}`); 25 | } 26 | 27 | // Handle spawning of webservers for each worker process 28 | let uws_socket; 29 | if (numCPUs <= 1 || cluster.isWorker) { 30 | // Perform startup tasks 31 | log('Initializing Webservers...'); 32 | (async () => { 33 | try { 34 | // Remember the initial port for HTTP request checks after all servers are started 35 | const initial_port = configuration.port_start; 36 | 37 | // Initialize the uWebsockets server instance 38 | uws_socket = await new Promise((resolve) => 39 | uWebsockets.listen(configuration.hostname, configuration.port_start, resolve) 40 | ); 41 | log(`uWebsockets.js server listening on port ${configuration.port_start}`); 42 | 43 | // Initialize the NanoExpress server instance 44 | configuration.port_start++; 45 | await HyperExpress.listen(configuration.port_start, configuration.hostname); 46 | log(`HyperExpress server listening on port ${configuration.port_start}`); 47 | 48 | // Initialize the NanoExpress server instance 49 | configuration.port_start++; 50 | await NanoExpress.listen(configuration.port_start); 51 | log(`NanoExpress server listening on port ${configuration.port_start}`); 52 | 53 | // Initialize the Fastify server instance 54 | configuration.port_start++; 55 | Fastify.listen({ port: configuration.port_start, host: configuration.hostname }); 56 | log(`Fastify server listening on port ${configuration.port_start}`); 57 | 58 | // Initialize the Express server instance 59 | configuration.port_start++; 60 | await new Promise((resolve) => Express.listen(configuration.port_start, configuration.hostname, resolve)); 61 | log(`Express.js server listening on port ${configuration.port_start}`); 62 | 63 | // Make HTTP GET requests to all used ports to test the servers 64 | log('Testing each webserver with a HTTP GET request...'); 65 | for (let i = initial_port; i <= configuration.port_start; i++) { 66 | const response = await fetch(`http://localhost:${i}/`); 67 | if (response.status !== 200) 68 | throw new Error(`HTTP request to port ${i} failed with status ${response.status}`); 69 | log(`GET HTTP -> Port ${i} -> Status ${response.status} -> ${response.headers.get('content-type')}`); 70 | } 71 | 72 | log( 73 | 'All webservers are ready to receive request between ports ' + 74 | initial_port + 75 | ' - ' + 76 | configuration.port_start + 77 | '!', 78 | false 79 | ); 80 | } catch (error) { 81 | console.log(error); 82 | process.exit(); 83 | } 84 | })(); 85 | } 86 | 87 | ['exit', 'SIGINT', 'SIGUSR1', 'SIGUSR2', 'SIGTERM'].forEach((type) => 88 | process.once(type, () => { 89 | // Close all the webserver instances 90 | try { 91 | if (uws_socket) uWebsocketsJS.us_listen_socket_close(uws_socket); 92 | NanoExpress.close(); 93 | HyperExpress.close(); 94 | Fastify.close(); 95 | } catch (error) { 96 | console.log(error); 97 | } 98 | 99 | // Exit the process 100 | process.exit(); 101 | }) 102 | ); 103 | -------------------------------------------------------------------------------- /benchmarks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "benchmarks", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node index.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "express": "^5.0.1", 15 | "fastify": "^5.0.0", 16 | "nanoexpress": "^6.4.4", 17 | "node-fetch": "^3.3.2", 18 | "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.49.0" 19 | } 20 | } -------------------------------------------------------------------------------- /benchmarks/scenarios/simple_html_page.js: -------------------------------------------------------------------------------- 1 | export function get_simple_html_page({ server_name }) { 2 | const date = new Date(); 3 | return { 4 | status: 200, 5 | headers: { 6 | 'unix-ms-ts': date.getTime().toString(), 7 | 'cache-control': 'no-cache', 8 | 'content-type': 'text/html; charset=utf-8', 9 | 'server-name': server_name, 10 | }, 11 | body: ` 12 | 13 | 14 | Welcome @ ${date.toLocaleDateString()} 15 | 16 | 17 |

This is a simple HTML page.

18 |

This page was rendered at ${date.toLocaleString()} and delivered using '${server_name}' webserver.

19 | 20 | 21 | `, 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /benchmarks/setup/express.js: -------------------------------------------------------------------------------- 1 | import Express from 'express'; 2 | import { get_simple_html_page } from '../scenarios/simple_html_page.js'; 3 | 4 | // Initialize the Express app instance 5 | const app = Express(); 6 | 7 | // Bind the 'simple_html_page' scenario route 8 | app.get('/', (request, response) => { 9 | // Generate the scenario payload 10 | const { status, headers, body } = get_simple_html_page({ server_name: 'Express.js' }); 11 | 12 | // Write the status and headers 13 | response.status(status); 14 | Object.keys(headers).forEach((header) => response.header(header, headers[header])); 15 | 16 | // Write the body and end the response 17 | return response.send(body); 18 | }); 19 | 20 | export default app; 21 | -------------------------------------------------------------------------------- /benchmarks/setup/fastify.js: -------------------------------------------------------------------------------- 1 | import Fastify from 'fastify'; 2 | import { get_simple_html_page } from '../scenarios/simple_html_page.js'; 3 | 4 | // Initialize the Express app instance 5 | const app = Fastify(); 6 | 7 | // Bind the 'simple_html_page' scenario route 8 | app.get('/', (request, response) => { 9 | // Generate the scenario payload 10 | const { status, headers, body } = get_simple_html_page({ server_name: 'Fastify' }); 11 | 12 | // Write the status and headers 13 | response.status(status); 14 | Object.keys(headers).forEach((header) => response.header(header, headers[header])); 15 | 16 | // Write the body and end the response 17 | return response.send(body); 18 | }); 19 | 20 | export default app; 21 | -------------------------------------------------------------------------------- /benchmarks/setup/hyperexpress.js: -------------------------------------------------------------------------------- 1 | import HyperExpress from '../../index.js'; 2 | import { get_simple_html_page } from '../scenarios/simple_html_page.js'; 3 | 4 | // Initialize the Express app instance 5 | const app = new HyperExpress.Server(); 6 | 7 | // Generate the scenario payload 8 | const { status, headers, body } = get_simple_html_page({ server_name: 'HyperExpress' }); 9 | 10 | // Bind the 'simple_html_page' scenario route 11 | app.get('/', (request, response) => { 12 | // Write the status and headers 13 | response.status(status); 14 | 15 | for (const key in headers) response.header(key, headers[key]); 16 | 17 | // Write the body and end the response 18 | response.send(body); 19 | }); 20 | 21 | export default app; 22 | -------------------------------------------------------------------------------- /benchmarks/setup/nanoexpress.js: -------------------------------------------------------------------------------- 1 | import NanoExpress from 'nanoexpress'; 2 | import { get_simple_html_page } from '../scenarios/simple_html_page.js'; 3 | 4 | // Initialize the Express app instance 5 | const app = NanoExpress(); 6 | 7 | // Bind the 'simple_html_page' scenario route 8 | app.get('/', (request, response) => { 9 | // Generate the scenario payload 10 | const { status, headers, body } = get_simple_html_page({ server_name: 'NanoExpress' }); 11 | 12 | // Write the status and headers 13 | response.status(status); 14 | Object.keys(headers).forEach((header) => response.header(header, headers[header])); 15 | 16 | // Write the body and end the response 17 | return response.send(body); 18 | }); 19 | 20 | export default app; 21 | -------------------------------------------------------------------------------- /benchmarks/setup/uwebsockets.js: -------------------------------------------------------------------------------- 1 | import uWebsockets from 'uWebSockets.js'; 2 | import { get_simple_html_page } from '../scenarios/simple_html_page.js'; 3 | 4 | // Initialize an app instance which will be used to create the server 5 | const app = uWebsockets.App(); 6 | 7 | // Bind the 'simple_html_page' scenario route 8 | app.get('/', (response, request) => { 9 | // Generate the scenario payload 10 | const { status, headers, body } = get_simple_html_page({ server_name: 'uWebSockets.js' }); 11 | 12 | // Write the status and headers 13 | response.writeStatus(`${status} OK`); 14 | Object.keys(headers).forEach((header) => response.writeHeader(header, headers[header])); 15 | 16 | // Write the body and end the response 17 | return response.end(body); 18 | }); 19 | 20 | export default app; 21 | -------------------------------------------------------------------------------- /benchmarks/utils.js: -------------------------------------------------------------------------------- 1 | import cluster from 'cluster'; 2 | 3 | /** 4 | * Logs a message to the console. 5 | * Will only log if the current process is a worker and primary_only is set to false. 6 | * 7 | * @param {String} message 8 | * @param {Boolean} [primary_only=true] 9 | * @returns 10 | */ 11 | export function log(message, primary_only = true) { 12 | if (primary_only && cluster.isWorker) return; 13 | console.log(`[${process.pid}] ${message}`); 14 | } 15 | 16 | /** 17 | * Returns a Promise which is resolved after the given number of milliseconds. 18 | * 19 | * @param {Number} ms 20 | * @returns {Promise} 21 | */ 22 | export function async_wait(ms) { 23 | return new Promise((resolve) => setTimeout(resolve, ms)); 24 | } 25 | -------------------------------------------------------------------------------- /docs/Benchmarks.md: -------------------------------------------------------------------------------- 1 | ## Benchmarks 2 | Below benchmark results were derived using the **[autocannon](https://www.npmjs.com/package/autocannon)** HTTP benchmarking utility. The benchmark source code is included in this repository in the benchmarks folder. 3 | 4 | #### CLI Command 5 | This command simulates a high stress situation where **2500 unique visitors** visit your website at the same time and their browsers on average make **4 pipelined requests** per TCP connection sustained for **30 seconds**. 6 | ``` 7 | autocannon -c 2500 -d 30 -p 4 http://HOST:PORT/ 8 | ``` 9 | 10 | ### Environment Specifications 11 | * __Machine:__ Ubuntu 21.04 | **1 vCPU** | **1GB Mem** | 32GB Nvme | **Vultr Compute Instance @ $6/Month** 12 | * __Node:__ `v18.0.0` 13 | * __Method:__ Two rounds; one to warm-up, one to measure 14 | * __Response Body:__ Small HTML page with a dynamic timestamp generated with `Date`. See more in [HTML Test](../benchmarks/scenarios/simple_html_page.js). 15 | * __Linux Optimizations:__ None. 16 | 17 | ### Benchmark Results 18 | **Note!** these benchmarks should be **run over network for proper results** as running these benchmarks on localhost significantly strains the C++ to Javascript communications and class initializations performance due to near **no latency** in request times which is **unrealistic for real world scenarios**. 19 | 20 | | | Version | Requests/s | Latency | Throughput/s | 21 | | :-- | --: | :-: | --: | --: | 22 | | uWebsockets.js | 20.8.0 | 196,544 | 464 ms | 102 Mb/s | 23 | | HyperExpress | 6.0.0 | 195,832 | 469 ms | 101 Mb/s | 24 | | Fastify | 3.28.0 | 13,329 | 746 ms | 8 Mb/s | 25 | | Express | 4.17.3 | 5,608 | 1821 ms | 3.7 Mb/s | 26 | 27 | ### Running Benchmarks 28 | To run benchmarks in your own environments, you may follow the steps below. 29 | 1. Clone the HyperExpress repository to your machine. 30 | 2. Run `npm install` in the root of the HyperExpress directory. 31 | 3. Run the `index.js` file in the `/benchmarks` directory to start all webservers on neighboring ports. 32 | 4. You may run the `autocannon` command provided above yourself manually from any remote instance or you may customize and run the `benchmarks.sh` file to receive benchmark results for each webserver. -------------------------------------------------------------------------------- /docs/HostManager.md: -------------------------------------------------------------------------------- 1 | # HostManager 2 | Below is a breakdown of the `HostManager` object made available through the `Server.hosts` property allowing for support of multiple hostnames with their own SSL configurations. 3 | 4 | #### Working With A HostManager 5 | 6 | ```javascript 7 | // Let's support example.com with it's own SSL configuration 8 | server.hosts.add('example.com', { 9 | cert_file_name: 'path/to/example.com/cert', 10 | key_file_name: 'path/to/example.com/key' 11 | }); 12 | 13 | // Bind a handler which is called on requests that do not come from a supported hostname 14 | server.hosts.on('missing', (hostname) => { 15 | // Note! This event handler should be treated synchronously only 16 | // uWebsockets.js expects you to register an appropriate handler for the incoming request in this synchronous execution 17 | switch (hostname) { 18 | case 'example2.com': 19 | return server.hosts.add(hostname, { 20 | cert_file_name: 'path/to/example2.com/cert', 21 | key_file_name: 'path/to/example2.com/key' 22 | }); 23 | } 24 | }); 25 | ``` 26 | 27 | #### HostManager Properties 28 | | Property | Type | Description | 29 | | :-------- | :------- | :------------------------- | 30 | | `registered` | `Object` | All of the registered host configurations. | 31 | 32 | #### HostManager Methods 33 | * `add(hostname: string, options: HostOptions)`: Registers the unique host options to use for the specified hostname for incoming requests. 34 | * **Returns** the self `HostManager` instance. 35 | * `HostOptions`: 36 | * `passphrase`[`String`]: Strong passphrase for SSL cryptographic purposes. 37 | * `cert_file_name`[`String`]: Path to SSL certificate file to be used for SSL/TLS. 38 | * `key_file_name`[`String`]: Path to SSL private key file to be used for SSL/TLS. 39 | * `dh_params_file_name`[`String`]: Path to file containing Diffie-Hellman parameters. 40 | * `ssl_prefer_low_memory_usage`[`Boolean`]: Whether to prefer low memory usage over high performance. 41 | * `remove(hostname: string)`: Un-Registers the unique host options to use for the specified hostname for incoming requests. 42 | * **Returns** the self `HostManager` instance. -------------------------------------------------------------------------------- /docs/LiveDirectory.md: -------------------------------------------------------------------------------- 1 | # LiveDirectory 2 | Below is a simple guide on implementing static serving functionality to HyperExpress while maintaining high performance. 3 | 4 | #### Why LiveDirectory? 5 | LiveDirectory loads files from the specified path into memory and watches them for updates allowing for instantaneous changes. This is desirable for both development and production environments as we do not have to wait on any I/O operation when serving assets. Each request will serve the most updated file content from memory allowing for high performance and throughput without any bottlenecks. 6 | - See [`> [LiveDirectory]`](https://github.com/kartikk221/live-directory) for all available properties, methods and documentation on this package. 7 | 8 | #### Getting Started 9 | Please install [`live-directory`](https://www.npmjs.com/package/live-directory) using the `npm` package manager. 10 | ```js 11 | npm i live-directory 12 | ``` 13 | 14 | #### Creating A Static Serve Route 15 | ```js 16 | const HyperExpress = require('hyper-express'); 17 | const LiveDirectory = require('live-directory'); 18 | const Server = new HyperExpress.Server(); 19 | 20 | // Create a LiveDirectory instance to virtualize directory with our assets 21 | // Specify the "path" of the directory we want to consume using this instance as the first argument 22 | const LiveAssets = new LiveDirectory('/var/www/website/files', { 23 | // Optional: Configure filters to ignore or include certain files, names, extensions etc etc. 24 | filter: { 25 | keep: { 26 | // Something like below can be used to only serve images, css, js, json files aka. most common web assets ONLY 27 | extensions: ['css', 'js', 'json', 'png', 'jpg', 'jpeg'] 28 | }, 29 | ignore: (path) => { 30 | // You can define a function to perform any kind of matching on the path of each file being considered by LiveDirectory 31 | // For example, the below is a simple dot-file ignore match which will prevent any files starting with a dot from being loaded into live-directory 32 | return path.startsWith('.'); 33 | }, 34 | } 35 | 36 | // Optional: You can customize how LiveDirectory caches content under the hood 37 | cache: { 38 | // The parameters below can be tuned to control the total size of the cache and the type of files which will be cached based on file size 39 | // For example, the below configuration (default) should cache most <1 MB assets but will not cache any larger assets that may use a lot of memory 40 | // In the scenario that LiveDirectory encounters an uncached file, It will s 41 | max_file_count: 250, // Files will only be cached up to 250 MB of memory usage 42 | max_file_size: 1024 * 1024, // All files under 1 MB will be cached 43 | }, 44 | }); 45 | 46 | // Create static serve route to serve frontend assets 47 | Server.get('/assets/*', (request, response) => { 48 | // Strip away '/assets' from the request path to get asset relative path 49 | // Lookup LiveFile instance from our LiveDirectory instance. 50 | const path = request.path.replace('/assets', ''); 51 | const file = LiveAssets.get(path); 52 | 53 | // Return a 404 if no asset/file exists on the derived path 54 | if (file === undefined) return response.status(404).send(); 55 | 56 | const fileParts = file.path.split("."); 57 | const extension = fileParts[fileParts.length - 1]; 58 | 59 | // Retrieve the file content and serve it depending on the type of content available for this file 60 | const content = file.content; 61 | if (content instanceof Buffer) { 62 | // Set appropriate mime-type and serve file content Buffer as response body (This means that the file content was cached in memory) 63 | return response.type(extension).send(content); 64 | } else { 65 | // Set the type and stream the content as the response body (This means that the file content was NOT cached in memory) 66 | return response.type(extension).stream(content); 67 | } 68 | }); 69 | 70 | // Some examples of how the above route will map & serve requests: 71 | // [GET /assets/images/logo.png] >> [/var/www/website/files/images/logo.png] 72 | // [GET /assets/js/index.js] >> [/var/www/website/files/js/index.js] 73 | ``` 74 | 75 | #### Is This Secure? 76 | LiveDirectory will traverse through all sub-directories and files from your specified path ahead of time and load its files into memory during startup. Due to this, we do not perform any file system operations for any `get()` calls making things performant. This eliminates any path manipulation vulnerability that may allow a bad actor to access sensitive files on your hardware as only files that loaded into memory are served. You can inspect the `LiveDirectory.files` property at any time to confirm which file paths are being loaded by `LiveDirectory` to detect any unintended files. 77 | -------------------------------------------------------------------------------- /docs/Middlewares.md: -------------------------------------------------------------------------------- 1 | # Middlewares 2 | HyperExpress follows the standard format of middlewares and implements similar API to ExpressJS. This allows for limited compatibility with some existing ExpressJS middlewares while maintaining high performance. 3 | * See [`> [Server]`](./Server.md) and [`> [Router]`](./Router.md) for details about the `use()` method and parameter types. 4 | 5 | # How To Use 6 | Middlewares support both callback and promise based iteration similar to ExpressJS. Throwing or iterating `next` with an `Error` object will trigger the global error handler. 7 | 8 | #### Callback-Based Iteration 9 | ```javascript 10 | // Binds a midddleware that will run on all routes that begin with '/api' in this router. 11 | router.use('/api', (request, response, next) => { 12 | some_async_operation(request, response) 13 | .then(() => next()) // Calling next() will trigger iteration to the next middleware 14 | .catch((error) => next(error)) // passing an Error as a parameter will automatically trigger global error handler 15 | }); 16 | ``` 17 | 18 | #### Async/Promise-Based Iteration 19 | ```javascript 20 | // Binds a global middleware that will run on all routes. 21 | server.use(async (request, response) => { 22 | // You can also just return new Promise((resolve, reject) => {}); instead of async callback 23 | try { 24 | await some_async_operation(); 25 | // The request proceeds to the next middleware/handler after the promise resolves 26 | } catch (error) { 27 | return error; // This will trigger global error handler as we are returning an Error 28 | } 29 | }); 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/MultipartField.md: -------------------------------------------------------------------------------- 1 | # MultipartField 2 | Below is a breakdown of the `MultipartField` object made available through the `Request.multipart()` handler provided when parsing multipart forms and accepting file uploads. 3 | 4 | #### Working With A Multipart Field 5 | The `MultipartField` component is meant to be an abstraction that explains each incoming field from a multipart form request. This component differentiates between text-type fields and file-type fields by populating the `file` property only when the field is a file-type. 6 | 7 | See below for an example profile image file upload scenario: 8 | ```javascript 9 | const FileSystem = require('fs'); 10 | webserver.post('/profile/image/upload', async (request, response) => { 11 | // Ensure the user is signed in and retrieve their acccount id 12 | // We use the hyper-express-session middleware here 13 | await request.session.start(); 14 | const account_id = request.session.get('account_id'); 15 | if (account_id === undefined) return response.status(403).send('You must be logged in to use this endpoint.'); 16 | 17 | // Begin parsing this request as a multipart request 18 | let save_path; 19 | try { 20 | await request.multipart(async (field) => { 21 | // Ensure that this field is a file-type 22 | // You may also perform your own checks on the encoding and mime type as needed 23 | if (field.file) { 24 | // Save the file to the profile images folder 25 | save_path = `./storage/images/user-image-${account_id}.jpg`; 26 | 27 | // Use an await while writing to ensure the "await request.multipart()" does not continue until this file is done writing 28 | await field.write(save_path); 29 | } 30 | }); 31 | } catch (error) { 32 | // The multipart parser may throw a string constant as an error 33 | // Be sure to handle these as stated in the documentation 34 | if (typeof error === 'FILES_LIMIT_REACHED') { 35 | return response.status(403).send('You sent too many files! Try again.'); 36 | } else { 37 | return response.status(500).send('Oops! An uncaught error occured on our end.'); 38 | } 39 | } 40 | 41 | // Ensure save_path is defined, if it is undefined than that means we did not receive an image. 42 | if (save_path) { 43 | // You may do your own post processing on the image here 44 | save_image_to_database(account_id, save_path); 45 | 46 | // Send a response to the user so they know the image was successfully uploaded 47 | return response.send('Your profile image has been updated!'); 48 | } else { 49 | // We did not receive any image in the multipart request, let the user know 50 | return response.status(400).send('No profile image was received. Please try again.'); 51 | } 52 | }); 53 | ``` 54 | 55 | #### MultipartField Properties 56 | | Property | Type | Description | 57 | | :-------- | :------- | :------------------------- | 58 | | `name` | `String` | Field Name. | 59 | | `encoding`| `String` | Field data encoding. | 60 | | `mime_type`| `String` | Field data mime type. | 61 | | `value`| `String` | Field value (only populated if field is not a file-type).| 62 | | `file`| `Object` | Field file data (only populated if field is a file-type).| 63 | | `file.name`| `String` | File name (only populated if supplied). | 64 | | `file.stream`| `stream.Readable` | Readable stream of file data. | 65 | | `truncated`| `Object` | Field truncations (Only populated if field is not a file-type). | 66 | | `truncated.name`| `Boolean` | Field name was truncated. | 67 | | `truncated.value`| `Boolean` | Field value was truncated. | 68 | 69 | #### MultipartField Methods 70 | * `write(path: String, options?: stream.WritableOptions)`: Writes/Saves file content to the specified path and name. 71 | * **Returns** a `Promise` which is resolved once file writing has completed. 72 | * **Note** this method is **only** available for file-type fields. 73 | * **Note** this method uses the `field.file.stream` stream therefore you will not be able to re-use this field's file stream after running this method. -------------------------------------------------------------------------------- /docs/Request.md: -------------------------------------------------------------------------------- 1 | # Request 2 | Below is a breakdown of the `Request` component which is an extended `Readable` stream matching official Node.js specification. 3 | * See [`> [ExpressJS]`](https://expressjs.com/en/4x/api.html#req) for more information on additional compatibility methods and properties. 4 | * See [`> [Stream.Readable]`](https://nodejs.org/api/stream.html#new-streamreadableoptions) for more information on additional native methods and properties. 5 | 6 | #### Request Properties 7 | | Property | Type | Description | 8 | | :-------- | :------- | :------------------------- | 9 | | `raw` | `uWS.HttpRequest` | The underlying raw uWS Http Request instance. (Unsafe) | 10 | | `app` | `HyperExpress.Server` | HyperExpress Server instance this `Request` originated from. | 11 | | `method` | `String` | Request HTTP method in uppercase. | 12 | | `url` | `String` | path + path_query string. | 13 | | `path` | `String` | Request path without the query.| 14 | | `path_query` | `String` | Request query string without the `?`.| 15 | | `headers` | `Object` | Request Headers from incoming request. | 16 | | `cookies` | `Object` | Request cookies from incoming request. | 17 | | `path_parameters` | `Object` | Path parameters from incoming request. | 18 | | `query_parameters` | `Object` | Query parameters from incoming request. | 19 | | `ip` | `String` | Remote connection IP. | 20 | | `proxy_ip` | `String` | Remote proxy connection IP. | 21 | 22 | #### Request Methods 23 | * `sign(String: string, String: secret)`: Signs provided string with provided secret. 24 | * **Returns** a `String`. 25 | * `unsign(String: signed_value, String: secret)`: Attempts to unsign provided value with provided secret. 26 | * **Returns** `String` or `undefined` if signed value is invalid. 27 | * `buffer()`: Parses body as a Buffer from incoming request. 28 | * **Returns** `Promise` which is then resolved to a `Buffer`. 29 | * `text()`: Parses body as a string from incoming request. 30 | * **Returns** `Promise` which is then resolved to a `String`. 31 | * `urlencoded()`: Parses body as an object from incoming urlencoded body. 32 | * **Returns** `Promise` which is then resolved to an `Object`. 33 | * `json(Any: default_value)`: Parses body as a JSON Object from incoming request. 34 | * **Returns** `Promise` which is then resolved to an `Object` or `typeof default_value`. 35 | * **Note** this method returns the specified `default_value` if JSON parsing fails instead of throwing an exception. To have this method throw an exception, pass `undefined` for `default_value`. 36 | * **Note** `default_value` is `{}` by default meaning `json()` is a safe method even if incoming body is invalid json. 37 | * `multipart(...2 Overloads)`: Parses incoming multipart form based requests allowing for file uploads. 38 | * **Returns** a `Promise` which is **resolved** once **all** of the fields have been processed. 39 | * **Note** you may provide an async `handler` to ensure all fields get executed after each `handler` invocaton has finished. 40 | * **Note** the returnd `Promise` can **reject** with one of the `String` constants below or an uncaught `Error` object. 41 | * `PARTS_LIMIT_REACHED`: This error is rejected when the configured Busboy `limits.parts` limit has been reached. 42 | * `FILES_LIMIT_REACHED`: This error is rejected when the configured Busboy `limits.files` limit has been reached. 43 | * `FIELDS_LIMIT_REACHED`: This error is rejected when the configured Busboy `limits.fields` limit has been reached. 44 | * **Overload Types**: 45 | * `multipart(Function: handler)`: Parses the incoming multipart request with the default Busboy `options` through the specified `handler`. 46 | * `multipart(BusboyConfig: options, Function: handler)`: Parses the incoming multipart request with the spcified `options` through the specified `handler`. 47 | * **Handler Example**: `(field: MultipartField) => { /* Your Code Here */}` 48 | * **Note** this `handler` can be either a synchronous or asynchronous function. 49 | * **Note** HyperExpress will automatically pause and wait for your **async** handler to resolve on **each** field. 50 | * **See** [`> [MultipartField]`](./MultipartField.md) to view all properties and methods available for each multipart field. 51 | * **See** [`> [Busboy]`](https://github.com/mscdex/busboy) to view all customizable `BusboyConfig` options and learn more about the Busboy multipart parser. 52 | * **Note** the body parser uses the global `Server.max_body_length` by default. You can **override** this property on a route by specifying a higher `max_body_length` in the route options when creating that route. 53 | * **Note** HyperExpress currently **does not support** chunked transfer requests. 54 | * See [ExpressJS](https://github.com/expressjs/express) documentation for more properties/methods that are also implemented for compatibility. 55 | 56 | #### Request Events 57 | The `Request` component extends an `EventEmitter`/`Readable` stream meaning your application can listen for the following lifecycle events. 58 | - [`received`]: This event will get emitted when `Request` has completely received all of the incoming body data. 59 | - See the official [`> [stream.Readable]`](https://nodejs.org/api/stream.html#readable-streams) Node.js documentation for more information. -------------------------------------------------------------------------------- /docs/SSEventStream.md: -------------------------------------------------------------------------------- 1 | # SSEventStream 2 | Below is a breakdown of the `SSEventStream` object made available through the `Response.sse` property for requests being eligible for Server-Sent Events based communication. 3 | 4 | #### Working With Server-Sent Events 5 | Server-Sent Events are essentially a HTTP request that stays alive and gradually receives data from the server until disconnection. With this in mind, this functionality is provided through the `Response.sse` property on the Response object. You may not set the HTTP status or write any headers after a `SSEventStream` has been opened on a `Response` object. 6 | 7 | See below for an example of a simple news events endpoint using Server-Sent Events: 8 | ```javascript 9 | const crypto = require('crypto'); 10 | 11 | const sse_streams = {}; 12 | function broadcast_message(message) { 13 | // Send the message to each connection in our connections object 14 | Object.keys(sse_streams).forEach((id) => { 15 | sse_streams[id].send(message); 16 | }) 17 | } 18 | 19 | webserver.get('/news/events', (request, response) => { 20 | // You may perform some authentication here as this is just a normal HTTP GET request 21 | 22 | // Check to ensure that SSE if available for this request 23 | if (response.sse) { 24 | // Looks like we're all good, let's open the stream 25 | response.sse.open(); 26 | // OR you may also send a message which will open the stream automatically 27 | response.sse.send('Some initial message'); 28 | 29 | // Assign a unique identifier to this stream and store it in our broadcast pool 30 | response.sse.id = crypto.randomUUID(); 31 | sse_streams[response.sse.id] = response.sse; 32 | 33 | // Bind a 'close' event handler to cleanup this connection once it disconnects 34 | response.once('close', () => { 35 | // Delete the stream from our broadcast pool 36 | delete sse_streams[response.sse.id] 37 | }); 38 | } else { 39 | // End the response with some kind of error message as this request did not support SSE 40 | response.send('Server-Sent Events Not Supported!'); 41 | } 42 | }); 43 | ``` 44 | 45 | #### SSEventStream Properties 46 | | Property | Type | Description | 47 | | :-------- | :------- | :------------------------- | 48 | | `active` | `Boolean` | Whether this SSE stream is still active. | 49 | 50 | #### SSEventStream Methods 51 | * `open()`: Opens the Server-Sent Events stream. 52 | * **Returns** a `Boolean` which signifies whether this stream was successfully opened or not. 53 | * **Note** this method will automatically be called on your first `send()` if not already called yet. 54 | * `close()`: Closes the Server-Sent Events stream. 55 | * **Returns** a `Boolean` which signifies whether this stream was successfully closed or not. 56 | * `comment(data: string)`: Sends a comment type message to the client that will **NOT** be handled by the client EventSource. 57 | * **Returns** a `Boolean` which signifies whether this comment was successfully sent or not. 58 | * **Note** this can be useful as a keep-alive mechanism if messages might not be sent regularly. 59 | * `send(...3 Overloads)`: Sends a message to the client with the specified custom id, event and data. 60 | * **Overload Types:** 61 | * `send(data: string)`: Sends a message with the specified `data`. 62 | * `send(event: string, data: string)`: Sends a message on the custom `event` with the specified `data`. 63 | * `send(id: string, event: string, data: string)`: Sends a message with a custom `id` on the custom `event` with the specified `data`. 64 | * **Returns** as `Boolean` which signifies whether this message was sent or not. 65 | * **Note** this method will automatically call the `open()` method if not already called yet. 66 | * **Note** messages sent with just the `data` parameter will be handled by `source.onmessage`/`message` event on the client-side `EventSource`. 67 | * **Note** messages sent with both the `event`/`data` parameters will be handled by the appropriate `event` listener on the client-side `EventSource`. -------------------------------------------------------------------------------- /docs/Sessions.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartikk221/hyper-express/d859aacb8802cb6d952b126818750ed036d1a2b3/docs/Sessions.md -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load uWebSockets.js and fundamental Server/Router classes 4 | const uWebsockets = require('uWebSockets.js'); 5 | const Server = require('./src/components/Server.js'); 6 | const Router = require('./src/components/router/Router.js'); 7 | const Request = require('./src/components/http/Request.js'); 8 | const Response = require('./src/components/http/Response.js'); 9 | const LiveFile = require('./src/components/plugins/LiveFile.js'); 10 | const MultipartField = require('./src/components/plugins/MultipartField.js'); 11 | const SSEventStream = require('./src/components/plugins/SSEventStream.js'); 12 | const Websocket = require('./src/components/ws/Websocket.js'); 13 | 14 | // Disable the uWebsockets.js version header if not specified to be kept 15 | if (!process.env['KEEP_UWS_HEADER']) { 16 | try { 17 | uWebsockets._cfg('999999990007'); 18 | } catch (error) {} 19 | } 20 | 21 | // Expose Server and Router classes along with uWebSockets.js constants 22 | module.exports = { 23 | Server, 24 | Router, 25 | Request, 26 | Response, 27 | LiveFile, 28 | MultipartField, 29 | SSEventStream, 30 | Websocket, 31 | compressors: uWebsockets, 32 | express(...args) { return new Server(...args); }, 33 | }; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyper-express", 3 | "version": "6.17.3", 4 | "description": "High performance Node.js webserver with a simple-to-use API powered by uWebsockets.js under the hood.", 5 | "main": "index.js", 6 | "types": "./types/index.d.ts", 7 | "scripts": { 8 | "test": "node tests/index.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+ssh://git@github.com/kartikk221/hyper-express.git" 13 | }, 14 | "keywords": [ 15 | "uws", 16 | "websockets", 17 | "uwebsocketsjs", 18 | "express", 19 | "expressjs", 20 | "fast", 21 | "http-server", 22 | "https-server", 23 | "http", 24 | "https", 25 | "sse", 26 | "events", 27 | "streaming", 28 | "stream", 29 | "upload", 30 | "file", 31 | "multipart", 32 | "ws", 33 | "websocket", 34 | "performance", 35 | "router" 36 | ], 37 | "author": "Kartik Kumar", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/kartikk221/hyper-express/issues" 41 | }, 42 | "homepage": "https://github.com/kartikk221/hyper-express#readme", 43 | "dependencies": { 44 | "busboy": "^1.6.0", 45 | "cookie": "^1.0.1", 46 | "cookie-signature": "^1.2.1", 47 | "mime-types": "^2.1.35", 48 | "negotiator": "^1.0.0", 49 | "range-parser": "^1.2.1", 50 | "type-is": "^1.6.18", 51 | "typed-emitter": "^2.1.0", 52 | "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.51.0" 53 | }, 54 | "devDependencies": { 55 | "@types/busboy": "^1.5.4", 56 | "@types/express": "^5.0.0", 57 | "@types/node": "^22.7.5", 58 | "typescript": "^5.6.3" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/compatibility/ExpressResponse.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class ExpressResponse { 4 | /* Methods */ 5 | append(name, values) { 6 | return this.header(name, values); 7 | } 8 | 9 | setHeader(name, values) { 10 | return this.append(name, values); 11 | } 12 | 13 | writeHeaders(headers) { 14 | Object.keys(headers).forEach((name) => this.header(name, headers[name])); 15 | } 16 | 17 | setHeaders(headers) { 18 | this.writeHeaders(headers); 19 | } 20 | 21 | writeHeaderValues(name, values) { 22 | values.forEach((value) => this.header(name, value)); 23 | } 24 | 25 | getHeader(name) { 26 | return this._headers[name]; 27 | } 28 | 29 | removeHeader(name) { 30 | delete this._headers[name]; 31 | } 32 | 33 | setCookie(name, value, options) { 34 | return this.cookie(name, value, null, options); 35 | } 36 | 37 | hasCookie(name) { 38 | return this._cookies && this._cookies[name] !== undefined; 39 | } 40 | 41 | removeCookie(name) { 42 | return this.cookie(name, null); 43 | } 44 | 45 | clearCookie(name) { 46 | return this.cookie(name, null); 47 | } 48 | 49 | end(data) { 50 | return this.send(data); 51 | } 52 | 53 | format() { 54 | this._throw_unsupported('format()'); 55 | } 56 | 57 | get(name) { 58 | let values = this._headers[name]; 59 | if (values) return values.length == 0 ? values[0] : values; 60 | } 61 | 62 | links(links) { 63 | // Build chunks of links and combine into header spec 64 | let chunks = []; 65 | Object.keys(links).forEach((rel) => { 66 | let url = links[rel]; 67 | chunks.push(`<${url}>; rel="${rel}"`); 68 | }); 69 | 70 | // Write the link header 71 | this.header('link', chunks.join(', ')); 72 | } 73 | 74 | location(path) { 75 | return this.header('location', path); 76 | } 77 | 78 | render() { 79 | this._throw_unsupported('render()'); 80 | } 81 | 82 | sendFile(path) { 83 | return this.file(path); 84 | } 85 | 86 | sendStatus(status_code) { 87 | return this.status(status_code).send(); 88 | } 89 | 90 | set(field, value) { 91 | if (typeof field == 'object') { 92 | const reference = this; 93 | Object.keys(field).forEach((name) => { 94 | let value = field[name]; 95 | reference.header(name, value); 96 | }); 97 | } else { 98 | this.header(field, value); 99 | } 100 | } 101 | 102 | vary(name) { 103 | return this.header('vary', name); 104 | } 105 | 106 | /* Properties */ 107 | get headersSent() { 108 | return this.initiated; 109 | } 110 | } 111 | 112 | module.exports = ExpressResponse; 113 | -------------------------------------------------------------------------------- /src/components/compatibility/NodeRequest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class NodeRequest {} 4 | 5 | module.exports = NodeRequest; 6 | -------------------------------------------------------------------------------- /src/components/compatibility/NodeResponse.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @typedef {Object} NodeResponseTypes 5 | * @property {number} statusCode 6 | * @property {string} statusMessage 7 | */ 8 | class NodeResponse { 9 | /* Properties */ 10 | get statusCode() { 11 | return this._status_code; 12 | } 13 | 14 | set statusCode(value) { 15 | this._status_code = value; 16 | } 17 | 18 | get statusMessage() { 19 | return this._status_message; 20 | } 21 | 22 | set statusMessage(value) { 23 | this._status_message = value; 24 | } 25 | } 26 | 27 | module.exports = NodeResponse; 28 | -------------------------------------------------------------------------------- /src/components/plugins/HostManager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const EventEmitter = require('events'); 3 | 4 | class HostManager extends EventEmitter { 5 | #app; 6 | #hosts = {}; 7 | 8 | constructor(app) { 9 | // Initialize event emitter 10 | super(); 11 | 12 | // Store app reference 13 | this.#app = app; 14 | 15 | // Bind a listener which emits 'missing' events from uWS when a host is not found 16 | this.#app.uws_instance.missingServerName((hostname) => this.emit('missing', hostname)); 17 | } 18 | 19 | /** 20 | * @typedef {Object} HostOptions 21 | * @property {String=} passphrase Strong passphrase for SSL cryptographic purposes. 22 | * @property {String=} cert_file_name Path to SSL certificate file to be used for SSL/TLS. 23 | * @property {String=} key_file_name Path to SSL private key file to be used for SSL/TLS. 24 | * @property {String=} dh_params_file_name Path to file containing Diffie-Hellman parameters. 25 | * @property {Boolean=} ssl_prefer_low_memory_usage Whether to prefer low memory usage over high performance. 26 | */ 27 | 28 | /** 29 | * Registers the unique host options to use for the specified hostname for incoming requests. 30 | * 31 | * @param {String} hostname 32 | * @param {HostOptions} options 33 | * @returns {HostManager} 34 | */ 35 | add(hostname, options) { 36 | // Store host options 37 | this.#hosts[hostname] = options; 38 | 39 | // Register the host server with uWS 40 | this.#app.uws_instance.addServerName(hostname, options); 41 | 42 | // Return this instance 43 | return this; 44 | } 45 | 46 | /** 47 | * Un-Registers the unique host options to use for the specified hostname for incoming requests. 48 | * 49 | * @param {String} hostname 50 | * @returns {HostManager} 51 | */ 52 | remove(hostname) { 53 | // Remove host options 54 | delete this.#hosts[hostname]; 55 | 56 | // Un-Register the host server with uWS 57 | this.#app.uws_instance.removeServerName(hostname); 58 | 59 | // Return this instance 60 | return this; 61 | } 62 | 63 | /* HostManager Getters & Properties */ 64 | 65 | /** 66 | * Returns all of the registered hostname options. 67 | * @returns {Object.} 68 | */ 69 | get registered() { 70 | return this.#hosts; 71 | } 72 | } 73 | 74 | module.exports = HostManager; 75 | -------------------------------------------------------------------------------- /src/components/plugins/MultipartField.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const stream = require('stream'); 3 | const FileSystem = require('fs'); 4 | 5 | class MultipartField { 6 | #name; 7 | #encoding; 8 | #mime_type; 9 | #file; 10 | #value; 11 | #truncated; 12 | 13 | constructor(name, value, info) { 14 | // Store general information about this field 15 | this.#name = name; 16 | this.#encoding = info.encoding; 17 | this.#mime_type = info.mimeType; 18 | 19 | // Determine if this field is a file or a normal field 20 | if (value instanceof stream.Readable) { 21 | // Store this file's supplied name and data stream 22 | this.#file = { 23 | name: info.filename, 24 | stream: value, 25 | }; 26 | } else { 27 | // Store field value and truncation information 28 | this.#value = value; 29 | this.#truncated = { 30 | name: info.nameTruncated, 31 | value: info.valueTruncated, 32 | }; 33 | } 34 | } 35 | 36 | /* MultipartField Methods */ 37 | 38 | /** 39 | * Saves this multipart file content to the specified path. 40 | * Note! You must specify the file name and extension in the path itself. 41 | * 42 | * @param {String} path Path with file name to which you would like to save this file. 43 | * @param {stream.WritableOptions} options Writable stream options 44 | * @returns {Promise} 45 | */ 46 | write(path, options) { 47 | // Throw an error if this method is called on a non file field 48 | if (this.file === undefined) 49 | throw new Error( 50 | 'HyperExpress.Request.MultipartField.write(path, options) -> This method can only be called on a field that is a file type.' 51 | ); 52 | 53 | // Return a promise which resolves once write stream has finished 54 | const reference = this; 55 | return new Promise((resolve, reject) => { 56 | const writable = FileSystem.createWriteStream(path, options); 57 | writable.on('close', resolve); 58 | writable.on('error', reject); 59 | reference.file.stream.pipe(writable); 60 | }); 61 | } 62 | 63 | /* MultipartField Properties */ 64 | 65 | /** 66 | * Field name as specified in the multipart form. 67 | * @returns {String} 68 | */ 69 | get name() { 70 | return this.#name; 71 | } 72 | 73 | /** 74 | * Field encoding as specified in the multipart form. 75 | * @returns {String} 76 | */ 77 | get encoding() { 78 | return this.#encoding; 79 | } 80 | 81 | /** 82 | * Field mime type as specified in the multipart form. 83 | * @returns {String} 84 | */ 85 | get mime_type() { 86 | return this.#mime_type; 87 | } 88 | 89 | /** 90 | * @typedef {Object} MultipartFile 91 | * @property {String=} name If supplied, this file's name as supplied by sender. 92 | * @property {stream.Readable} stream Readable stream to consume this file's data. 93 | */ 94 | 95 | /** 96 | * Returns file information about this field if it is a file type. 97 | * Note! This property will ONLY be defined if this field is a file type. 98 | * 99 | * @returns {MultipartFile} 100 | */ 101 | get file() { 102 | return this.#file; 103 | } 104 | 105 | /** 106 | * Returns field value if this field is a non-file type. 107 | * Note! This property will ONLY be defined if this field is a non-file type. 108 | * 109 | * @returns {String} 110 | */ 111 | get value() { 112 | return this.#value; 113 | } 114 | 115 | /** 116 | * @typedef {Object} Truncations 117 | * @property {Boolean} name Whether this field's name was truncated. 118 | * @property {Boolean} value Whether this field's value was truncated. 119 | */ 120 | 121 | /** 122 | * Returns information about truncations in this field. 123 | * Note! This property will ONLY be defined if this field is a non-file type. 124 | * 125 | * @returns {Truncations} 126 | */ 127 | get truncated() { 128 | return this.#truncated; 129 | } 130 | } 131 | 132 | module.exports = MultipartField; 133 | -------------------------------------------------------------------------------- /src/components/plugins/SSEventStream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | class SSEventStream { 3 | _response; 4 | 5 | #wrote_headers = false; 6 | /** 7 | * @private 8 | * Ensures the proper SSE headers are written to the client to initiate the SSE stream. 9 | * @returns {Boolean} Whether the headers were written 10 | */ 11 | _initiate_sse_stream() { 12 | // If the response has already been initiated, we cannot write headers anymore 13 | if (this._response.initiated) return false; 14 | 15 | // If we have already written headers, we cannot write again 16 | if (this.#wrote_headers) return false; 17 | this.#wrote_headers = true; 18 | 19 | // Write the headers for the SSE stream to the client 20 | this._response 21 | .header('content-type', 'text/event-stream') 22 | .header('cache-control', 'no-cache') 23 | .header('connection', 'keep-alive') 24 | .header('x-accel-buffering', 'no'); 25 | 26 | // Return true to signify that we have written headers 27 | return true; 28 | } 29 | 30 | /** 31 | * @private 32 | * Internal method to write data to the response stream. 33 | * @returns {Boolean} Whether the data was written 34 | */ 35 | _write(data) { 36 | // Initialize the SSE stream 37 | this._initiate_sse_stream(); 38 | 39 | // Write the data to the response stream 40 | return this._response.write(data); 41 | } 42 | 43 | /** 44 | * Opens the "Server-Sent Events" connection to the client. 45 | * 46 | * @returns {Boolean} 47 | */ 48 | open() { 49 | // We simply send a comment-type message to the client to indicate that the connection has been established 50 | // The "data" can be anything as it will not be handled by the client EventSource object 51 | return this.comment('open'); 52 | } 53 | 54 | /** 55 | * Closes the "Server-Sent Events" connection to the client. 56 | * 57 | * @returns {Boolean} 58 | */ 59 | close() { 60 | // Ends the connection by sending the final empty message 61 | return this._response.send(); 62 | } 63 | 64 | /** 65 | * Sends a comment-type message to the client that will not be emitted by EventSource. 66 | * This can be useful as a keep-alive mechanism if messages might not be sent regularly. 67 | * 68 | * @param {String} data 69 | * @returns {Boolean} 70 | */ 71 | comment(data) { 72 | // Prefix the message with a colon character to signify a comment 73 | return this._write(`: ${data}\n`); 74 | } 75 | 76 | /** 77 | * Sends a message to the client based on the specified event and data. 78 | * Note! You must retry failed messages if you receive a false output from this method. 79 | * 80 | * @param {String} id 81 | * @param {String=} event 82 | * @param {String=} data 83 | * @returns {Boolean} 84 | */ 85 | send(id, event, data) { 86 | // Parse arguments into overloaded parameter translations 87 | const _id = id && event && data ? id : undefined; 88 | const _event = id && event ? (_id ? event : id) : undefined; 89 | const _data = data || event || id; 90 | 91 | // Build message parts to prepare a payload 92 | const parts = []; 93 | if (_id) parts.push(`id: ${_id}`); 94 | if (_event) parts.push(`event: ${_event}`); 95 | if (_data) parts.push(`data: ${_data}`); 96 | 97 | // Push an empty line to indicate the end of the message 98 | parts.push('', ''); 99 | 100 | // Write the string based payload to the client 101 | return this._write(parts.join('\n')); 102 | } 103 | 104 | /* SSEConnection Properties */ 105 | 106 | /** 107 | * Whether this Server-Sent Events stream is still active. 108 | * 109 | * @returns {Boolean} 110 | */ 111 | get active() { 112 | return !this._response.completed; 113 | } 114 | } 115 | 116 | module.exports = SSEventStream; 117 | -------------------------------------------------------------------------------- /tests/components/Server.js: -------------------------------------------------------------------------------- 1 | const { server, HyperExpress } = require('../configuration.js'); 2 | const { log, assert_log } = require('../scripts/operators.js'); 3 | 4 | // Create a test HyperExpress instance 5 | const TEST_SERVER = new HyperExpress.Server({ 6 | fast_buffers: true, 7 | max_body_length: 1000 * 1000 * 7, 8 | }); 9 | 10 | // Set some value into the locals object to be checked in the future 11 | // through the Request/Response app property 12 | TEST_SERVER.locals.some_reference = { 13 | some_data: true, 14 | }; 15 | 16 | // Bind error handler for catch-all logging 17 | TEST_SERVER.set_error_handler((request, response, error) => { 18 | // Handle expected errors with their appropriate callbacks 19 | if (typeof request.expected_error == 'function') { 20 | request.expected_error(error); 21 | } else { 22 | // Treat as global error and log to console 23 | log( 24 | 'UNCAUGHT_ERROR_REQUEST', 25 | `${request.method} | ${request.url}\n ${JSON.stringify(request.headers, null, 2)}` 26 | ); 27 | console.log(error); 28 | response.send('Uncaught Error Occured'); 29 | } 30 | }); 31 | 32 | function not_found_handler(request, response) { 33 | // Handle dynamic middleware executions to the requester 34 | if (Array.isArray(request.middleware_executions)) { 35 | request.middleware_executions.push('not-found'); 36 | return response.json(request.middleware_executions); 37 | } 38 | 39 | // Return a 404 response 40 | return response.status(404).send('Not Found'); 41 | } 42 | 43 | // Bind not found handler for unexpected incoming requests 44 | TEST_SERVER.set_not_found_handler((request, response) => { 45 | console.warn( 46 | 'This handler should not actually be called as one of the tests binds a Server.all("*") route which should prevent this handler from ever being ran.' 47 | ); 48 | not_found_handler(request, response); 49 | }); 50 | 51 | // Bind a test route which returns a response with a delay 52 | // This will be used to simulate long running requests 53 | TEST_SERVER.get('/echo/:delay', async (request, response) => { 54 | // Wait for the specified delay and return a response 55 | const delay = Number(request.path_parameters.delay) || 0; 56 | await new Promise((resolve) => setTimeout(resolve, delay)); 57 | return response.send(delay.toString()); 58 | }); 59 | 60 | async function test_server_shutdown() { 61 | let group = 'SERVER'; 62 | 63 | // Make a fetch request to the echo endpoint with a delay of 100ms 64 | const delay = 100; 65 | const started_at = Date.now(); 66 | 67 | // Send the request and time the response 68 | const response = await fetch(`${server.base}/echo/${delay}`); 69 | 70 | // Begin the server shutdown process and time the shutdown 71 | let shutdown_time_ms = 0; 72 | const shutdown_promise = TEST_SERVER.shutdown(); 73 | shutdown_promise.then(() => (shutdown_time_ms = Date.now() - started_at)); 74 | 75 | // Send a second fetch which should be immediately closed 76 | let response2_error; 77 | try { 78 | const response2 = await fetch(`${server.base}/echo/${delay}`); 79 | } catch (error) { 80 | response2_error = error; 81 | } 82 | 83 | // Begin processing the response body 84 | const body = await response.text(); 85 | const request_time_ms = Date.now() - started_at; 86 | 87 | // Wait for the server shutdown to complete 88 | await shutdown_promise; 89 | 90 | // Verify middleware functionalitiy and property binding 91 | assert_log( 92 | group, 93 | 'Graceful Shutdown Test In ' + (Date.now() - started_at) + 'ms', 94 | // Ensure that the response body matches the delay 95 | // Ensure that the request time is greater than the delay (The handler artificially waited for the delay) 96 | // Ensure that the shutdown time is greater than the delay (The server shutdown took longer than the delay) 97 | // Ensure that response2 failed over network as the server shutdown was in process which would immediately close the request 98 | () => 99 | body === delay.toString() && 100 | request_time_ms >= delay && 101 | shutdown_time_ms >= delay && 102 | response2_error !== undefined 103 | ); 104 | } 105 | 106 | module.exports = { 107 | TEST_SERVER, 108 | not_found_handler, 109 | test_server_shutdown, 110 | }; 111 | -------------------------------------------------------------------------------- /tests/components/features/HostManager.js: -------------------------------------------------------------------------------- 1 | const { assert_log } = require('../../scripts/operators.js'); 2 | const { TEST_SERVER } = require('../Server.js'); 3 | 4 | function test_hostmanager_object() { 5 | let group = 'Server'; 6 | let candidate = 'HyperExpress.HostManager'; 7 | 8 | // Retrieve the host manager 9 | const manager = TEST_SERVER.hosts; 10 | 11 | // Define random host configurations 12 | const hostnames = [ 13 | [ 14 | 'example.com', 15 | { 16 | passphrase: 'passphrase', 17 | }, 18 | ], 19 | [ 20 | 'google.com', 21 | { 22 | passphrase: 'passphrase', 23 | }, 24 | ], 25 | ]; 26 | 27 | // Add the host names to the host manager 28 | for (const [hostname, options] of hostnames) { 29 | manager.add(hostname, options); 30 | } 31 | 32 | // Assert that the host manager contains the host names 33 | for (const [hostname, options] of hostnames) { 34 | assert_log( 35 | group, 36 | candidate + ` - Host Registeration Test For ${hostname}`, 37 | () => JSON.stringify(manager.registered[hostname]) === JSON.stringify(options) 38 | ); 39 | } 40 | 41 | // Remove the host names from the host manager 42 | for (const [hostname, options] of hostnames) { 43 | manager.remove(hostname); 44 | } 45 | 46 | // Assert that the host manager does not contain the host names 47 | for (const [hostname, options] of hostnames) { 48 | assert_log( 49 | group, 50 | candidate + ` - Host Un-Registeration Test For ${hostname}`, 51 | () => !(hostname in manager.registered) 52 | ); 53 | } 54 | } 55 | 56 | module.exports = { 57 | test_hostmanager_object, 58 | }; 59 | -------------------------------------------------------------------------------- /tests/components/features/LiveFile.js: -------------------------------------------------------------------------------- 1 | const { assert_log } = require('../../scripts/operators.js'); 2 | const { fetch, server } = require('../../configuration.js'); 3 | const { TEST_SERVER } = require('../Server.js'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const endpoint = '/tests/response/send-file'; 7 | const endpoint_url = server.base + endpoint; 8 | const test_file_path = path.resolve(__dirname, '../../../tests/content/test.html'); 9 | 10 | // Create Backend HTTP Route 11 | TEST_SERVER.get(endpoint, async (request, response) => { 12 | // We purposely delay 100ms so cached vs. uncached does not rely too much on system disk 13 | return response.download(test_file_path, 'something.html'); 14 | }); 15 | 16 | async function test_livefile_object() { 17 | let group = 'RESPONSE'; 18 | let candidate = 'HyperExpress.Response'; 19 | 20 | // Read the test file into memory 21 | const test_file_string = fs.readFileSync(test_file_path).toString(); 22 | 23 | // Perform fetch request 24 | const response = await fetch(endpoint_url); 25 | const body = await response.text(); 26 | 27 | // Test initial content type and length test for file 28 | const headers = response.headers.raw(); 29 | const content_type = headers['content-type']; 30 | const content_length = headers['content-length']; 31 | assert_log(group, candidate + '.file()', () => { 32 | return ( 33 | content_type == 'text/html; charset=utf-8' && 34 | content_length == test_file_string.length.toString() && 35 | body.length == test_file_string.length 36 | ); 37 | }); 38 | 39 | // Test Content-Disposition header to validate .attachment() 40 | assert_log( 41 | group, 42 | `${candidate}.attachment() & ${candidate}.download()`, 43 | () => headers['content-disposition'][0] == 'attachment; filename="something.html"' 44 | ); 45 | } 46 | 47 | module.exports = { 48 | test_livefile_object: test_livefile_object, 49 | }; 50 | -------------------------------------------------------------------------------- /tests/components/http/scenarios/middleware_double_iteration.js: -------------------------------------------------------------------------------- 1 | const { assert_log } = require('../../../scripts/operators.js'); 2 | const { HyperExpress, fetch, server } = require('../../../configuration.js'); 3 | const router = new HyperExpress.Router(); 4 | const endpoint = '/tests/request'; 5 | const scenario_endpoint = '/middleware-double-iteration'; 6 | const endpoint_url = server.base + endpoint + scenario_endpoint; 7 | 8 | // This middleware should only run on this endpoint 9 | const double_iteration_middleware = async (request, response, next) => { 10 | // Bind an artificial error handler so we don't treat this as uncaught error 11 | request.expected_error = () => response.status(501).send('DOUBLE_ITERATION_VIOLATION'); 12 | 13 | // Since this is an async callback, calling next and the async callback resolving will trigger a double iteration violation 14 | next(); 15 | }; 16 | 17 | const delay_middleware = (request, response, next) => setTimeout(next, 10); 18 | 19 | // Create Backend HTTP Route 20 | router.get( 21 | scenario_endpoint, 22 | double_iteration_middleware, 23 | [delay_middleware], // This weird parameter pattern is to test Express.js compatibility pattern for providing multiple middlewares through parameters/arrays 24 | { 25 | max_body_length: 1024 * 1024 * 10, 26 | middlewares: [delay_middleware], 27 | }, 28 | async (request, response) => { 29 | return response.send('Good'); 30 | } 31 | ); 32 | 33 | // Bind router to webserver 34 | const { TEST_SERVER } = require('../../Server.js'); 35 | TEST_SERVER.use(endpoint, router); 36 | 37 | async function test_middleware_double_iteration() { 38 | const group = 'REQUEST'; 39 | const candidate = 'HyperExpress.Request'; 40 | 41 | // Perform fetch request 42 | const response = await fetch(endpoint_url); 43 | const body = await response.text(); 44 | 45 | // Test to see error handler was properly called on expected middleware error 46 | assert_log( 47 | group, 48 | `${candidate} Middleware Double Iteration Violation`, 49 | () => response.status === 501 && body === 'DOUBLE_ITERATION_VIOLATION' 50 | ); 51 | } 52 | 53 | module.exports = { 54 | test_middleware_double_iteration, 55 | }; 56 | -------------------------------------------------------------------------------- /tests/components/http/scenarios/middleware_dynamic_iteration.js: -------------------------------------------------------------------------------- 1 | const { assert_log } = require('../../../scripts/operators.js'); 2 | const { HyperExpress, fetch, server } = require('../../../configuration.js'); 3 | const router = new HyperExpress.Router(); 4 | const endpoint = '/tests/request'; 5 | const scenario_endpoint = '/middleware-dynamic-iteration'; 6 | const endpoint_url = server.base + endpoint + scenario_endpoint; 7 | const { TEST_SERVER } = require('../../Server.js'); 8 | 9 | // Bind a global middleware which is a wildcard for a path that has no existing routes 10 | // This middleware will apply to the not found handler 11 | const global_wildcard_middleware = (request, response, next) => { 12 | // Check if dynamic middleware is enabled 13 | if (request.headers['x-dynamic-middleware'] === 'true') { 14 | return response.send('GLOBAL_WILDCARD'); 15 | } 16 | 17 | // Call next middleware 18 | next(); 19 | }; 20 | 21 | TEST_SERVER.use('/global-wildcard/', global_wildcard_middleware); 22 | 23 | // Bind a global middleware which is a wildcard for a path that is on an existing route path 24 | const route_specific_dynamic_middleware = (request, response, next) => { 25 | if (request.headers['x-dynamic-middleware'] === 'true') { 26 | response.send('ROUTE_SPECIFIC_WILDCARD'); 27 | } 28 | 29 | // Call next middleware 30 | next(); 31 | }; 32 | router.use('/middleware-dynamic-iteration/middleware', route_specific_dynamic_middleware); 33 | 34 | // Bind a middleware which will try target an incomplete part of the path and should not be executed 35 | const incomplete_path_middleware = (request, response, next) => { 36 | // This should never be executed 37 | console.log('INCOMPLETE_PATH_MIDDLEWARE'); 38 | return response.send('INCOMPLETE_PATH_MIDDLEWARE'); 39 | }; 40 | router.use('/middleware-dy', incomplete_path_middleware); // Notice how "/middleware-dy" should not match "/middleware-dynamic-iteration/..." 41 | 42 | // Create Backend HTTP Route 43 | router.get(scenario_endpoint + '/*', async (request, response) => { 44 | response.send('ROUTE_HANDLER'); 45 | }); 46 | 47 | // Bind router to webserver 48 | TEST_SERVER.use(endpoint, router); 49 | 50 | async function test_middleware_dynamic_iteration() { 51 | const group = 'REQUEST'; 52 | const candidate = 'HyperExpress.Request'; 53 | 54 | // Make a fetch request to a random path that will not be found 55 | const not_found_response = await fetch(server.base + '/not-found/' + Math.random(), { 56 | headers: { 57 | 'x-dynamic-middleware': 'true', 58 | }, 59 | }); 60 | 61 | // Assert that we received a 404 response 62 | assert_log(group, `${candidate} Unhandled Middleware Iteration`, () => not_found_response.status === 404); 63 | 64 | // Make a fetch request to a global not found path on the global wildcard pattern 65 | const global_response = await fetch(server.base + '/global-wildcard/' + Math.random(), { 66 | headers: { 67 | 'x-dynamic-middleware': 'true', 68 | }, 69 | }); 70 | const global_text = await global_response.text(); 71 | 72 | // Assert that the global wildcard middleware was executed 73 | assert_log(group, `${candidate} Global Dynamic Middleware Iteration`, () => global_text === 'GLOBAL_WILDCARD'); 74 | 75 | // Make a fetch request to a path that has a route with a wildcard middleware 76 | const route_specific_response = await fetch(endpoint_url + '/middleware/' + Math.random(), { 77 | headers: { 78 | 'x-dynamic-middleware': 'true', 79 | }, 80 | }); 81 | const route_specific_text = await route_specific_response.text(); 82 | 83 | // Assert that the route specific wildcard middleware was executed 84 | assert_log( 85 | group, 86 | `${candidate} Route-Specific Dynamic Middleware Iteration`, 87 | () => route_specific_text === 'ROUTE_SPECIFIC_WILDCARD' 88 | ); 89 | 90 | // Make a fetch request to a path that has an exact route match 91 | const route_handler_response = await fetch(endpoint_url + '/test/random/' + Math.random(), { 92 | headers: { 93 | 'x-dynamic-middleware': 'true', 94 | }, 95 | }); 96 | const route_handler_text = await route_handler_response.text(); 97 | 98 | // Assert that the route handler was executed 99 | assert_log( 100 | group, 101 | `${candidate} Route-Specific Dynamic Middleware Pattern Matching Check`, 102 | () => route_handler_text === 'ROUTE_HANDLER' 103 | ); 104 | } 105 | 106 | module.exports = { 107 | test_middleware_dynamic_iteration, 108 | }; 109 | -------------------------------------------------------------------------------- /tests/components/http/scenarios/middleware_execution_order.js: -------------------------------------------------------------------------------- 1 | const { assert_log } = require('../../../scripts/operators.js'); 2 | const { HyperExpress, fetch, server } = require('../../../configuration.js'); 3 | const router = new HyperExpress.Router(); 4 | const endpoint = '/tests/request'; 5 | const scenario_endpoint = '/middleware-execution-order'; 6 | const endpoint_url = server.base + endpoint + scenario_endpoint; 7 | const { TEST_SERVER } = require('../../Server.js'); 8 | 9 | // Create a middleware to bind a middleware executions array to the request object 10 | router.use(scenario_endpoint, (request, response, next) => { 11 | // Initialize an array to contain middleware_executions 12 | request.middleware_executions = []; 13 | next(); 14 | }); 15 | 16 | // Create a single depth middleware 17 | router.use(scenario_endpoint + '/one', (request, response, next) => { 18 | request.middleware_executions.push('one'); 19 | next(); 20 | }); 21 | 22 | // Create a two depth middleware that depends on the previous middleware 23 | router.use(scenario_endpoint + '/one/two', (request, response, next) => { 24 | request.middleware_executions.push('one/two'); 25 | next(); 26 | }); 27 | 28 | // Create a unique single depth middleware 29 | router.use(scenario_endpoint + '/three', (request, response, next) => { 30 | request.middleware_executions.push('three'); 31 | next(); 32 | }); 33 | 34 | // Create a catch-all middleware to ensure execution order 35 | router.use(scenario_endpoint, (request, response, next) => { 36 | request.middleware_executions.push('catch-all'); 37 | next(); 38 | }); 39 | 40 | // Bind routes for each middleware to test route assignment 41 | router.get(scenario_endpoint + '/one', (request, response) => { 42 | request.middleware_executions.push('one/route'); 43 | response.json(request.middleware_executions); 44 | }); 45 | 46 | router.get( 47 | scenario_endpoint + '/one/two/*', 48 | { 49 | max_body_length: 100 * 1e6, 50 | }, 51 | (request, response) => { 52 | request.middleware_executions.push('one/two/route'); 53 | response.json(request.middleware_executions); 54 | } 55 | ); 56 | 57 | // Bind router to webserver 58 | TEST_SERVER.use(endpoint, router); 59 | 60 | async function test_middleware_execution_order() { 61 | const group = 'REQUEST'; 62 | const candidate = 'HyperExpress.Request'; 63 | 64 | // Make a fetch request to just the scenario endpoint which should only trigger catch-all 65 | const catch_all_response = await fetch(endpoint_url); 66 | const catch_all_response_json = await catch_all_response.json(); 67 | assert_log( 68 | group, 69 | `${candidate} Catch-All Middleware Execution Order`, 70 | () => ['catch-all', 'not-found'].join(',') === catch_all_response_json.join(',') 71 | ); 72 | 73 | // Make a fetch request to the single depth middleware 74 | const single_depth_response = await fetch(endpoint_url + '/one'); 75 | const single_depth_response_json = await single_depth_response.json(); 76 | assert_log( 77 | group, 78 | `${candidate} Single Path Depth Middleware Execution Order`, 79 | () => ['one', 'catch-all', 'one/route'].join(',') === single_depth_response_json.join(',') 80 | ); 81 | 82 | // Make a fetch request to the two depth middleware that depends on the previous middleware 83 | const two_depth_response = await fetch(endpoint_url + '/one/two/' + Math.random()); 84 | const two_depth_response_json = await two_depth_response.json(); 85 | assert_log( 86 | group, 87 | `${candidate} Double Path Depth-Dependent Middleware Execution Order`, 88 | () => ['one', 'one/two', 'catch-all', 'one/two/route'].join(',') === two_depth_response_json.join(',') 89 | ); 90 | 91 | // Make a fetch request to the unique single depth middleware 92 | const unique_single_depth_response = await fetch(endpoint_url + '/three/' + Math.random()); 93 | const unique_single_depth_response_json = await unique_single_depth_response.json(); 94 | assert_log( 95 | group, 96 | `${candidate} Single Path Depth Unique Middleware Execution Order`, 97 | () => ['three', 'catch-all', 'not-found'].join(',') === unique_single_depth_response_json.join(',') 98 | ); 99 | } 100 | 101 | module.exports = { 102 | test_middleware_execution_order, 103 | }; 104 | -------------------------------------------------------------------------------- /tests/components/http/scenarios/middleware_iteration_error.js: -------------------------------------------------------------------------------- 1 | const { assert_log } = require('../../../scripts/operators.js'); 2 | const { HyperExpress, fetch, server } = require('../../../configuration.js'); 3 | const router = new HyperExpress.Router(); 4 | const endpoint = '/tests/request'; 5 | const scenario_endpoint = '/middleware-error'; 6 | const endpoint_url = server.base + endpoint + scenario_endpoint; 7 | 8 | const middleware = (request, response, next) => { 9 | // Bind an artificial error handler so we don't treat this as uncaught error 10 | request.expected_error = () => response.status(501).send('MIDDLEWARE_ERROR'); 11 | 12 | // Assume some problem occured, so we pass an error to next 13 | next(new Error('EXPECTED_ERROR')); 14 | }; 15 | 16 | // Create Backend HTTP Route 17 | router.get(scenario_endpoint, middleware, async (request, response) => { 18 | return response.send('Good'); 19 | }); 20 | 21 | // Bind router to webserver 22 | const { TEST_SERVER } = require('../../Server.js'); 23 | TEST_SERVER.use(endpoint, router); 24 | 25 | async function test_middleware_iteration_error() { 26 | const group = 'REQUEST'; 27 | const candidate = 'HyperExpress.Request'; 28 | 29 | // Perform fetch request 30 | const response = await fetch(endpoint_url); 31 | const body = await response.text(); 32 | 33 | // Test to see error handler was properly called on expected middleware error 34 | assert_log( 35 | group, 36 | `${candidate} Middleware Thrown Iteration Error Handler`, 37 | () => response.status === 501 && body === 'MIDDLEWARE_ERROR' 38 | ); 39 | } 40 | 41 | module.exports = { 42 | test_middleware_iteration_error, 43 | }; 44 | -------------------------------------------------------------------------------- /tests/components/http/scenarios/middleware_layered_iteration.js: -------------------------------------------------------------------------------- 1 | const { assert_log } = require('../../../scripts/operators.js'); 2 | const { HyperExpress, fetch, server } = require('../../../configuration.js'); 3 | const crypto = require('crypto'); 4 | const router = new HyperExpress.Router(); 5 | const endpoint = '/tests/request'; 6 | const scenario_endpoint = '/middleware-layered-iteration'; 7 | const endpoint_url = server.base + endpoint + scenario_endpoint; 8 | 9 | // Create Backend HTTP Route 10 | const options = { 11 | max_body_length: 1024 * 1024 * 25, 12 | }; 13 | 14 | // Shallow copy of options before route creation 15 | const options_copy = { 16 | ...options, 17 | }; 18 | 19 | router.post( 20 | scenario_endpoint, 21 | options, 22 | async (req, res, next) => { 23 | req.body = await req.json(); 24 | }, 25 | (req, res, next) => { 26 | res.locals.data = req.body; 27 | next(); 28 | }, 29 | (req, res) => { 30 | res.status(200).json(res.locals.data); 31 | } 32 | ); 33 | 34 | // Bind router to webserver 35 | const { TEST_SERVER } = require('../../Server.js'); 36 | TEST_SERVER.use(endpoint, router); 37 | 38 | async function test_middleware_layered_iterations(iterations = 5) { 39 | const group = 'REQUEST'; 40 | const candidate = 'HyperExpress.Request'; 41 | for (let iteration = 0; iteration < iterations; iteration++) { 42 | // Generate a random payload 43 | const payload = {}; 44 | for (let i = 0; i < 10; i++) { 45 | payload[crypto.randomUUID()] = crypto.randomUUID(); 46 | } 47 | 48 | // Perform fetch request 49 | const response = await fetch(endpoint_url, { 50 | method: 'POST', 51 | body: JSON.stringify(payload), 52 | }); 53 | const body = await response.json(); 54 | 55 | // Test to see error handler was properly called on expected middleware error 56 | assert_log( 57 | group, 58 | `${candidate} Middleware Layered Iterations Test #${iteration + 1}`, 59 | () => JSON.stringify(payload) === JSON.stringify(body) 60 | ); 61 | } 62 | 63 | // Test to see that the provided options object was not modified 64 | assert_log( 65 | group, 66 | `${candidate} Middleware Provided Object Immutability Test`, 67 | () => JSON.stringify(options) === JSON.stringify(options_copy) 68 | ); 69 | } 70 | 71 | module.exports = { 72 | test_middleware_layered_iterations, 73 | }; 74 | -------------------------------------------------------------------------------- /tests/components/http/scenarios/middleware_uncaught_async_error.js: -------------------------------------------------------------------------------- 1 | const { assert_log } = require('../../../scripts/operators.js'); 2 | const { HyperExpress, fetch, server } = require('../../../configuration.js'); 3 | const router = new HyperExpress.Router(); 4 | const endpoint = '/tests/request'; 5 | const scenario_endpoint = '/middleware-uncaught-async-error'; 6 | const endpoint_url = server.base + endpoint + scenario_endpoint; 7 | 8 | const middleware = async (request, response, next) => { 9 | // Bind an artificial error handler so we don't treat this as uncaught error 10 | request.expected_error = () => response.status(501).send('MIDDLEWARE_ERROR'); 11 | 12 | // Assume some problem occured, so we pass an error to next 13 | throw new Error('EXPECTED_ERROR'); 14 | }; 15 | 16 | // Create Backend HTTP Route 17 | router.get(scenario_endpoint, middleware, async (request, response) => { 18 | return response.send('Good'); 19 | }); 20 | 21 | // Bind router to webserver 22 | const { TEST_SERVER } = require('../../Server.js'); 23 | TEST_SERVER.use(endpoint, router); 24 | 25 | async function test_middleware_uncaught_async_error() { 26 | const group = 'REQUEST'; 27 | const candidate = 'HyperExpress.Request'; 28 | 29 | // Perform fetch request 30 | const response = await fetch(endpoint_url); 31 | const body = await response.text(); 32 | 33 | // Test to see error handler was properly called on expected middleware error 34 | assert_log( 35 | group, 36 | `${candidate} Middleware Thrown Iteration Error Handler`, 37 | () => response.status === 501 && body === 'MIDDLEWARE_ERROR' 38 | ); 39 | } 40 | 41 | module.exports = { 42 | test_middleware_uncaught_async_error, 43 | }; 44 | -------------------------------------------------------------------------------- /tests/components/http/scenarios/request_body_echo_test.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const { assert_log } = require('../../../scripts/operators.js'); 3 | const { HyperExpress, fetch, server } = require('../../../configuration.js'); 4 | const router = new HyperExpress.Router(); 5 | const endpoint = '/tests/request'; 6 | const scenario_endpoint = '/json-body-echo'; 7 | const endpoint_url = server.base + endpoint + scenario_endpoint; 8 | 9 | // Create Backend HTTP Route 10 | router.post( 11 | scenario_endpoint, 12 | async (req) => { 13 | req.body = await req.json(); 14 | return; 15 | }, 16 | (req, res, next) => { 17 | res.locals.data = req.body; 18 | next(); 19 | }, 20 | (_, res) => { 21 | res.status(200).json(res.locals.data); 22 | } 23 | ); 24 | 25 | // Bind router to webserver 26 | const { TEST_SERVER } = require('../../Server.js'); 27 | TEST_SERVER.use(endpoint, router); 28 | 29 | async function test_request_body_echo_test(iterations = 5) { 30 | const group = 'REQUEST'; 31 | const candidate = 'HyperExpress.Request.json()'; 32 | 33 | for (let i = 0; i < iterations; i++) { 34 | // Generate a small random payload 35 | const payload = { 36 | foo: crypto.randomBytes(5).toString('hex'), 37 | }; 38 | 39 | // Make the fetch request 40 | const response = await fetch(endpoint_url, { 41 | method: 'POST', 42 | body: JSON.stringify(payload), 43 | }); 44 | 45 | // Retrieve the JSON response body 46 | const body = await response.json(); 47 | 48 | // Assert that the payload and response body are the same 49 | assert_log( 50 | group, 51 | `${candidate} JSON Small Body Echo Test #${i + 1}`, 52 | () => JSON.stringify(payload) === JSON.stringify(body) 53 | ); 54 | } 55 | } 56 | 57 | module.exports = { 58 | test_request_body_echo_test, 59 | }; 60 | -------------------------------------------------------------------------------- /tests/components/http/scenarios/request_chunked_json.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const { assert_log } = require('../../../scripts/operators.js'); 4 | const { HyperExpress, fetch, server } = require('../../../configuration.js'); 5 | const router = new HyperExpress.Router(); 6 | const endpoint = '/tests/request'; 7 | const scenario_endpoint = '/chunked-json'; 8 | const endpoint_url = server.base + endpoint + scenario_endpoint; 9 | const test_file_path = path.resolve(path.join(__dirname, '../../../content/test-body.json')); 10 | 11 | // Create Backend HTTP Route 12 | router.post(scenario_endpoint, async (request, response) => { 13 | const body = await request.json(); 14 | return response.json(body); 15 | }); 16 | 17 | // Bind router to webserver 18 | const { TEST_SERVER } = require('../../Server.js'); 19 | TEST_SERVER.use(endpoint, router); 20 | 21 | async function test_request_chunked_json() { 22 | const group = 'REQUEST'; 23 | const candidate = 'HyperExpress.Request.json()'; 24 | 25 | // Send a buffer of the file in the request body so we have a content-length on server side 26 | const expected_json = JSON.stringify(JSON.parse(fs.readFileSync(test_file_path).toString('utf8'))); 27 | const json_stream_response = await fetch(endpoint_url, { 28 | method: 'POST', 29 | headers: { 30 | 'transfer-encoding': 'chunked', 31 | 'x-file-name': 'request_upload_body.json', 32 | }, 33 | body: fs.createReadStream(test_file_path), 34 | }); 35 | 36 | // Validate the hash uploaded on the server side with the expected hash from client side 37 | const uploaded_json = await json_stream_response.text(); 38 | assert_log(group, `${candidate} Chunked Transfer JSON Upload Test`, () => expected_json === uploaded_json); 39 | } 40 | 41 | module.exports = { 42 | test_request_chunked_json, 43 | }; 44 | -------------------------------------------------------------------------------- /tests/components/http/scenarios/request_chunked_stream.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const crypto = require('crypto'); 3 | const fs = require('fs'); 4 | const { assert_log } = require('../../../scripts/operators.js'); 5 | const { HyperExpress, fetch, server } = require('../../../configuration.js'); 6 | const router = new HyperExpress.Router(); 7 | const endpoint = '/tests/request'; 8 | const scenario_endpoint = '/chunked-stream'; 9 | const endpoint_url = server.base + endpoint + scenario_endpoint; 10 | const test_file_path = path.resolve(path.join(__dirname, '../../../content/large-image.jpg')); 11 | const test_file_stats = fs.statSync(test_file_path); 12 | 13 | function get_file_write_path(file_name) { 14 | return path.resolve(path.join(__dirname, '../../../content/written/' + file_name)); 15 | } 16 | 17 | // Create Backend HTTP Route 18 | router.post(scenario_endpoint, async (request, response) => { 19 | // Create a writable stream to specified file name path 20 | const file_name = request.headers['x-file-name']; 21 | const path = get_file_write_path(file_name); 22 | const writable = fs.createWriteStream(path); 23 | 24 | // Pipe the readable body stream to the writable and wait for it to finish 25 | request.pipe(writable); 26 | await new Promise((resolve) => writable.once('finish', resolve)); 27 | 28 | // Read the written file's buffer and calculate its md5 hash 29 | const written_buffer = fs.readFileSync(path); 30 | const written_hash = crypto.createHash('md5').update(written_buffer).digest('hex'); 31 | 32 | // Cleanup the written file for future testing 33 | fs.rmSync(path); 34 | 35 | // Return the written hash to be validated on client side 36 | return response.json({ 37 | hash: written_hash, 38 | }); 39 | }); 40 | 41 | // Bind router to webserver 42 | const { TEST_SERVER } = require('../../Server.js'); 43 | TEST_SERVER.use(endpoint, router); 44 | 45 | async function test_request_chunked_stream() { 46 | const group = 'REQUEST'; 47 | const candidate = 'HyperExpress.Request.stream'; 48 | 49 | // Send a buffer of the file in the request body so we have a content-length on server side 50 | const expected_buffer = fs.readFileSync(test_file_path); 51 | const expected_hash = crypto.createHash('md5').update(expected_buffer).digest('hex'); 52 | const buffer_upload_response = await fetch(endpoint_url, { 53 | method: 'POST', 54 | headers: { 55 | 'transfer-encoding': 'chunked', 56 | 'x-file-name': 'request_upload_buffer.jpg', 57 | }, 58 | body: fs.createReadStream(test_file_path), 59 | }); 60 | 61 | // Validate the hash uploaded on the server side with the expected hash from client side 62 | const buffer_upload_body = await buffer_upload_response.json(); 63 | assert_log( 64 | group, 65 | `${candidate} Chunked Transfer Piped Upload With Content Length - ${expected_hash} === ${buffer_upload_body.hash} - ${test_file_stats.size} bytes`, 66 | () => expected_hash === buffer_upload_body.hash 67 | ); 68 | } 69 | 70 | module.exports = { 71 | test_request_chunked_stream, 72 | }; 73 | -------------------------------------------------------------------------------- /tests/components/http/scenarios/request_router_paths_test.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const { assert_log } = require('../../../scripts/operators.js'); 3 | const { HyperExpress, fetch, server } = require('../../../configuration.js'); 4 | const router = new HyperExpress.Router(); 5 | const endpoint = '/tests/request'; 6 | const scenario_endpoint = '/cached-paths'; 7 | const endpoint_url = server.base + endpoint + scenario_endpoint; 8 | 9 | // Create Backend HTTP Route to echo the path of the request 10 | router.get(scenario_endpoint, (req, res) => res.send(req.path)); 11 | router.get(scenario_endpoint + '/:random', (req, res) => res.send(req.path)); 12 | 13 | // Bind router to webserver 14 | const { TEST_SERVER } = require('../../Server.js'); 15 | TEST_SERVER.use(endpoint, router); 16 | 17 | async function test_request_router_paths_test() { 18 | const group = 'REQUEST'; 19 | const candidate = 'HyperExpress.Request.path'; 20 | 21 | // Test the candidates to ensure that the path is being cached properly 22 | const _candidates = []; 23 | const candidates = [ 24 | endpoint_url, 25 | `${endpoint_url}/${crypto.randomUUID()}`, 26 | `${endpoint_url}/${crypto.randomUUID()}`, 27 | ]; 28 | for (const candidate of candidates) { 29 | const response = await fetch(candidate); 30 | const _candidate = await response.text(); 31 | _candidates.push(_candidate); 32 | } 33 | 34 | // Assert that the candidates match 35 | assert_log( 36 | group, 37 | `${candidate} Cached Router Paths Test`, 38 | () => _candidates.join(',') === candidates.map((url) => url.replace(server.base, '')).join(',') 39 | ); 40 | } 41 | 42 | module.exports = { 43 | test_request_router_paths_test, 44 | }; 45 | -------------------------------------------------------------------------------- /tests/components/http/scenarios/request_stream.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const crypto = require('crypto'); 3 | const fs = require('fs'); 4 | const { assert_log } = require('../../../scripts/operators.js'); 5 | const { HyperExpress, fetch, server } = require('../../../configuration.js'); 6 | const router = new HyperExpress.Router(); 7 | const endpoint = '/tests/request'; 8 | const scenario_endpoint = '/stream-pipe'; 9 | const endpoint_url = server.base + endpoint + scenario_endpoint; 10 | const test_file_path = path.resolve(path.join(__dirname, '../../../content/large-image.jpg')); 11 | const test_file_stats = fs.statSync(test_file_path); 12 | 13 | function get_file_write_path(file_name) { 14 | return path.resolve(path.join(__dirname, '../../../content/written/' + file_name)); 15 | } 16 | 17 | // Create Backend HTTP Route 18 | router.post(scenario_endpoint, async (request, response) => { 19 | // Create a writable stream to specified file name path 20 | const file_name = request.headers['x-file-name']; 21 | const path = get_file_write_path(file_name); 22 | const writable = fs.createWriteStream(path); 23 | 24 | // Pipe the readable body stream to the writable and wait for it to finish 25 | request.pipe(writable); 26 | await new Promise((resolve) => writable.once('finish', resolve)); 27 | 28 | // Read the written file's buffer and calculate its md5 hash 29 | const written_buffer = fs.readFileSync(path); 30 | const written_hash = crypto.createHash('md5').update(written_buffer).digest('hex'); 31 | 32 | // Cleanup the written file for future testing 33 | fs.rmSync(path); 34 | 35 | // Return the written hash to be validated on client side 36 | return response.json({ 37 | hash: written_hash, 38 | }); 39 | }); 40 | 41 | // Bind router to webserver 42 | const { TEST_SERVER } = require('../../Server.js'); 43 | TEST_SERVER.use(endpoint, router); 44 | 45 | async function test_request_stream_pipe() { 46 | const group = 'REQUEST'; 47 | const candidate = 'HyperExpress.Request.stream'; 48 | 49 | // Send a buffer of the file in the request body so we have a content-length on server side 50 | const expected_buffer = fs.readFileSync(test_file_path); 51 | const expected_hash = crypto.createHash('md5').update(expected_buffer).digest('hex'); 52 | const buffer_upload_response = await fetch(endpoint_url, { 53 | method: 'POST', 54 | headers: { 55 | 'x-file-name': 'request_upload_buffer.jpg', 56 | }, 57 | body: expected_buffer, 58 | }); 59 | 60 | // Validate the hash uploaded on the server side with the expected hash from client side 61 | const buffer_upload_body = await buffer_upload_response.json(); 62 | assert_log( 63 | group, 64 | `${candidate} Piped Upload With Content Length - ${expected_hash} === ${buffer_upload_body.hash} - ${test_file_stats.size} bytes`, 65 | () => expected_hash === buffer_upload_body.hash 66 | ); 67 | } 68 | 69 | module.exports = { 70 | test_request_stream_pipe, 71 | }; 72 | -------------------------------------------------------------------------------- /tests/components/http/scenarios/request_uncaught_rejections.js: -------------------------------------------------------------------------------- 1 | const { assert_log } = require('../../../scripts/operators.js'); 2 | const { HyperExpress, fetch, server } = require('../../../configuration.js'); 3 | const router = new HyperExpress.Router(); 4 | const endpoint = '/tests/request'; 5 | const scenario_endpoint = '/uncaught-rejection'; 6 | const endpoint_url = server.base + endpoint + scenario_endpoint; 7 | 8 | // Create Backend HTTP Route 9 | router.post(scenario_endpoint, async (request, response) => { 10 | // Retrieve the desired scenario from the request body 11 | const { scenario } = await request.json(); 12 | 13 | // Bind an expected error handler 14 | request.expected_error = (error) => 15 | response.json({ 16 | code: error.message, 17 | }); 18 | 19 | // Trigger a specific error scenario 20 | switch (scenario) { 21 | case 1: 22 | // Manually throw a shallow error 23 | throw new Error('MANUAL_SHALLOW_ERROR'); 24 | case 2: 25 | // Manually throw a deep error 26 | await new Promise((_, reject) => reject(new Error('MANUAL_DEEP_ERROR'))); 27 | case 3: 28 | // Manually thrown non-Error object 29 | throw 'MANUAL_SHALLOW_NON_ERROR'; 30 | case 4: 31 | // Manually thrown non-Error object 32 | await (async () => { 33 | throw 'MANUAL_DEEP_NON_ERROR'; 34 | })(); 35 | default: 36 | return response.json({ 37 | code: 'SUCCESS', 38 | }); 39 | } 40 | }); 41 | 42 | // Bind router to webserver 43 | const { TEST_SERVER } = require('../../Server.js'); 44 | TEST_SERVER.use(endpoint, router); 45 | 46 | async function test_request_uncaught_rejections() { 47 | const group = 'REQUEST'; 48 | const candidate = 'HyperExpress.Request'; 49 | const promises = [ 50 | [1, 'MANUAL_SHALLOW_ERROR'], 51 | [2, 'MANUAL_DEEP_ERROR'], 52 | [3, 'ERR_CAUGHT_NON_ERROR_TYPE: MANUAL_SHALLOW_NON_ERROR'], 53 | [4, 'ERR_CAUGHT_NON_ERROR_TYPE: MANUAL_DEEP_NON_ERROR'], 54 | ].map( 55 | ([scenario, expected_code]) => 56 | new Promise(async (resolve) => { 57 | // Make the fetch request 58 | const response = await fetch(endpoint_url, { 59 | method: 'POST', 60 | body: JSON.stringify({ 61 | scenario, 62 | }), 63 | }); 64 | 65 | // Retrieve the received code from the server 66 | const { code } = await response.json(); 67 | 68 | // Validate the hash uploaded on the server side with the expected hash from client side 69 | assert_log( 70 | group, 71 | `${candidate} Uncaught Rejections Test Scenario ${scenario} => ${code}`, 72 | () => code === expected_code 73 | ); 74 | 75 | // Release this promise 76 | resolve(); 77 | }) 78 | ); 79 | 80 | await Promise.all(promises); 81 | } 82 | 83 | module.exports = { 84 | test_request_uncaught_rejections, 85 | }; 86 | -------------------------------------------------------------------------------- /tests/components/http/scenarios/response_chunked_write.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const crypto = require('crypto'); 3 | const fs = require('fs'); 4 | const { Writable } = require('stream'); 5 | const { assert_log } = require('../../../scripts/operators.js'); 6 | const { HyperExpress, fetch, server } = require('../../../configuration.js'); 7 | const router = new HyperExpress.Router(); 8 | const endpoint = '/tests/response'; 9 | const scenario_endpoint = '/write'; 10 | const endpoint_url = server.base + endpoint + scenario_endpoint; 11 | const test_file_path = path.resolve(path.join(__dirname, '../../../content/large-image.jpg')); 12 | const test_file_stats = fs.statSync(test_file_path); 13 | 14 | function safe_write_chunk(response, chunk, callback) { 15 | return response.write(chunk, 'utf8', callback); 16 | } 17 | 18 | // Create Backend HTTP Route 19 | router.get(scenario_endpoint, async (request, response) => { 20 | // Set some headers to ensure we have proper headers being received 21 | response.header('x-is-written', 'true'); 22 | 23 | // Create a readable stream for test file and stream it 24 | const readable = fs.createReadStream(test_file_path); 25 | 26 | // Create a Writable which we will pipe the readable into 27 | const writable = new Writable({ 28 | write: (chunk, encoding, callback) => { 29 | // Safe write a chunk until it has FULLY been served 30 | safe_write_chunk(response, chunk, callback); 31 | }, 32 | }); 33 | 34 | // Bind event handlers for ending the request once Writable has ended or closed 35 | writable.on('close', () => response.send()); 36 | 37 | // Pipe the readable into the writable we created 38 | readable.pipe(writable); 39 | }); 40 | 41 | // Bind router to webserver 42 | const { TEST_SERVER } = require('../../Server.js'); 43 | TEST_SERVER.use(endpoint, router); 44 | 45 | async function test_response_chunked_write() { 46 | const group = 'RESPONSE'; 47 | const candidate = 'HyperExpress.Response.write()'; 48 | 49 | // Read test file's buffer into memory 50 | const expected_buffer = fs.readFileSync(test_file_path); 51 | const expected_hash = crypto.createHash('md5').update(expected_buffer).digest('hex'); 52 | 53 | // Perform chunked encoding based fetch request to download streamed buffer for test file from server 54 | const chunked_response = await fetch(endpoint_url); 55 | 56 | // Ensure custom headers are received first 57 | assert_log( 58 | group, 59 | `${candidate} Custom Chunked Transfer Write Headers Test`, 60 | () => chunked_response.headers.get('x-is-written') === 'true' 61 | ); 62 | 63 | // Download buffer from request to compare 64 | let received_buffer = await chunked_response.buffer(); 65 | let received_hash = crypto.createHash('md5').update(received_buffer).digest('hex'); 66 | 67 | // Test to see error handler was properly called on expected middleware error 68 | assert_log( 69 | group, 70 | `${candidate} Custom Chunked Transfer Write Buffer/Hash Comparison Test - ${expected_hash} - ${test_file_stats.size} bytes`, 71 | () => expected_buffer.equals(received_buffer) && expected_hash === received_hash 72 | ); 73 | } 74 | 75 | module.exports = { 76 | test_response_chunked_write, 77 | }; 78 | -------------------------------------------------------------------------------- /tests/components/http/scenarios/response_custom_content_length.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const { assert_log } = require('../../../scripts/operators.js'); 3 | const { HyperExpress, fetch, server } = require('../../../configuration.js'); 4 | const router = new HyperExpress.Router(); 5 | const endpoint = '/tests/response'; 6 | const scenario_endpoint = '/custom-content-length'; 7 | const endpoint_url = server.base + endpoint + scenario_endpoint; 8 | 9 | // Generate a random string payload 10 | const payload = crypto.randomBytes(800).toString('hex'); 11 | 12 | // Create Backend HTTP Route 13 | router.get(scenario_endpoint, (_, response) => { 14 | response.header('content-length', payload.length.toString()).send(payload); 15 | }); 16 | 17 | // Bind router to webserver 18 | const { TEST_SERVER } = require('../../Server.js'); 19 | TEST_SERVER.use(endpoint, router); 20 | 21 | async function test_response_custom_content_length() { 22 | const group = 'RESPONSE'; 23 | const candidate = 'HyperExpress.Response.send()'; 24 | 25 | // Send a normal request to trigger the appropriate hooks 26 | const response = await fetch(endpoint_url); 27 | const received = await response.text(); 28 | 29 | // Assert that the received headers all match the expected headers 30 | assert_log(group, `${candidate} Custom Content-Length With Body Test`, () => received === payload); 31 | } 32 | 33 | module.exports = { 34 | test_response_custom_content_length, 35 | }; 36 | -------------------------------------------------------------------------------- /tests/components/http/scenarios/response_custom_status.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const { assert_log } = require('../../../scripts/operators.js'); 3 | const { HyperExpress, fetch, server } = require('../../../configuration.js'); 4 | const router = new HyperExpress.Router(); 5 | const endpoint = '/tests/response'; 6 | const scenario_endpoint = '/custom-status'; 7 | const endpoint_url = server.base + endpoint + scenario_endpoint; 8 | 9 | // Create Backend HTTP Route 10 | router.post(scenario_endpoint, async (request, response) => { 11 | const { status, message } = await request.json(); 12 | response.statusCode = status; 13 | response.statusMessage = message; 14 | response.send(); 15 | }); 16 | 17 | // Bind router to webserver 18 | const { TEST_SERVER } = require('../../Server.js'); 19 | TEST_SERVER.use(endpoint, router); 20 | 21 | async function test_response_custom_status() { 22 | const group = 'RESPONSE'; 23 | const candidate = 'HyperExpress.Response.statusCode'; 24 | 25 | [ 26 | { 27 | status: 200, 28 | message: 'Some Message', 29 | }, 30 | { 31 | status: 609, 32 | message: 'User Moved to Another Server', 33 | }, 34 | ].map(async ({ status, message }) => { 35 | // Make a request to the server with a custom status code and message 36 | const response = await fetch(endpoint_url, { 37 | method: 'POST', 38 | body: JSON.stringify({ 39 | status, 40 | message, 41 | }), 42 | }); 43 | 44 | // Validate the status code and message on the response 45 | assert_log( 46 | group, 47 | `${candidate} Custom Status Code & Response Test - "HTTP ${status} ${message}"`, 48 | () => response.status === status && response.statusText === message 49 | ); 50 | }); 51 | } 52 | 53 | module.exports = { 54 | test_response_custom_status, 55 | }; 56 | -------------------------------------------------------------------------------- /tests/components/http/scenarios/response_headers_behavior.js: -------------------------------------------------------------------------------- 1 | const { assert_log } = require('../../../scripts/operators.js'); 2 | const { HyperExpress, fetch, server } = require('../../../configuration.js'); 3 | const router = new HyperExpress.Router(); 4 | const endpoint = '/tests/response'; 5 | const scenario_endpoint = '/headers-behavior'; 6 | const endpoint_url = server.base + endpoint + scenario_endpoint; 7 | 8 | const RAW_HEADERS = [ 9 | { 10 | name: 'test', 11 | value: 'first', // This will be overwritten by the second header 12 | }, 13 | { 14 | name: 'test', 15 | value: 'second', // This will be overwritten by the third header 16 | }, 17 | { 18 | name: 'test', 19 | value: 'third', // This will be the served header for the the "test" header 20 | }, 21 | ]; 22 | 23 | const RAW_COOKIES = [ 24 | { 25 | name: 'test-cookie', 26 | value: 'test-value', // This will be overwritten by the second cookie 27 | }, 28 | { 29 | name: 'test-cookie', 30 | value: 'test-value-2', // This will be served to the client 31 | }, 32 | { 33 | name: 'test-cookie-3', 34 | value: 'test-value-3', // This will be served to the client 35 | }, 36 | ]; 37 | 38 | // Create Backend HTTP Route 39 | router.get(scenario_endpoint, (request, response) => { 40 | // Serve the headers 41 | RAW_HEADERS.forEach((header) => response.header(header.name, header.value)); 42 | 43 | // Serve the cookies 44 | RAW_COOKIES.forEach((cookie) => response.cookie(cookie.name, cookie.value, 1000 * 60 * 60 * 24 * 7)); 45 | 46 | // Send response 47 | response.send(); 48 | }); 49 | 50 | // Bind router to webserver 51 | const { TEST_SERVER } = require('../../Server.js'); 52 | TEST_SERVER.use(endpoint, router); 53 | 54 | async function test_response_headers_behavior() { 55 | const group = 'RESPONSE'; 56 | const candidate = 'HyperExpress.Response.header()'; 57 | 58 | // Parse the last written header as the expected value 59 | const EXPECTED_HEADERS = {}; 60 | RAW_HEADERS.forEach((header) => { 61 | if (EXPECTED_HEADERS[header.name]) { 62 | if (Array.isArray(EXPECTED_HEADERS[header.name])) { 63 | EXPECTED_HEADERS[header.name].push(header.value); 64 | } else { 65 | EXPECTED_HEADERS[header.name] = [EXPECTED_HEADERS[header.name], header.value]; 66 | } 67 | } else { 68 | EXPECTED_HEADERS[header.name] = header.value; 69 | } 70 | }); 71 | 72 | // Join all multi headers with comma whitespaces 73 | for (const name in EXPECTED_HEADERS) { 74 | if (Array.isArray(EXPECTED_HEADERS[name])) { 75 | EXPECTED_HEADERS[name] = EXPECTED_HEADERS[name].join(', '); 76 | } 77 | } 78 | 79 | // Parse the last written cookie as the expected value 80 | const EXPECTED_COOKIES = {}; 81 | RAW_COOKIES.forEach((cookie) => (EXPECTED_COOKIES[cookie.name] = cookie.value)); 82 | 83 | // Send a fetch request to retrieve headers 84 | const response = await fetch(endpoint_url); 85 | const received_headers = response.headers.raw(); 86 | 87 | // Assert that the headers were served correctly 88 | assert_log(group, `${candidate} - Single/Multiple Header Values Behavior Test`, () => { 89 | let valid = true; 90 | Object.keys(EXPECTED_HEADERS).forEach((name) => { 91 | let expected = EXPECTED_HEADERS[name]; 92 | let received = received_headers[name]; 93 | 94 | // Assert that the received header is an array 95 | valid = Array.isArray(expected) 96 | ? JSON.stringify(expected) === JSON.stringify(received) 97 | : expected === received[0]; 98 | }); 99 | return valid; 100 | }); 101 | 102 | // Assert that the cookies were served correctly 103 | assert_log(group, `${candidate} - Single/Multiple Cookie Values Behavior Test`, () => { 104 | const received_cookies = {}; 105 | received_headers['set-cookie'].forEach((cookie) => { 106 | const [name, value] = cookie.split('; ')[0].split('='); 107 | received_cookies[name] = value; 108 | }); 109 | 110 | let valid = true; 111 | Object.keys(EXPECTED_COOKIES).forEach((name) => { 112 | const expected_value = EXPECTED_COOKIES[name]; 113 | const received_value = received_cookies[name]; 114 | valid = expected_value === received_value; 115 | }); 116 | return valid; 117 | }); 118 | } 119 | 120 | module.exports = { 121 | test_response_headers_behavior, 122 | }; 123 | -------------------------------------------------------------------------------- /tests/components/http/scenarios/response_hooks.js: -------------------------------------------------------------------------------- 1 | const { assert_log, async_wait } = require('../../../scripts/operators.js'); 2 | const { HyperExpress, fetch, server, AbortController } = require('../../../configuration.js'); 3 | const router = new HyperExpress.Router(); 4 | const endpoint = '/tests/response'; 5 | const scenario_endpoint = '/hooks'; 6 | const endpoint_url = server.base + endpoint + scenario_endpoint; 7 | const response_delay = 100; 8 | 9 | const hook_emissions = {}; 10 | function increment_event(type) { 11 | hook_emissions[type] = hook_emissions[type] ? hook_emissions[type] + 1 : 1; 12 | } 13 | 14 | // Create Backend HTTP Route 15 | router.get(scenario_endpoint, (request, response) => { 16 | // Bind all of the hooks to the response 17 | ['abort', 'prepare', 'finish', 'close'].forEach((type) => response.on(type, () => increment_event(type))); 18 | 19 | // Send response after some delay to allow for client to prematurely abort 20 | setTimeout(() => (!response.completed ? response.send() : null), response_delay); 21 | }); 22 | 23 | // Bind router to webserver 24 | const { TEST_SERVER } = require('../../Server.js'); 25 | TEST_SERVER.use(endpoint, router); 26 | 27 | async function test_response_events() { 28 | const group = 'RESPONSE'; 29 | const candidate = 'HyperExpress.Response.on()'; 30 | 31 | // Send a normal request to trigger the appropriate hooks 32 | await fetch(endpoint_url); 33 | 34 | // Assert that only the appropriate hooks were called 35 | assert_log( 36 | group, 37 | `${candidate} - Normal Request Events Test`, 38 | () => hook_emissions['prepare'] === 1 && hook_emissions['finish'] === 1 && hook_emissions['close'] === 1 39 | ); 40 | 41 | // Send and prematurely abort a request to trigger the appropriate hooks 42 | const controller = new AbortController(); 43 | setTimeout(() => controller.abort(), response_delay / 3); 44 | try { 45 | await fetch(endpoint_url, { 46 | signal: controller.signal, 47 | }); 48 | } catch (error) { 49 | // Supress the error as we expect an abort 50 | // Wait a little bit for the hook emissions to be updated 51 | await async_wait(response_delay / 3); 52 | } 53 | 54 | // Assert that only the appropriate hooks were called 55 | assert_log( 56 | group, 57 | `${candidate} - Premature Aborted Request Events Test`, 58 | () => 59 | hook_emissions['prepare'] === 1 && 60 | hook_emissions['finish'] === 1 && 61 | hook_emissions['close'] === 2 && 62 | hook_emissions['abort'] === 1 63 | ); 64 | } 65 | 66 | module.exports = { 67 | test_response_events, 68 | }; 69 | -------------------------------------------------------------------------------- /tests/components/http/scenarios/response_piped.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const crypto = require('crypto'); 3 | const fs = require('fs'); 4 | const { assert_log } = require('../../../scripts/operators.js'); 5 | const { HyperExpress, fetch, server } = require('../../../configuration.js'); 6 | const router = new HyperExpress.Router(); 7 | const endpoint = '/tests/response'; 8 | const scenario_endpoint = '/pipe'; 9 | const endpoint_url = server.base + endpoint + scenario_endpoint; 10 | const test_file_path = path.resolve(path.join(__dirname, '../../../content/large-image.jpg')); 11 | const test_file_stats = fs.statSync(test_file_path); 12 | 13 | // Create Backend HTTP Route 14 | router.get(scenario_endpoint, async (request, response) => { 15 | // Set some headers to ensure we have proper headers being received 16 | response.header('x-is-written', 'true'); 17 | 18 | // Create a readable stream for test file and stream it 19 | const readable = fs.createReadStream(test_file_path); 20 | 21 | // Pipe the readable stream into the response 22 | readable.pipe(response); 23 | }); 24 | 25 | // Bind router to webserver 26 | const { TEST_SERVER } = require('../../Server.js'); 27 | TEST_SERVER.use(endpoint, router); 28 | 29 | async function test_response_piped_write() { 30 | const group = 'RESPONSE'; 31 | const candidate = 'HyperExpress.Response.write()'; 32 | 33 | // Read test file's buffer into memory 34 | const expected_buffer = fs.readFileSync(test_file_path); 35 | const expected_hash = crypto.createHash('md5').update(expected_buffer).digest('hex'); 36 | 37 | // Perform chunked encoding based fetch request to download streamed buffer for test file from server 38 | const chunked_response = await fetch(endpoint_url); 39 | 40 | // Ensure custom headers are received first 41 | assert_log( 42 | group, 43 | `${candidate} Piped Stream Write Headers Test`, 44 | () => chunked_response.headers.get('x-is-written') === 'true' 45 | ); 46 | 47 | // Download buffer from request to compare 48 | let received_buffer = await chunked_response.buffer(); 49 | let received_hash = crypto.createHash('md5').update(received_buffer).digest('hex'); 50 | 51 | // Test to see error handler was properly called on expected middleware error 52 | assert_log( 53 | group, 54 | `${candidate} Piped Stream Write Buffer/Hash Comparison Test - ${expected_hash} - ${test_file_stats.size} bytes`, 55 | () => expected_buffer.equals(received_buffer) && expected_hash === received_hash 56 | ); 57 | } 58 | 59 | module.exports = { 60 | test_response_piped_write, 61 | }; 62 | -------------------------------------------------------------------------------- /tests/components/http/scenarios/response_send_no_body.js: -------------------------------------------------------------------------------- 1 | const { assert_log } = require('../../../scripts/operators.js'); 2 | const { HyperExpress, fetch, server } = require('../../../configuration.js'); 3 | const router = new HyperExpress.Router(); 4 | const endpoint = '/tests/response'; 5 | const scenario_endpoint = '/send-no-body'; 6 | const endpoint_url = server.base + endpoint + scenario_endpoint; 7 | 8 | // Create Backend HTTP Route 9 | const response_headers = [ 10 | ['Content-Type', 'application/json'], 11 | ['Content-Length', Math.floor(Math.random() * 1e5).toString()], 12 | ['Last-Modified', new Date().toUTCString()], 13 | ['ETag', 'W/"' + Math.floor(Math.random() * 1e5).toString() + '"'], 14 | ]; 15 | 16 | router.head(scenario_endpoint, (_, response) => { 17 | // Write the response headers 18 | response_headers.forEach(([key, value]) => response.header(key, value)); 19 | 20 | // Should send without body under the hood with the custom content-length 21 | return response.vary('Accept-Encoding').send(); 22 | }); 23 | 24 | // Bind router to webserver 25 | const { TEST_SERVER } = require('../../Server.js'); 26 | TEST_SERVER.use(endpoint, router); 27 | 28 | async function test_response_send_no_body() { 29 | const group = 'RESPONSE'; 30 | const candidate = 'HyperExpress.Response.send()'; 31 | 32 | // Send a normal request to trigger the appropriate hooks 33 | const response = await fetch(endpoint_url, { 34 | method: 'HEAD', 35 | }); 36 | 37 | // Assert that the received headers all match the expected headers 38 | assert_log(group, `${candidate} Custom Content-Length Without Body Test`, () => { 39 | let verdict = true; 40 | response_headers.forEach(([key, value]) => { 41 | if (response.headers.get(key) !== value) verdict = false; 42 | }); 43 | return verdict; 44 | }); 45 | } 46 | 47 | module.exports = { 48 | test_response_send_no_body, 49 | }; 50 | -------------------------------------------------------------------------------- /tests/components/http/scenarios/response_send_status.js: -------------------------------------------------------------------------------- 1 | const { assert_log } = require('../../../scripts/operators.js'); 2 | const { HyperExpress, fetch, server } = require('../../../configuration.js'); 3 | const router = new HyperExpress.Router(); 4 | const endpoint = '/tests/response'; 5 | const scenario_endpoint = '/send-status'; 6 | const endpoint_url = server.base + endpoint + scenario_endpoint; 7 | 8 | // Create Backend HTTP Route 9 | router.post(scenario_endpoint, async (request, response) => { 10 | const { status } = await request.json(); 11 | response.sendStatus(status); 12 | }); 13 | 14 | // Bind router to webserver 15 | const { TEST_SERVER } = require('../../Server.js'); 16 | TEST_SERVER.use(endpoint, router); 17 | 18 | async function test_response_send_status() { 19 | const group = 'RESPONSE'; 20 | const candidate = 'HyperExpress.Response.statusCode'; 21 | 22 | [ 23 | { 24 | status: 200, 25 | }, 26 | { 27 | status: 609, 28 | }, 29 | ].map(async ({ status }) => { 30 | // Make a request to the server with a status code 31 | const response = await fetch(endpoint_url, { 32 | method: 'POST', 33 | body: JSON.stringify({ 34 | status, 35 | }), 36 | }); 37 | 38 | // Validate the status code on the response 39 | assert_log( 40 | group, 41 | `${candidate} Custom Status Code & Response Test - "HTTP ${status}"`, 42 | () => response.status === status 43 | ); 44 | }); 45 | } 46 | 47 | module.exports = { 48 | test_response_send_status, 49 | }; 50 | -------------------------------------------------------------------------------- /tests/components/http/scenarios/response_set_header.js: -------------------------------------------------------------------------------- 1 | const { assert_log } = require('../../../scripts/operators.js'); 2 | const { fetch, server } = require('../../../configuration.js'); 3 | const { TEST_SERVER } = require('../../Server.js'); 4 | 5 | const endpoint = '/tests/response/set'; 6 | const endpoint_url = server.base + endpoint; 7 | 8 | // Create Backend HTTP Route 9 | TEST_SERVER.get(endpoint, async (request, response) => { 10 | response.set({ 'test-header-1': 'test-value-1' }); 11 | response.set('test-header-2', 'test-value-2'); 12 | return response.end(); 13 | }); 14 | 15 | async function test_response_set_header() { 16 | let group = 'RESPONSE'; 17 | let candidate = 'HyperExpress.Response.set()'; 18 | 19 | // Perform fetch request 20 | const response = await fetch(endpoint_url); 21 | const headers = response.headers.raw(); 22 | 23 | assert_log( 24 | group, 25 | candidate + ' Set Header Test', 26 | () => { 27 | return headers['test-header-1'] == 'test-value-1' 28 | && headers['test-header-2'] == 'test-value-2'; 29 | } 30 | ); 31 | } 32 | 33 | module.exports = { 34 | test_response_set_header, 35 | }; 36 | -------------------------------------------------------------------------------- /tests/components/http/scenarios/response_sse.js: -------------------------------------------------------------------------------- 1 | const { assert_log, async_wait } = require('../../../scripts/operators.js'); 2 | const { HyperExpress, server, EventSource } = require('../../../configuration.js'); 3 | const router = new HyperExpress.Router(); 4 | const endpoint = '/tests/response'; 5 | const scenario_endpoint = '/sse'; 6 | const endpoint_url = server.base + endpoint + scenario_endpoint; 7 | 8 | const test_data = [ 9 | { 10 | data: 'asdasdasd', 11 | }, 12 | { 13 | data: 'xasdxasdxasd', 14 | }, 15 | { 16 | event: 'x3123x123x', 17 | data: 'xasdasdasdxasd', 18 | }, 19 | { 20 | event: '3x123x123x', 21 | data: '123123x123x12', 22 | }, 23 | { 24 | id: '3x12x123x123x', 25 | event: 'x3123x123', 26 | data: 'x123x123x123x123', 27 | }, 28 | { 29 | data: 'x3123x123x1231', 30 | }, 31 | ]; 32 | 33 | // Create Backend HTTP Route to serve test data 34 | let sse_closed = false; 35 | router.get(scenario_endpoint, async (request, response) => { 36 | // Ensure SSE is available for this request 37 | if (response.sse) { 38 | // Open the SSE connection to ensure the client is properly connected 39 | response.sse.open(); 40 | 41 | // Serve the appropriate test data after a short delay 42 | await async_wait(5); 43 | test_data.forEach(({ id, event, data }) => { 44 | // Send with the appropriate parameters based on the test data 45 | let output; 46 | if (id && event && data) { 47 | output = response.sse.send(id, event, data); 48 | } else if (event && data) { 49 | output = response.sse.send(event, data); 50 | } else { 51 | output = response.sse.send(data); 52 | } 53 | 54 | if (!output) console.log(`Failed to send SSE message: ${id}, ${event}, ${data}`); 55 | }); 56 | 57 | // Listen for the client to close the connection 58 | response.once('abort', () => (sse_closed = true)); 59 | response.once('close', () => (sse_closed = true)); 60 | } 61 | }); 62 | 63 | // Bind router to webserver 64 | const { TEST_SERVER } = require('../../Server.js'); 65 | TEST_SERVER.use(endpoint, router); 66 | 67 | async function test_response_sse() { 68 | const group = 'RESPONSE'; 69 | const candidate = 'HyperExpress.Response.sse'; 70 | 71 | // Open a new SSE connection to the server 72 | const sse = new EventSource(endpoint_url); 73 | 74 | // Record all of the incoming events to assert against test data 75 | const recorded_data = []; 76 | const recorded_ids = []; 77 | const record_event = (event, customEvent) => { 78 | // Determine various properties about this event 79 | const is_custom_id = Number.isNaN(+event.lastEventId); 80 | const is_recorded_id = recorded_ids.includes(event.lastEventId); 81 | const data = event.data; 82 | 83 | // Build the event based on recorded properties 84 | const payload = {}; 85 | if (is_custom_id && !is_recorded_id) payload.id = event.lastEventId; 86 | if (customEvent) payload.event = customEvent; 87 | if (data) payload.data = data; 88 | recorded_data.push(payload); 89 | 90 | // Remember the event ID for future reference as the last event ID does not reset 91 | if (is_custom_id && !is_recorded_id) recorded_ids.push(event.lastEventId); 92 | }; 93 | 94 | // Bind custom event handlers from test data array 95 | test_data.forEach(({ event }) => (event ? sse.addEventListener(event, (ev) => record_event(ev, event)) : null)); 96 | 97 | // Bind a catch-all message handler 98 | sse.onmessage = record_event; 99 | 100 | // Wait for the connection to initially open and disconnect 101 | let interval; 102 | await new Promise((resolve, reject) => { 103 | sse.onerror = reject; 104 | 105 | // Wait for all test data to be received 106 | interval = setInterval(() => { 107 | if (recorded_data.length >= test_data.length) resolve(); 108 | }, 100); 109 | }); 110 | clearInterval(interval); 111 | 112 | // Close the connection 113 | sse.close(); 114 | 115 | // Let the server propogate the boolean value 116 | await async_wait(5); 117 | 118 | // Assert that all test data was received successfully 119 | assert_log( 120 | group, 121 | `${candidate} - Server-Sent Events Communiciation Test`, 122 | () => 123 | sse_closed && 124 | test_data.find( 125 | (test) => 126 | recorded_data.find( 127 | (recorded) => 128 | test.id === recorded.id && test.event === recorded.event && test.data === recorded.data 129 | ) === undefined 130 | ) === undefined 131 | ); 132 | } 133 | 134 | module.exports = { 135 | test_response_sse, 136 | }; 137 | -------------------------------------------------------------------------------- /tests/components/http/scenarios/response_stream.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const crypto = require('crypto'); 3 | const fs = require('fs'); 4 | const { assert_log } = require('../../../scripts/operators.js'); 5 | const { HyperExpress, fetch, server } = require('../../../configuration.js'); 6 | const router = new HyperExpress.Router(); 7 | const endpoint = '/tests/response'; 8 | const scenario_endpoint = '/stream'; 9 | const endpoint_url = server.base + endpoint + scenario_endpoint; 10 | const test_file_path = path.resolve(path.join(__dirname, '../../../content/large-image.jpg')); 11 | const test_file_stats = fs.statSync(test_file_path); 12 | 13 | // Create Backend HTTP Route 14 | router.get(scenario_endpoint, async (request, response) => { 15 | // Set some headers to ensure we have proper headers being received 16 | response.header('x-is-streamed', 'true'); 17 | 18 | // Create a readable stream for test file and stream it 19 | const readable = fs.createReadStream(test_file_path); 20 | 21 | // Deliver with chunked encoding if specified by header or fall back to normal handled delivery 22 | const use_chunked_encoding = request.headers['x-chunked-encoding'] === 'true'; 23 | response.stream(readable, use_chunked_encoding ? undefined : test_file_stats.size); 24 | }); 25 | 26 | // Bind router to webserver 27 | const { TEST_SERVER } = require('../../Server.js'); 28 | TEST_SERVER.use(endpoint, router); 29 | 30 | async function test_response_stream_method() { 31 | const group = 'RESPONSE'; 32 | const candidate = 'HyperExpress.Response.stream()'; 33 | 34 | // Read test file's buffer into memory 35 | const expected_buffer = fs.readFileSync(test_file_path); 36 | const expected_hash = crypto.createHash('md5').update(expected_buffer).digest('hex'); 37 | 38 | // Perform chunked encoding based fetch request to download streamed buffer for test file from server 39 | const chunked_response = await fetch(endpoint_url, { 40 | headers: { 41 | 'x-chunked-encoding': 'true', 42 | }, 43 | }); 44 | 45 | // Ensure custom headers are received first 46 | assert_log( 47 | group, 48 | `${candidate} Chunked Transfer Streamed Headers Test`, 49 | () => chunked_response.headers.get('x-is-streamed') === 'true' 50 | ); 51 | 52 | // Download buffer from request to compare 53 | let received_buffer = await chunked_response.buffer(); 54 | let received_hash = crypto.createHash('md5').update(received_buffer).digest('hex'); 55 | 56 | // Test to see error handler was properly called on expected middleware error 57 | assert_log( 58 | group, 59 | `${candidate} Chunked Transfer Streamed Buffer/Hash Comparison Test - ${expected_hash} - ${test_file_stats.size} bytes`, 60 | () => { 61 | const matches = expected_buffer.equals(received_buffer) && expected_hash === received_hash; 62 | if (!matches) { 63 | console.log({ 64 | expected_buffer, 65 | received_buffer, 66 | expected_hash, 67 | received_hash, 68 | }); 69 | } 70 | 71 | return matches; 72 | } 73 | ); 74 | 75 | // Perform handled response based fetch request to download streamed buffer for test file from server 76 | const handled_response = await fetch(endpoint_url); 77 | 78 | // Ensure custom headers are received and a valid content-length is also received 79 | assert_log( 80 | group, 81 | `${candidate} Handled Response Streamed Headers & Content-Length Test`, 82 | () => 83 | handled_response.headers.get('x-is-streamed') === 'true' && 84 | +handled_response.headers.get('content-length') === expected_buffer.byteLength 85 | ); 86 | 87 | // Download buffer from request to compare 88 | received_buffer = await handled_response.buffer(); 89 | received_hash = crypto.createHash('md5').update(received_buffer).digest('hex'); 90 | 91 | // Test to see error handler was properly called on expected middleware error 92 | assert_log( 93 | group, 94 | `${candidate} Handled Response Streamed Buffer/Hash Comparison Test - ${expected_hash} - ${test_file_stats.size} bytes`, 95 | () => expected_buffer.equals(received_buffer) && expected_hash === received_hash 96 | ); 97 | } 98 | 99 | module.exports = { 100 | test_response_stream_method, 101 | }; 102 | -------------------------------------------------------------------------------- /tests/components/http/scenarios/response_stream_sync_writes.js: -------------------------------------------------------------------------------- 1 | const { assert_log } = require('../../../scripts/operators.js'); 2 | const { HyperExpress, fetch, server } = require('../../../configuration.js'); 3 | const router = new HyperExpress.Router(); 4 | const endpoint = '/tests/response'; 5 | const scenario_endpoint = '/sync-writes'; 6 | const endpoint_url = server.base + endpoint + scenario_endpoint; 7 | 8 | const expected_parts = ['1', '2', '3', 'done']; 9 | 10 | // Create Backend HTTP Route 11 | router.get(scenario_endpoint, (request, response) => { 12 | // Write the first 3 parts with response.write() 13 | response.write(expected_parts[0]); 14 | response.write(expected_parts[1]); 15 | response.write(expected_parts[2]); 16 | 17 | // Send the last part with response.send() 18 | response.send(expected_parts[3]); 19 | }); 20 | 21 | // Bind router to webserver 22 | const { TEST_SERVER } = require('../../Server.js'); 23 | TEST_SERVER.use(endpoint, router); 24 | 25 | async function test_response_sync_writes() { 26 | const group = 'RESPONSE'; 27 | const candidate = 'HyperExpress.Response.write()'; 28 | 29 | // Make a fetch request to the endpoint 30 | const response = await fetch(endpoint_url); 31 | 32 | // Get the received body from the response 33 | const expected_body = expected_parts.join(''); 34 | const received_body = await response.text(); 35 | 36 | // Ensure that the received body is the same as the expected body 37 | assert_log(group, `${candidate} Sync Writes Test`, () => expected_body === received_body); 38 | } 39 | 40 | module.exports = { 41 | test_response_sync_writes, 42 | }; 43 | -------------------------------------------------------------------------------- /tests/components/router/Router.js: -------------------------------------------------------------------------------- 1 | const { log, assert_log } = require('../../scripts/operators.js'); 2 | const { HyperExpress, fetch, server } = require('../../configuration.js'); 3 | const { test_router_chainable_route } = require('./scenarios/chainable_routes.js'); 4 | const { TEST_SERVER } = require('../Server.js'); 5 | const endpoint_base = '/tests/router/echo-'; 6 | 7 | // Inject middleweare signature values into the requests through global, local and route specific middlewares 8 | const middleware_signature = [Math.random(), Math.random(), Math.random()]; 9 | TEST_SERVER.use((request, response, next) => { 10 | // Initialize with first signature value 11 | request.middleware_signature = [middleware_signature[0]]; 12 | next(); 13 | }); 14 | 15 | // Define all HTTP test method definitions 16 | const route_definitions = { 17 | get: { 18 | method: 'GET', 19 | call: 'get', 20 | }, 21 | post: { 22 | method: 'POST', 23 | call: 'post', 24 | }, 25 | del: { 26 | method: 'DELETE', 27 | call: 'delete', 28 | }, 29 | options: { 30 | method: 'OPTIONS', 31 | call: 'options', 32 | }, 33 | patch: { 34 | method: 'PATCH', 35 | call: 'patch', 36 | }, 37 | put: { 38 | method: 'PUT', 39 | call: 'put', 40 | }, 41 | trace: { 42 | method: 'TRACE', 43 | call: 'trace', 44 | }, 45 | }; 46 | 47 | // Create dynamic routes for testing across all methods 48 | const router = new HyperExpress.Router(); 49 | Object.keys(route_definitions).forEach((type) => { 50 | const { method, call } = route_definitions[type]; 51 | router[call]( 52 | endpoint_base + type, 53 | async (request, response) => { 54 | // Push the third signature value to the request 55 | request.middleware_signature.push(middleware_signature[2]); 56 | }, 57 | (request, response) => { 58 | // Echo the methods, call and signature values to the client 59 | response.json({ 60 | method: request.method, 61 | signature: request.middleware_signature, 62 | }); 63 | } 64 | ); 65 | }); 66 | TEST_SERVER.use(router); 67 | 68 | // Bind a second global middleware 69 | TEST_SERVER.use((request, response, next) => { 70 | // Push the second signature value to the request 71 | request.middleware_signature.push(middleware_signature[1]); 72 | next(); 73 | }); 74 | 75 | async function test_router_object() { 76 | // Prepare Test Candidates 77 | log('ROUTER', 'Testing HyperExpress.Router Object...'); 78 | const group = 'ROUTER'; 79 | const candidate = 'HyperExpress.Router'; 80 | const start_time = Date.now(); 81 | 82 | // Test all route definitions to ensure consistency 83 | await Promise.all( 84 | Object.keys(route_definitions).map(async (type) => { 85 | // Retrieve the expected method and call values 86 | const { method, call } = route_definitions[type]; 87 | 88 | // Make the fetch request 89 | const response = await fetch(server.base + endpoint_base + type, { 90 | method, 91 | }); 92 | 93 | // Retrieve the response body 94 | const body = await response.json(); 95 | 96 | // Assert the response body 97 | assert_log(group, `${candidate}.${call}() - HTTP ${method} Test`, () => { 98 | const call_check = typeof router[call] == 'function'; 99 | const method_check = method === body.method; 100 | const signature_check = JSON.stringify(body.signature) === JSON.stringify(middleware_signature); 101 | const route_check = TEST_SERVER.routes[type][endpoint_base + type] !== undefined; 102 | return call_check && method_check && signature_check && route_check; 103 | }); 104 | }) 105 | ); 106 | 107 | // Test the chainable route scenario 108 | await test_router_chainable_route(); 109 | 110 | log(group, `Finished Testing ${candidate} In ${Date.now() - start_time}ms\n`); 111 | } 112 | 113 | module.exports = { 114 | test_router_object, 115 | }; 116 | -------------------------------------------------------------------------------- /tests/components/router/scenarios/chainable_routes.js: -------------------------------------------------------------------------------- 1 | const { assert_log } = require('../../../scripts/operators.js'); 2 | const { HyperExpress, fetch, server } = require('../../../configuration.js'); 3 | const endpoint = '/tests/router'; 4 | const scenario_endpoint = '/chainable-routes'; 5 | const endpoint_url = server.base + endpoint + scenario_endpoint; 6 | 7 | const routes = [ 8 | { 9 | method: 'GET', 10 | payload: Math.random().toString(), 11 | }, 12 | { 13 | method: 'POST', 14 | payload: Math.random().toString(), 15 | }, 16 | { 17 | method: 'PUT', 18 | payload: Math.random().toString(), 19 | }, 20 | { 21 | method: 'DELETE', 22 | payload: Math.random().toString(), 23 | }, 24 | ]; 25 | 26 | const router = new HyperExpress.Router(); 27 | 28 | let chainable = router.route(scenario_endpoint); 29 | for (const route of routes) { 30 | // This will test the chainability of the router 31 | // Simulates Router.route().get().post().put().delete() 32 | chainable = chainable[route.method.toLowerCase()]((_, response) => { 33 | response.send(route.payload); 34 | }); 35 | } 36 | 37 | // Bind router to webserver 38 | const { TEST_SERVER } = require('../../Server.js'); 39 | TEST_SERVER.use(endpoint, router); 40 | 41 | async function test_router_chainable_route() { 42 | const group = 'REQUEST'; 43 | const candidate = 'HyperExpress.Router.route()'; 44 | 45 | // Perform fetch requests for each method 46 | for (const route of routes) { 47 | const response = await fetch(endpoint_url, { 48 | method: route.method, 49 | }); 50 | 51 | // Assert that the payload matches payload sent 52 | const _payload = await response.text(); 53 | assert_log(group, `${candidate} Chained HTTP ${route.method} Route`, () => _payload === route.payload); 54 | } 55 | } 56 | 57 | module.exports = { 58 | test_router_chainable_route, 59 | }; 60 | -------------------------------------------------------------------------------- /tests/components/ws/WebsocketRoute.js: -------------------------------------------------------------------------------- 1 | const { log, random_string, assert_log } = require('../../scripts/operators.js'); 2 | const { HyperExpress, Websocket, server } = require('../../configuration.js'); 3 | 4 | const Router = new HyperExpress.Router(); 5 | const TestPath = '/websocket-route'; 6 | const TestPayload = random_string(30); 7 | const TestKey = random_string(30); 8 | const TestOptions = { 9 | idle_timeout: 500, 10 | message_type: 'String', 11 | compression: HyperExpress.compressors.DISABLED, 12 | max_backpressure: 512 * 1024, 13 | max_payload_length: 16 * 1024, 14 | }; 15 | 16 | // Create websocket route for testing default upgrade handler 17 | Router.ws('/unprotected', TestOptions, (ws) => { 18 | // Send test payload and close if successful 19 | if (ws.send(TestPayload)) ws.close(); 20 | }); 21 | 22 | // Create upgrade route for testing user assigned upgrade handler 23 | Router.upgrade('/protected', (request, response) => { 24 | // Reject upgrade request if valid key is not provided 25 | const key = request.query_parameters['key']; 26 | if (key !== TestKey) return response.status(403).send(); 27 | 28 | // Upgrade request normally 29 | response.upgrade({ 30 | key, 31 | }); 32 | }); 33 | 34 | // Create websocket route for handling protected upgrade 35 | Router.ws('/protected', (ws) => { 36 | // Send test payload and close if successful 37 | if (ws.send(TestPayload)) ws.close(); 38 | }); 39 | 40 | // Bind router to test server instance 41 | const { TEST_SERVER } = require('../../components/Server.js'); 42 | TEST_SERVER.use(TestPath, Router); 43 | 44 | async function test_websocket_route() { 45 | const group = 'WEBSOCKET'; 46 | const candidate = 'HyperExpress.WebsocketRoute'; 47 | const endpoint_base = `${server.base.replace('http', 'ws')}${TestPath}`; 48 | log(group, 'Testing ' + candidate); 49 | 50 | // Test unprotected websocket route upgrade handling 51 | const ws_unprotected = new Websocket(`${endpoint_base}/unprotected`); 52 | await new Promise((resolve, reject) => { 53 | // Store last message to test payload integrity 54 | let last_message; 55 | ws_unprotected.on('message', (message) => { 56 | last_message = message.toString(); 57 | }); 58 | 59 | // Create a reject timeout to throw on hangups 60 | let timeout = setTimeout(reject, 1000); 61 | ws_unprotected.on('close', () => { 62 | // Perform assertion to test for valid last message 63 | assert_log(group, `${candidate} Default/Unprotected Upgrade Handler`, () => last_message === TestPayload); 64 | 65 | // Cancel reject timeout and move on after assertion succeeds 66 | clearTimeout(timeout); 67 | resolve(); 68 | }); 69 | }); 70 | 71 | // Test protected websocket route upgrade handling (NO KEY) 72 | const ws_protected_nokey = new Websocket(`${endpoint_base}/protected`); 73 | await new Promise((resolve, reject) => { 74 | // Store last error so we can compare the expected error type 75 | let last_error; 76 | ws_protected_nokey.on('error', (error) => { 77 | last_error = error; 78 | }); 79 | 80 | // Create a reject timeout to throw on hangups 81 | let timeout = setTimeout(reject, 1000); 82 | ws_protected_nokey.on('close', () => { 83 | // Perform assertion to test for valid last message 84 | assert_log( 85 | group, 86 | `${candidate} Protected Upgrade Handler Rejection With No Key`, 87 | () => last_error && last_error.message.indexOf('403') > -1 88 | ); 89 | 90 | // Cancel reject timeout and move on after assertion succeeds 91 | clearTimeout(timeout); 92 | resolve(); 93 | }); 94 | }); 95 | 96 | // Test protected websocket route upgrade handling (WITH KEY) 97 | const ws_protected_key = new Websocket(`${endpoint_base}/protected?key=${TestKey}`); 98 | await new Promise((resolve, reject) => { 99 | // Store last message to test payload integrity 100 | let last_message; 101 | ws_protected_key.on('message', (message) => { 102 | last_message = message.toString(); 103 | }); 104 | 105 | // Create a reject timeout to throw on hangups 106 | let timeout = setTimeout(reject, 1000); 107 | ws_protected_key.on('close', () => { 108 | // Perform assertion to test for valid last message 109 | assert_log(group, `${candidate} Protected Upgrade Handler With Key`, () => last_message === TestPayload); 110 | 111 | // Cancel reject timeout and move on after assertion succeeds 112 | clearTimeout(timeout); 113 | resolve(); 114 | }); 115 | }); 116 | 117 | log(group, `Finished Testing ${candidate}\n`); 118 | } 119 | 120 | module.exports = { 121 | test_websocket_route, 122 | }; 123 | -------------------------------------------------------------------------------- /tests/components/ws/scenarios/stream.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const crypto = require('crypto'); 3 | const fs = require('fs'); 4 | const { assert_log } = require('../../../scripts/operators.js'); 5 | const { HyperExpress, Websocket, server } = require('../../../configuration.js'); 6 | 7 | const Router = new HyperExpress.Router(); 8 | const TestPath = '/websocket-component'; 9 | const TestFilePath = path.resolve(path.join(__dirname, '../../../content/large-image.jpg')); 10 | 11 | // Create an endpoint for serving a file 12 | Router.ws('/stream', async (ws) => { 13 | // Create a readable stream to serve to the receiver 14 | const readable = fs.createReadStream(TestFilePath); 15 | 16 | // Stream the readable stream to the receiver 17 | await ws.stream(readable); 18 | 19 | // Close the connection once we are done streaming 20 | ws.close(); 21 | }); 22 | 23 | // Bind router to test server instance 24 | const { TEST_SERVER } = require('../../../components/Server.js'); 25 | TEST_SERVER.use(TestPath, Router); 26 | 27 | async function test_websocket_stream() { 28 | const group = 'WEBSOCKET'; 29 | const candidate = 'HyperExpress.Websocket.stream()'; 30 | const endpoint_base = `${server.base.replace('http', 'ws')}${TestPath}`; 31 | 32 | // Read test file's buffer into memory 33 | const expected_buffer = fs.readFileSync(TestFilePath); 34 | const expected_hash = crypto.createHash('md5').update(expected_buffer).digest('hex'); 35 | 36 | // Test protected websocket route upgrade handling (NO KEY) 37 | const ws_stream = new Websocket(`${endpoint_base}/stream`); 38 | await new Promise((resolve, reject) => { 39 | let received_buffer; 40 | let received_hash; 41 | 42 | // Assign a message handler to receive from websocket 43 | ws_stream.on('message', (message) => { 44 | // Store the received buffer and its hash 45 | received_buffer = message; 46 | received_hash = crypto.createHash('md5').update(received_buffer).digest('hex'); 47 | }); 48 | 49 | // Assign a close handler to handle assertion 50 | ws_stream.on('close', () => { 51 | // Perform assertion to compare buffers and hashes 52 | assert_log( 53 | group, 54 | `${candidate} - Streamed Binary Buffer Integrity - [${expected_hash}] == [${received_hash}]`, 55 | () => expected_buffer.equals(received_buffer) && expected_hash === received_hash 56 | ); 57 | resolve(); 58 | }); 59 | }); 60 | } 61 | 62 | module.exports = { 63 | test_websocket_stream, 64 | }; 65 | -------------------------------------------------------------------------------- /tests/components/ws/scenarios/writable.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const crypto = require('crypto'); 3 | const fs = require('fs'); 4 | const { assert_log } = require('../../../scripts/operators.js'); 5 | const { HyperExpress, Websocket, server } = require('../../../configuration.js'); 6 | 7 | const Router = new HyperExpress.Router(); 8 | const TestPath = '/websocket-component'; 9 | const TestFilePath = path.resolve(path.join(__dirname, '../../../content/large-image.jpg')); 10 | 11 | // Create an endpoint for serving a file 12 | Router.ws('/writable', async (ws) => { 13 | // Create a readable stream to serve to the receiver 14 | let readable = fs.createReadStream(TestFilePath); 15 | 16 | // Pipe the readable into the websocket writable 17 | readable.pipe(ws.writable); 18 | 19 | // Bind a handler for once readable is finished 20 | readable.once('close', () => { 21 | // Repeat the same process as above to test multiple pipes to the same websocket connection 22 | readable = fs.createReadStream(TestFilePath); 23 | readable.pipe(ws.writable); 24 | 25 | // Bind the end handler again to close the connection this time 26 | readable.once('close', () => ws.close()); 27 | }); 28 | }); 29 | 30 | // Bind router to test server instance 31 | const { TEST_SERVER } = require('../../../components/Server.js'); 32 | TEST_SERVER.use(TestPath, Router); 33 | 34 | async function test_websocket_writable() { 35 | const group = 'WEBSOCKET'; 36 | const candidate = 'HyperExpress.Websocket.writable'; 37 | const endpoint_base = `${server.base.replace('http', 'ws')}${TestPath}`; 38 | 39 | // Read test file's buffer into memory 40 | const expected_buffer = fs.readFileSync(TestFilePath); 41 | const expected_hash = crypto.createHash('md5').update(expected_buffer).digest('hex'); 42 | 43 | // Test protected websocket route upgrade handling (NO KEY) 44 | const ws_writable = new Websocket(`${endpoint_base}/writable`); 45 | await new Promise((resolve, reject) => { 46 | // Assign a message handler to receive from websocket 47 | let counter = 1; 48 | ws_writable.on('message', (message) => { 49 | // Derive the retrieved buffer and its hash 50 | const received_buffer = message; 51 | const received_hash = crypto.createHash('md5').update(received_buffer).digest('hex'); 52 | 53 | // Assert the received data against the expected data 54 | assert_log( 55 | group, 56 | `${candidate} - Piped Binary Buffer Integrity #${counter} - [${expected_hash}] == [${received_hash}]`, 57 | () => expected_buffer.equals(received_buffer) && expected_hash === received_hash 58 | ); 59 | counter++; 60 | }); 61 | 62 | // Assign a close handler to handle assertion 63 | ws_writable.on('close', () => resolve()); 64 | }); 65 | } 66 | 67 | module.exports = { 68 | test_websocket_writable, 69 | }; 70 | -------------------------------------------------------------------------------- /tests/configuration.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const fetch = require('node-fetch'); 3 | const Websocket = require('ws'); 4 | const EventSource = require('eventsource'); 5 | const HyperExpress = require('../index.js'); 6 | const AbortController = require('abort-controller'); 7 | 8 | const patchedFetch = (url, options = {}) => { 9 | // Use a different http agent for each request to prevent connection pooling 10 | options.agent = new http.Agent({ keepAlive: false }); 11 | return fetch(url, options); 12 | }; 13 | 14 | module.exports = { 15 | fetch: patchedFetch, 16 | Websocket, 17 | EventSource, 18 | HyperExpress, 19 | AbortController, 20 | server: { 21 | host: '127.0.0.1', 22 | port: '8080', // Ports should always be numbers but we are maintaining compatibility with strings 23 | secure_port: 8443, 24 | base: 'http://127.0.0.1:8080', 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /tests/content/example.txt: -------------------------------------------------------------------------------- 1 | This is some example text -------------------------------------------------------------------------------- /tests/content/large-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartikk221/hyper-express/d859aacb8802cb6d952b126818750ed036d1a2b3/tests/content/large-image.jpg -------------------------------------------------------------------------------- /tests/content/test-body.json: -------------------------------------------------------------------------------- 1 | { 2 | "field1": "value1", 3 | "field2": "value2", 4 | "field3": "value3", 5 | "field4": "12381923819283192831923" 6 | } 7 | -------------------------------------------------------------------------------- /tests/content/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Test HTML 4 | 5 | 6 | Test HTML 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/content/written/.required: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kartikk221/hyper-express/d859aacb8802cb6d952b126818750ed036d1a2b3/tests/content/written/.required -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | const HyperExpress = require('../index.js'); 2 | const { log, assert_log } = require('./scripts/operators.js'); 3 | const { test_hostmanager_object } = require('./components/features/HostManager.js'); 4 | const { test_router_object } = require('./components/router/Router.js'); 5 | const { test_request_object } = require('./components/http/Request.js'); 6 | const { test_response_object } = require('./components/http/Response.js'); 7 | const { test_websocket_route } = require('./components/ws/WebsocketRoute.js'); 8 | const { test_session_middleware } = require('./middlewares/hyper-express-session/index.js'); 9 | const { test_websocket_component } = require('./components/ws/Websocket.js'); 10 | // const { test_body_parser_middleware } = require('./middlewares/hyper-express-body-parser/index.js'); 11 | 12 | const { server } = require('./configuration.js'); 13 | const { TEST_SERVER, not_found_handler, test_server_shutdown } = require('./components/Server.js'); 14 | (async () => { 15 | try { 16 | // While this is effectively doing the same thing as the not_found_handler, we do not want HyperExpress to also bind its own not found handler which would throw a duplicate route error 17 | TEST_SERVER.all('*', not_found_handler); 18 | 19 | // Initiate Test API Webserver 20 | const group = 'Server'; 21 | const start_time = Date.now(); 22 | await TEST_SERVER.listen(server.port, server.host); 23 | log('TESTING', `Successfully Started HyperExpress HTTP Server @ ${server.host}:${server.port}`); 24 | 25 | // Assert that the server port matches the configuration port 26 | assert_log(group, 'Server Listening Port Test', () => +server.port === TEST_SERVER.port); 27 | 28 | // Assert that a server instance with a bad SSL configuration throws an error 29 | await assert_log(group, 'Good SSL Configuration Initialization Test', async () => { 30 | let result = false; 31 | try { 32 | const TEST_GOOD_SERVER = new HyperExpress.Server({ 33 | key_file_name: './tests/ssl/dummy-key.pem', 34 | cert_file_name: './tests/ssl/dummy-cert.pem', 35 | }); 36 | 37 | // Also tests the callback functionality of the listen method 38 | await new Promise((resolve) => { 39 | TEST_GOOD_SERVER.listen(server.secure_port, server.host, resolve); 40 | }); 41 | TEST_GOOD_SERVER.close(); 42 | result = true; 43 | } catch (error) { 44 | console.error(error); 45 | } 46 | return result; 47 | }); 48 | 49 | // Assert that a server instance with a bad SSL configuration throws an error 50 | assert_log(group, 'Bad SSL Configuration Error Test', () => { 51 | let result = true; 52 | try { 53 | const TEST_BAD_SERVER = new HyperExpress.Server({ 54 | key_file_name: './error.key', 55 | cert_file_name: './error.cert', 56 | }); 57 | result = false; 58 | } catch (error) { 59 | return true; 60 | } 61 | return result; 62 | }); 63 | 64 | // Test Server.HostManager Object 65 | test_hostmanager_object(); 66 | 67 | // Test Router Object 68 | await test_router_object(); 69 | 70 | // Test Request Object 71 | await test_request_object(); 72 | 73 | // Test Response Object 74 | await test_response_object(); 75 | 76 | // Test WebsocketRoute Object 77 | await test_websocket_route(); 78 | 79 | // Test Websocket Polyfill Object 80 | await test_websocket_component(); 81 | 82 | // Test SessionEngine Middleware 83 | await test_session_middleware(); 84 | 85 | // Test the server shutdown process 86 | await test_server_shutdown(); 87 | 88 | log('TESTING', `Successfully Tested All Specified Tests For HyperExpress In ${Date.now() - start_time}ms!`); 89 | process.exit(); 90 | } catch (error) { 91 | console.log(error); 92 | } 93 | })(); 94 | -------------------------------------------------------------------------------- /tests/local.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const crypto = require('crypto'); 3 | const { log } = require('./scripts/operators.js'); 4 | const { server, fetch } = require('./configuration.js'); 5 | const { TEST_SERVER } = require('./components/Server.js'); 6 | 7 | (async () => { 8 | try { 9 | // Define information about the test file 10 | const test_file_path = './content/large-files/song.mp3'; 11 | const test_file_checksum = crypto.createHash('md5').update(fs.readFileSync(test_file_path)).digest('hex'); 12 | const test_file_stats = fs.statSync(test_file_path); 13 | 14 | // Create a simple GET route to stream a large file with chunked encoding 15 | TEST_SERVER.get('/stream', async (request, response) => { 16 | // Write appropriate headers 17 | response.header('md5-checksum', test_file_checksum).type('mp3'); 18 | 19 | // Stream the file to the client 20 | const readable = fs.createReadStream(test_file_path); 21 | 22 | // Stream the file to the client with a random streaming method 23 | const random = Math.floor(Math.random() * 3); 24 | switch (random) { 25 | case 0: 26 | // Use Chunked Transfer 27 | readable.once('close', () => response.send()); 28 | readable.pipe(response); 29 | break; 30 | case 1: 31 | // Use Chunked-Transfer With Built In Streaming 32 | response.stream(readable); 33 | break; 34 | case 2: 35 | // Use Streaming With Content-Length 36 | response.stream(readable, test_file_stats.size); 37 | break; 38 | } 39 | }); 40 | 41 | // Initiate Test API Webserver 42 | await TEST_SERVER.listen(server.port, server.host); 43 | log( 44 | 'TESTING', 45 | `Successfully Started HyperExpress HTTP Server For Local Testing @ ${server.host}:${server.port}` 46 | ); 47 | 48 | // Perform a stress test of the endpoint 49 | let completed = 0; 50 | let start_ts = Date.now(); 51 | const test_endpoint = async () => { 52 | // Make a request to the endpoint 53 | const response = await fetch(`${server.base}/stream`); 54 | 55 | // Retrieve both the expected and received checksums 56 | const expected_checksum = response.headers.get('md5-checksum'); 57 | const received_checksum = crypto 58 | .createHash('md5') 59 | .update(await response.buffer()) 60 | .digest('hex'); 61 | 62 | // Assert that the checksums match 63 | if (expected_checksum !== received_checksum) 64 | throw new Error( 65 | `Checksums Do Not Match! Expected: ${expected_checksum} Received: ${received_checksum}` 66 | ); 67 | completed++; 68 | }; 69 | 70 | setInterval(test_endpoint, 0); 71 | setInterval( 72 | () => console.log(`Requests/Second: ${(completed / ((Date.now() - start_ts) / 1000)).toFixed(2)}`), 73 | 1000 74 | ); 75 | } catch (error) { 76 | console.log(error); 77 | } 78 | })(); 79 | -------------------------------------------------------------------------------- /tests/middlewares/hyper-express-body-parser/configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "path": "/middlewares/hyper-express-body-parser" 3 | } 4 | -------------------------------------------------------------------------------- /tests/middlewares/hyper-express-body-parser/index.js: -------------------------------------------------------------------------------- 1 | const { test_parser_limit } = require('./scenarios/parser_limit.js'); 2 | const { test_parser_validation } = require('./scenarios/parser_validation.js'); 3 | const { test_parser_compression } = require('./scenarios/parser_compression.js'); 4 | const { test_parser_types } = require('./scenarios/parser_types.js'); 5 | 6 | async function test_body_parser_middleware() { 7 | // Test the BodyParser.options.limit property for limiting the size of the body 8 | await test_parser_limit(); 9 | 10 | // Test the BodyParser.options.type and BodyParser.options.verify options functionaltiy 11 | await test_parser_validation(); 12 | 13 | // Test the BodyParser compression functionality 14 | await test_parser_compression(); 15 | 16 | // Test the BodyParser body types functionality 17 | await test_parser_types(); 18 | } 19 | 20 | module.exports = { 21 | test_body_parser_middleware, 22 | }; 23 | -------------------------------------------------------------------------------- /tests/middlewares/hyper-express-body-parser/scenarios/parser_limit.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const BodyParser = require('../../../../middlewares/hyper-express-body-parser/index.js'); 3 | const { log, assert_log } = require('../../../scripts/operators.js'); 4 | const { fetch, server } = require('../../../configuration.js'); 5 | const { TEST_SERVER } = require('../../../components/Server.js'); 6 | const { path } = require('../configuration.json'); 7 | const endpoint = `${path}/scenarios/parser-limit`; 8 | const endpoint_url = server.base + endpoint; 9 | 10 | // Bind a raw parser to the endpoint 11 | const TEST_LIMIT_BYTES = Math.floor(Math.random() * 100) + 100; 12 | TEST_SERVER.use( 13 | endpoint, 14 | BodyParser.raw({ 15 | limit: TEST_LIMIT_BYTES, 16 | }) 17 | ); 18 | 19 | // Create Backend HTTP Route 20 | TEST_SERVER.post(endpoint, (request, response) => { 21 | return response.send(); 22 | }); 23 | 24 | async function test_parser_limit() { 25 | // User Specified ID Brute Vulnerability Test 26 | let group = 'MIDDLEWARE'; 27 | let candidate = 'Middleware.BodyParser'; 28 | log(group, 'Testing ' + candidate + ' - Parser Body Size Limit Test'); 29 | 30 | // Perform fetch requests with various body sizes 31 | const promises = [ 32 | Math.floor(Math.random() * TEST_LIMIT_BYTES), // Smaller than max size 33 | TEST_LIMIT_BYTES, // Max size 34 | Math.floor(Math.random() * TEST_LIMIT_BYTES) + TEST_LIMIT_BYTES, // Larger than max size 35 | TEST_LIMIT_BYTES * Math.floor(Math.random() * 5), // Random Factor Larger than max size 36 | Math.floor(TEST_LIMIT_BYTES * 0.1), // Smaller than max size 37 | ].map( 38 | (size_bytes) => 39 | new Promise(async (resolve) => { 40 | // Generate a random buffer of bytes size 41 | const buffer = crypto.randomBytes(size_bytes); 42 | 43 | // Make the fetch request 44 | const response = await fetch(endpoint_url, { 45 | method: 'POST', 46 | body: buffer, 47 | headers: { 48 | 'content-type': 'application/octet-stream', 49 | }, 50 | }); 51 | 52 | // Assert that the response status code is 413 for the large body 53 | assert_log( 54 | group, 55 | candidate + 56 | ` - Body Size Limit Test With ${size_bytes} / ${TEST_LIMIT_BYTES} Bytes Limit -> HTTP ${response.status}`, 57 | () => response.status == (size_bytes > TEST_LIMIT_BYTES ? 413 : 200) 58 | ); 59 | 60 | resolve(); 61 | }) 62 | ); 63 | 64 | // Wait for all the promises to resolve 65 | await Promise.all(promises); 66 | log(group, 'Finished ' + candidate + ' - Parser Body Size Limit Test\n'); 67 | } 68 | 69 | module.exports = { 70 | test_parser_limit, 71 | }; 72 | -------------------------------------------------------------------------------- /tests/middlewares/hyper-express-body-parser/scenarios/parser_types.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const BodyParser = require('../../../../middlewares/hyper-express-body-parser/index.js'); 3 | const { log, assert_log } = require('../../../scripts/operators.js'); 4 | const { fetch, server } = require('../../../configuration.js'); 5 | const { TEST_SERVER } = require('../../../components/Server.js'); 6 | const { path } = require('../configuration.json'); 7 | const endpoint = `${path}/scenarios/parser-types`; 8 | const endpoint_url = server.base + endpoint; 9 | 10 | // Bind all parser types to the endpoint 11 | TEST_SERVER.use(endpoint, BodyParser.raw(), BodyParser.text(), BodyParser.json()); 12 | 13 | // Create Backend HTTP Route 14 | TEST_SERVER.post(endpoint, (request, response) => { 15 | const content_type = request.headers['content-type']; 16 | switch (content_type) { 17 | case 'application/json': 18 | return response.json(request.body); 19 | default: 20 | return response.send(request.body); 21 | } 22 | }); 23 | 24 | async function test_parser_types() { 25 | // User Specified ID Brute Vulnerability Test 26 | let group = 'MIDDLEWARE'; 27 | let candidate = 'Middleware.BodyParser'; 28 | log(group, 'Testing ' + candidate + ' - Parser Body Types Test'); 29 | 30 | // Test the empty bodies 31 | 32 | // Perform fetch requests with various body types 33 | const promises = [ 34 | [crypto.randomBytes(1000), 'application/octet-stream'], 35 | [crypto.randomBytes(1000).toString('hex'), 'text/plain'], 36 | [ 37 | JSON.stringify({ 38 | name: 'json test', 39 | payload: crypto.randomBytes(1000).toString('hex'), 40 | }), 41 | 'application/json', 42 | ], 43 | ].map( 44 | ([request_body, content_type]) => 45 | new Promise(async (resolve) => { 46 | // Make the fetch request 47 | const response = await fetch(endpoint_url, { 48 | method: 'POST', 49 | headers: { 50 | 'content-type': content_type, 51 | }, 52 | body: request_body, 53 | }); 54 | 55 | // Parse the incoming body as the appropriate type 56 | let response_body; 57 | switch (content_type) { 58 | case 'application/octet-stream': 59 | response_body = await response.buffer(); 60 | break; 61 | case 'text/plain': 62 | response_body = await response.text(); 63 | break; 64 | case 'application/json': 65 | response_body = await response.text(); 66 | break; 67 | } 68 | 69 | // Assert that the response status code is 413 for the large body 70 | assert_log(group, candidate + ` - Body Type Test With '${content_type}'`, () => { 71 | switch (content_type) { 72 | case 'application/octet-stream': 73 | return Buffer.compare(request_body, response_body) === 0; 74 | case 'text/plain': 75 | return request_body === response_body; 76 | case 'application/json': 77 | return JSON.stringify(request_body) === JSON.stringify(response_body); 78 | default: 79 | return false; 80 | } 81 | }); 82 | 83 | resolve(); 84 | }) 85 | ); 86 | 87 | // Wait for all the promises to resolve 88 | await Promise.all(promises); 89 | log(group, 'Finished ' + candidate + ' - Parser Body Types Test\n'); 90 | } 91 | 92 | module.exports = { 93 | test_parser_types, 94 | }; 95 | -------------------------------------------------------------------------------- /tests/middlewares/hyper-express-body-parser/scenarios/parser_validation.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const BodyParser = require('../../../../middlewares/hyper-express-body-parser/index.js'); 3 | const { log, assert_log } = require('../../../scripts/operators.js'); 4 | const { fetch, server } = require('../../../configuration.js'); 5 | const { TEST_SERVER } = require('../../../components/Server.js'); 6 | const { path } = require('../configuration.json'); 7 | const endpoint = `${path}/scenarios/parser-validation`; 8 | const endpoint_url = server.base + endpoint; 9 | 10 | const TEST_PAYLOAD_SIZE = Math.floor(Math.random() * 250) + 250; 11 | 12 | // Bind a raw parser that will only parse if the content type matches 13 | TEST_SERVER.use( 14 | endpoint, 15 | BodyParser.raw({ 16 | type: 'application/octet-stream', 17 | verify: (req, res, buffer) => { 18 | return buffer.length > TEST_PAYLOAD_SIZE * 0.5; 19 | }, // Reject bodies that are less than half size of the payload 20 | }) 21 | ); 22 | 23 | // Create Backend HTTP Route 24 | TEST_SERVER.post(endpoint, (request, response) => { 25 | // Send a 200 if we have some body content else send a 204 26 | response.status(request.body.length > 0 ? 200 : 204).send(); 27 | }); 28 | 29 | async function test_parser_validation() { 30 | // User Specified ID Brute Vulnerability Test 31 | let group = 'MIDDLEWARE'; 32 | let candidate = 'Middleware.BodyParser'; 33 | log(group, 'Testing ' + candidate + ' - Parser Body Validation Test'); 34 | 35 | // Perform fetch requests with various body sizes 36 | const promises = [ 37 | ['application/json', TEST_PAYLOAD_SIZE, 204], // ~100% of the payload size but incorrect content type 38 | ['application/octet-stream', Math.floor(TEST_PAYLOAD_SIZE * 0.25), 403], // ~25% of the payload size 39 | ['application/octet-stream', Math.floor(TEST_PAYLOAD_SIZE * 0.75), 200], // ~75% of the payload size 40 | ['application/octet-stream', TEST_PAYLOAD_SIZE, 200], // ~75% of the payload size 41 | ].map( 42 | ([content_type, size_bytes, status_code]) => 43 | new Promise(async (resolve) => { 44 | // Generate a random buffer of bytes size 45 | const buffer = crypto.randomBytes(size_bytes); 46 | 47 | // Make the fetch request 48 | const response = await fetch(endpoint_url, { 49 | method: 'POST', 50 | body: buffer, 51 | headers: { 52 | 'content-type': content_type, 53 | }, 54 | }); 55 | 56 | // Assert that the response status code is 413 for the large body 57 | assert_log( 58 | group, 59 | candidate + 60 | ` - Content Type & Verify Function Test With "${content_type}" @ ${size_bytes} Bytes Payload`, 61 | () => response.status === status_code 62 | ); 63 | 64 | resolve(); 65 | }) 66 | ); 67 | 68 | // Wait for all the promises to resolve 69 | await Promise.all(promises); 70 | log(group, 'Finished ' + candidate + ' - Parser Body Validation Test\n'); 71 | } 72 | 73 | module.exports = { 74 | test_parser_validation, 75 | }; 76 | -------------------------------------------------------------------------------- /tests/middlewares/hyper-express-session/configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "path": "/middlewares/hyper-express-session", 3 | "log_store_events": false 4 | } 5 | -------------------------------------------------------------------------------- /tests/middlewares/hyper-express-session/index.js: -------------------------------------------------------------------------------- 1 | // Bind test session engine to configuration path on test server 2 | const { TEST_SERVER } = require('../../components/Server.js'); 3 | const { TEST_ENGINE } = require('./test_engine.js'); 4 | const { path } = require('./configuration.json'); 5 | TEST_SERVER.use(path, TEST_ENGINE); 6 | 7 | const { test_properties_scenario } = require('./scenarios/properties.js'); 8 | const { test_brute_scenario } = require('./scenarios/brute.js'); 9 | const { test_duration_scenario } = require('./scenarios/duration.js'); 10 | const { test_roll_scenario } = require('./scenarios/roll.js'); 11 | const { test_visits_scenario } = require('./scenarios/visits.js'); 12 | 13 | async function test_session_middleware() { 14 | await test_properties_scenario(); 15 | await test_brute_scenario(); 16 | await test_roll_scenario(); 17 | await test_visits_scenario(); 18 | await test_duration_scenario(); 19 | } 20 | 21 | module.exports = { 22 | test_session_middleware, 23 | }; 24 | -------------------------------------------------------------------------------- /tests/middlewares/hyper-express-session/scenarios/brute.js: -------------------------------------------------------------------------------- 1 | const { log, assert_log, random_string, async_for_each } = require('../../../scripts/operators.js'); 2 | const { fetch, server } = require('../../../configuration.js'); 3 | const { TEST_SERVER } = require('../../../components/Server.js'); 4 | const { TEST_STORE } = require('../test_engine.js'); 5 | const { path } = require('../configuration.json'); 6 | const endpoint = `${path}/scenarios/brute`; 7 | const endpoint_url = server.base + endpoint; 8 | 9 | // Create Backend HTTP Route 10 | TEST_SERVER.post(endpoint, async (request, response) => { 11 | await request.session.start(); 12 | return response.json({ 13 | session_id: request.session.id, 14 | store: TEST_STORE.data, 15 | }); 16 | }); 17 | 18 | async function test_brute_scenario() { 19 | // User Specified ID Brute Vulnerability Test 20 | let group = 'MIDDLEWARE'; 21 | let candidate = 'Middleware.SessionEngine.Session'; 22 | let last_session_id = ''; 23 | log(group, `Testing ${candidate} - Self-Specified/Brute Session ID Test`); 24 | TEST_STORE.empty(); 25 | await async_for_each([0, 1, 2, 3, 4], async (value, next) => { 26 | let response = await fetch(endpoint_url, { 27 | method: 'POST', 28 | headers: { 29 | cookie: 'test_sess=' + random_string(30), // Random Session IDs 30 | }, 31 | }); 32 | 33 | let body = await response.json(); 34 | assert_log( 35 | group, 36 | candidate + ' Brute-Force ID Vulnerability Prevention @ ' + value, 37 | () => Object.keys(body.store).length === 0 && last_session_id !== body.session_id 38 | ); 39 | 40 | last_session_id = body.session_id; 41 | next(); 42 | }); 43 | 44 | log(group, `Finished Testing ${candidate} - Self-Specified/Brute Session ID Test\n`); 45 | } 46 | 47 | module.exports = { 48 | test_brute_scenario, 49 | }; 50 | -------------------------------------------------------------------------------- /tests/middlewares/hyper-express-session/scenarios/duration.js: -------------------------------------------------------------------------------- 1 | const { log, assert_log, async_for_each } = require('../../../scripts/operators.js'); 2 | const { fetch, server } = require('../../../configuration.js'); 3 | const { TEST_SERVER } = require('../../../components/Server.js'); 4 | const { TEST_STORE, TEST_ENGINE } = require('../test_engine.js'); 5 | const { path } = require('../configuration.json'); 6 | const endpoint = `${path}/scenarios/duration`; 7 | const endpoint_url = server.base + endpoint; 8 | 9 | // Create Backend HTTP Route 10 | TEST_SERVER.post(endpoint, async (request, response) => { 11 | await TEST_ENGINE.cleanup(); // Purposely trigger cleanup before every request to simulate ideal session cleanup 12 | await request.session.start(); 13 | let body = await request.text(); 14 | let duration = parseInt(body); 15 | 16 | if (duration > 0) request.session.set_duration(duration); 17 | 18 | return response.json({ 19 | session_id: request.session.id, 20 | store: TEST_STORE.data, 21 | }); 22 | }); 23 | 24 | async function test_duration_scenario() { 25 | let group = 'MIDDLEWARE'; 26 | let candidate = 'Middleware.SessionEngine.Session'; 27 | let cookies = []; 28 | 29 | log(group, 'Testing ' + candidate + ' - Custom Duration/Cleanup Test'); 30 | TEST_STORE.empty(); 31 | await async_for_each([1, 2, 3], async (value, next) => { 32 | let response = await fetch(endpoint_url, { 33 | method: 'POST', 34 | headers: { 35 | cookie: cookies.join('; '), 36 | }, 37 | body: value < 3 ? '250' : '', // Set Custom Duration On First 2 Requests 38 | }); 39 | let headers = response.headers.raw(); 40 | let body = await response.json(); 41 | 42 | // Send session cookie with future requests 43 | if (Array.isArray(headers['set-cookie'])) { 44 | cookies = []; 45 | headers['set-cookie'].forEach((chunk) => { 46 | chunk = chunk.split('; ')[0].split('='); 47 | let name = chunk[0]; 48 | let value = chunk[1]; 49 | let header = `${name}=${value}`; 50 | cookies.push(header); 51 | }); 52 | } 53 | 54 | assert_log(group, candidate + ' Custom Duration/Cleanup @ Iteration ' + value, () => { 55 | if (value == 1 || value == 3) return Object.keys(body.store).length == 0; 56 | 57 | let store_test = Object.keys(body.store).length == 1; 58 | let sess_obj_test = body.store?.[body.session_id]?.data !== undefined; 59 | 60 | return store_test && sess_obj_test; 61 | }); 62 | 63 | // Wait 1.5 Seconds for session to expire with custom duration before 3rd request 64 | let delay = value == 2 ? 300 : 0; 65 | if (delay > 0) log(group, `Waiting ${delay}ms to simulate custom duration expiry...`); 66 | 67 | setTimeout((n) => n(), delay, next); 68 | }); 69 | 70 | log(group, 'Finished Testing ' + candidate + ' - Custom Duration/Cleanup Test\n'); 71 | } 72 | 73 | module.exports = { 74 | test_duration_scenario, 75 | }; 76 | -------------------------------------------------------------------------------- /tests/middlewares/hyper-express-session/scenarios/properties.js: -------------------------------------------------------------------------------- 1 | const { log, assert_log, random_string, async_for_each } = require('../../../scripts/operators.js'); 2 | const { fetch, server } = require('../../../configuration.js'); 3 | const { TEST_SERVER } = require('../../../components/Server.js'); 4 | const { TEST_STORE } = require('../test_engine.js'); 5 | const { path } = require('../configuration.json'); 6 | const endpoint = `${path}/scenarios/properties`; 7 | const endpoint_url = server.base + endpoint; 8 | 9 | // Create Backend HTTP Route 10 | TEST_SERVER.get(endpoint, async (request, response) => { 11 | // Start the session 12 | await request.session.start(); 13 | 14 | // Set some value into the session object 15 | // The await is unneccessary but it is used to simulate a long running operation 16 | await request.session.set({ 17 | myid: 'some_id', 18 | visits: 0, 19 | }); 20 | 21 | return response.json({ 22 | id: request.session.id, 23 | signed_id: request.session.signed_id, 24 | ready: request.session.ready, 25 | stored: request.session.stored, 26 | }); 27 | }); 28 | 29 | async function test_properties_scenario() { 30 | // Test session persistence with visits test - VISITS ITERATOR TEST 31 | let group = 'MIDDLEWARE'; 32 | let candidate = 'Middleware.SessionEngine.Session'; 33 | 34 | // Make first fetch request 35 | const response1 = await fetch(endpoint_url); 36 | const data1 = await response1.json(); 37 | 38 | // Make second fetch request 39 | const response2 = await fetch(endpoint_url, { 40 | headers: { 41 | cookie: response1.headers.get('set-cookie').split('; ')[0], 42 | }, 43 | }); 44 | const data2 = await response2.json(); 45 | 46 | // Assert that the Session.id is a string and exactly same in both requests 47 | assert_log( 48 | group, 49 | `${candidate}.id`, 50 | () => typeof data1.id == 'string' && data1.id.length > 0 && data1.id == data2.id 51 | ); 52 | 53 | // Assert that the Session.signed_id is a string and exactly same in both requests 54 | assert_log( 55 | group, 56 | `${candidate}.signed_id`, 57 | () => typeof data1.signed_id == 'string' && data1.signed_id.length > 0 && data1.signed_id == data2.signed_id 58 | ); 59 | 60 | // Assert that the session was Session.ready in both requests 61 | assert_log(group, `${candidate}.ready`, () => data1.ready && data2.ready); 62 | 63 | // Assert that the session was Session.stored only in second request 64 | assert_log(group, `${candidate}.stored`, () => !data1.stored && data2.stored); 65 | 66 | log(group, 'Finished Testing ' + candidate + ' - Properties Test\n'); 67 | } 68 | 69 | module.exports = { 70 | test_properties_scenario, 71 | }; 72 | -------------------------------------------------------------------------------- /tests/middlewares/hyper-express-session/scenarios/roll.js: -------------------------------------------------------------------------------- 1 | const { log, assert_log, random_string, async_for_each } = require('../../../scripts/operators.js'); 2 | const { fetch, server } = require('../../../configuration.js'); 3 | const { TEST_SERVER } = require('../../../components/Server.js'); 4 | const { TEST_STORE } = require('../test_engine.js'); 5 | const { path } = require('../configuration.json'); 6 | const endpoint = `${path}/scenarios/roll`; 7 | const endpoint_url = server.base + endpoint; 8 | 9 | // Create Backend HTTP Route 10 | TEST_SERVER.post(endpoint, async (request, response) => { 11 | await request.session.start(); 12 | if (request.session.get('some_data') == undefined) { 13 | request.session.set('some_data', random_string(10)); 14 | } else { 15 | // Performs a delete and migrate to a new roll id 16 | await request.session.roll(); 17 | } 18 | 19 | return response.json({ 20 | session_id: request.session.id, 21 | session_data: request.session.get(), 22 | store: TEST_STORE.data, 23 | }); 24 | }); 25 | 26 | async function test_roll_scenario() { 27 | let group = 'MIDDLEWARE'; 28 | let candidate = 'Middleware.SessionEngine.Session'; 29 | let cookies = []; 30 | let last_rolled_id = ''; 31 | log(group, 'Testing ' + candidate + ' - Roll Test'); 32 | 33 | TEST_STORE.empty(); 34 | await async_for_each([0, 0, 1, 0], async (value, next) => { 35 | let response = await fetch(endpoint_url, { 36 | method: 'POST', 37 | headers: { 38 | cookie: cookies.join('; '), 39 | }, 40 | }); 41 | let headers = response.headers.raw(); 42 | let body = await response.json(); 43 | 44 | // Send session cookie with future requests 45 | let current_session_id; 46 | if (Array.isArray(headers['set-cookie'])) { 47 | cookies = []; // Reset cookies for new session id 48 | headers['set-cookie'].forEach((chunk) => { 49 | chunk = chunk.split('; ')[0].split('='); 50 | let name = chunk[0]; 51 | let value = chunk[1]; 52 | let header = `${name}=${value}`; 53 | if (name === 'test_sess') current_session_id = value; 54 | cookies.push(header); 55 | }); 56 | } 57 | 58 | assert_log(group, candidate + ' Session Roll @ Iterative Scenario ' + value, () => { 59 | // Store will always be empty due to lazy persistance and .roll() destroying session during request 60 | let store_test = Object.keys(body.store).length === Math.floor(value); 61 | let id_test = value < 1 ? current_session_id !== last_rolled_id : true; 62 | last_rolled_id = current_session_id; 63 | return store_test && id_test; 64 | }); 65 | 66 | next(); 67 | }); 68 | 69 | log(group, 'Finished Testing ' + candidate + ' - Roll Test\n'); 70 | } 71 | 72 | module.exports = { 73 | test_roll_scenario, 74 | }; 75 | -------------------------------------------------------------------------------- /tests/middlewares/hyper-express-session/scenarios/visits.js: -------------------------------------------------------------------------------- 1 | const { log, assert_log, async_for_each } = require('../../../scripts/operators.js'); 2 | const { fetch, server } = require('../../../configuration.js'); 3 | const { TEST_SERVER } = require('../../../components/Server.js'); 4 | const { TEST_STORE } = require('../test_engine.js'); 5 | const { path } = require('../configuration.json'); 6 | const endpoint = `${path}/scenarios/visits`; 7 | const endpoint_url = server.base + endpoint; 8 | 9 | // Create Backend HTTP Route 10 | TEST_SERVER.post(endpoint, async (request, response) => { 11 | await request.session.start(); 12 | let visits = request.session.get('visits'); 13 | 14 | if (visits == undefined) { 15 | visits = 1; 16 | } else if (visits < 5) { 17 | visits++; 18 | } else { 19 | visits = undefined; 20 | } 21 | 22 | if (visits) { 23 | request.session.set('visits', visits); 24 | } else { 25 | await request.session.destroy(); 26 | } 27 | 28 | return response.json({ 29 | session_id: request.session.id, 30 | session: request.session.get(), 31 | store: TEST_STORE.data, 32 | }); 33 | }); 34 | 35 | async function test_visits_scenario() { 36 | // Test session persistence with visits test - VISITS ITERATOR TEST 37 | let group = 'MIDDLEWARE'; 38 | let candidate = 'Middleware.SessionEngine.Session'; 39 | let cookies = []; 40 | let session_expiry = 0; 41 | 42 | TEST_STORE.empty(); 43 | log(group, 'Testing ' + candidate + ' - Visits Test'); 44 | await async_for_each([1, 2, 3, 4, 5, 0, 1, 2, 3, 4], async (value, next) => { 45 | let response = await fetch(endpoint_url, { 46 | method: 'POST', 47 | headers: { 48 | cookie: cookies.join('; '), 49 | }, 50 | }); 51 | let headers = response.headers.raw(); 52 | let body = await response.json(); 53 | 54 | // Send session cookie with future requests 55 | if (Array.isArray(headers['set-cookie'])) { 56 | cookies = []; 57 | headers['set-cookie'].forEach((chunk) => { 58 | let chunks = chunk.split('; ')[0].split('='); 59 | let name = chunks[0]; 60 | let value = chunks[1]; 61 | let header = `${name}=${value}`; 62 | 63 | // Ensure the cookie is not a "delete" operation with a max-age of 0 64 | if (chunk.toLowerCase().includes('max-age=0')) return; 65 | 66 | // Push the cookie to the list of cookies to send with future requests 67 | cookies.push(header); 68 | }); 69 | } 70 | 71 | // Perform Visits Check 72 | if (value == 0) { 73 | assert_log(group, `${candidate} VISITS_TEST @ ${value}`, () => { 74 | let visits_test = Object.keys(body.session).length == 0; 75 | let store_test = body.store[body.session_id] == undefined && Object.keys(body.store).length == 0; 76 | return visits_test && store_test; 77 | }); 78 | } else if (value == 1) { 79 | assert_log(group, `${candidate} VISITS_TEST @ ${value}`, () => { 80 | let visits_test = body.session.visits === value; 81 | let store_test = body.store[body.session_id] == undefined; 82 | return visits_test && store_test; 83 | }); 84 | } else { 85 | assert_log(group, `${candidate} OBJ_TOUCH_TEST & OBJ_VISITS_TEST @ ${value}`, () => { 86 | let session_object = body.store?.[body.session_id]; 87 | let visits_test = body.session.visits === value; 88 | let store_test = session_object?.data?.visits === value; 89 | 90 | let touch_test = value < 3; 91 | if (!touch_test && session_object.expiry >= session_expiry) touch_test = true; 92 | 93 | session_expiry = session_object.expiry; 94 | return visits_test && store_test && touch_test; 95 | }); 96 | } 97 | 98 | next(); 99 | }); 100 | 101 | log(group, 'Finished Testing ' + candidate + ' - Visits Test\n'); 102 | } 103 | 104 | module.exports = { 105 | test_visits_scenario, 106 | }; 107 | -------------------------------------------------------------------------------- /tests/middlewares/hyper-express-session/test_engine.js: -------------------------------------------------------------------------------- 1 | const SessionEngine = require('../../../middlewares/hyper-express-session/index.js'); 2 | const MemoryStore = require('../../scripts/MemoryStore.js'); 3 | const { random_string } = require('../../scripts/operators.js'); 4 | 5 | // Create Test Engine For Usage In Tests 6 | const TEST_ENGINE = new SessionEngine({ 7 | duration: 1000 * 60 * 45, 8 | cookie: { 9 | name: 'test_sess', 10 | httpOnly: false, 11 | secure: false, 12 | sameSite: 'none', 13 | secret: random_string(20), 14 | }, 15 | }); 16 | 17 | const { log } = require('../../scripts/operators.js'); 18 | const { log_store_events } = require('./configuration.json'); 19 | function store_log(message) { 20 | if (log_store_events === true) log('SESSION_STORE', message); 21 | } 22 | 23 | // Use a simulated SQL-like memory store 24 | const TEST_STORE = new MemoryStore(); 25 | 26 | // Handle READ events 27 | TEST_ENGINE.use('read', (session) => { 28 | store_log('READ -> ' + session.id); 29 | return TEST_STORE.select(session.id); 30 | }); 31 | 32 | // Handle WRITE events 33 | TEST_ENGINE.use('write', (session) => { 34 | if (session.stored) { 35 | store_log('UPDATE -> ' + session.id + ' -> ' + session.expires_at); 36 | TEST_STORE.update(session.id, session.get(), session.expires_at); 37 | } else { 38 | store_log('INSERT -> ' + session.id + ' -> ' + session.expires_at); 39 | TEST_STORE.insert(session.id, session.get(), session.expires_at); 40 | } 41 | }); 42 | 43 | // Handle TOUCH events 44 | TEST_ENGINE.use('touch', (session) => { 45 | store_log('TOUCH -> ' + session.id + ' -> ' + session.expires_at); 46 | TEST_STORE.touch(session.id, session.expires_at); 47 | }); 48 | 49 | // Handle DESTROY events 50 | TEST_ENGINE.use('destroy', (session) => { 51 | store_log('DESTROY -> ' + session.id); 52 | TEST_STORE.delete(session.id); 53 | }); 54 | 55 | // Handle CLEANUP events 56 | TEST_ENGINE.use('cleanup', () => { 57 | store_log('CLEANUP -> ALL SESSIONS'); 58 | TEST_STORE.cleanup(); 59 | }); 60 | 61 | module.exports = { 62 | TEST_ENGINE, 63 | TEST_STORE, 64 | }; 65 | -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyper-express-tests", 3 | "version": "1.0.0", 4 | "description": "Unit/API Tests For HyperExpress", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "abort-controller": "^3.0.0", 13 | "eventsource": "^2.0.0", 14 | "form-data": "^4.0.0", 15 | "i": "^0.3.7", 16 | "node-fetch": "^2.6.6", 17 | "npm": "^8.5.5", 18 | "ws": "^8.2.3", 19 | "zlib": "^1.0.5" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/performance.js: -------------------------------------------------------------------------------- 1 | class PerformanceMeasurement { 2 | #data = []; 3 | 4 | /** 5 | * Name for this performance measurement. 6 | * @param {string} name 7 | */ 8 | constructor(name) { 9 | // Register graceful shutdown handlers 10 | let context = this; 11 | let in_shutdown = false; 12 | [`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach((type) => 13 | process.on(type, () => { 14 | // Mark the server as shutting down 15 | if (in_shutdown) return; 16 | in_shutdown = true; 17 | 18 | // Log the performance measurements 19 | console.log(name, JSON.stringify(context.measurements)); 20 | 21 | // Set a timeout to exit the process after 1 second 22 | setTimeout(() => process.exit(0), 1000); 23 | }) 24 | ); 25 | } 26 | 27 | /** 28 | * Records the amount of time it took to execute a function. 29 | * Use `process.hrtime.bigint()` to get the start time. 30 | * @param {BigInt} start_time 31 | */ 32 | record(start_time) { 33 | const delta = process.hrtime.bigint() - start_time; 34 | if (delta > 0) this.#data.push(delta); 35 | } 36 | 37 | /** 38 | * Returns the measurements of this performance measurement. 39 | */ 40 | get measurements() { 41 | // Initialize the individual statistics 42 | let average = 0; 43 | let sum = BigInt(0); 44 | let min = BigInt(Number.MAX_SAFE_INTEGER); 45 | let max = BigInt(Number.MIN_SAFE_INTEGER); 46 | 47 | // Iterate over all of the measurements 48 | for (const measurement of this.#data) { 49 | // Do not consider measurements that are less than 0ns (invalid) 50 | if (measurement >= 0) { 51 | // Update the sum 52 | sum += BigInt(measurement); 53 | 54 | // Update the min and max 55 | if (measurement < min) min = measurement; 56 | if (measurement > max) max = measurement; 57 | } 58 | } 59 | 60 | // Calculate the average 61 | average = sum / BigInt(this.#data.length); 62 | 63 | // Return the statistics object 64 | return { 65 | min: min.toString(), 66 | max: max.toString(), 67 | sum: sum.toString(), 68 | count: this.#data.length.toString(), 69 | average: average.toString(), 70 | }; 71 | } 72 | } 73 | 74 | module.exports = PerformanceMeasurement; 75 | -------------------------------------------------------------------------------- /tests/scripts/MemoryStore.js: -------------------------------------------------------------------------------- 1 | // Memory store with simulated functionalities similar to SQL databases 2 | class MemoryStore { 3 | #container = {}; 4 | constructor() {} 5 | 6 | /** 7 | * This method can be used to lookup/select specific keys from store 8 | * 9 | * @param {String} key 10 | * @returns {Any} Any OR undefined 11 | */ 12 | select(key) { 13 | return this.#container?.[key]?.data; 14 | } 15 | 16 | /** 17 | * 18 | * @param {String} key 19 | * @param {Object} data 20 | * @param {Number} expiry_ts In Milliseconds 21 | */ 22 | insert(key, data, expiry_ts) { 23 | // Throw on overwrites 24 | if (this.#container[key]) 25 | throw new Error('MemoryStore: key ' + key + ' already exists. Use update() method.'); 26 | 27 | this.#container[key] = { 28 | data: data, 29 | expiry: expiry_ts, 30 | }; 31 | } 32 | 33 | update(key, data, expiry_ts) { 34 | // Throw on non existent source 35 | if (this.#container[key] == undefined) 36 | throw new Error( 37 | 'MemoryStore: key ' + key + ' does not exist in store. Use insert() method.' 38 | ); 39 | 40 | this.#container[key].data = data; 41 | if (typeof expiry_ts == 'number') this.#container[key].expiry = expiry_ts; 42 | } 43 | 44 | touch(key, expiry_ts) { 45 | // Throw on non existent source 46 | if (this.#container[key] == undefined) 47 | throw new Error( 48 | 'MemoryStore: cannot touch key ' + key + ' because it does not exist in store.' 49 | ); 50 | 51 | this.#container[key].expiry = expiry_ts; 52 | } 53 | 54 | delete(key) { 55 | delete this.#container[key]; 56 | } 57 | 58 | empty() { 59 | this.#container = {}; 60 | } 61 | 62 | cleanup() { 63 | let removed = 0; 64 | Object.keys(this.#container).forEach((key) => { 65 | let data = this.#container[key]; 66 | let expiry = data.expiry; 67 | if (expiry < Date.now()) { 68 | delete this.#container[key]; 69 | removed++; 70 | } 71 | }); 72 | return removed; 73 | } 74 | 75 | get data() { 76 | return this.#container; 77 | } 78 | } 79 | 80 | module.exports = MemoryStore; 81 | -------------------------------------------------------------------------------- /tests/scripts/operators.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const HTTP = require('http'); 3 | 4 | function log(logger = 'SYSTEM', message) { 5 | let dt = new Date(); 6 | let timeStamp = dt.toLocaleString([], { hour12: true, timeZone: 'America/New_York' }).replace(', ', ' ').split(' '); 7 | timeStamp[1] += ':' + dt.getMilliseconds().toString().padStart(3, '0') + 'ms'; 8 | timeStamp = timeStamp.join(' '); 9 | console.log(`[${timeStamp}][${logger}] ${message}`); 10 | } 11 | 12 | function random_string(length = 7) { 13 | var result = []; 14 | var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 15 | var charactersLength = characters.length; 16 | for (var i = 0; i < length; i++) { 17 | result.push(characters.charAt(Math.floor(Math.random() * charactersLength))); 18 | } 19 | return result.join(''); 20 | } 21 | 22 | async function assert_log(group, target, assertion) { 23 | try { 24 | let result = await assertion(); 25 | if (result) { 26 | log(group, 'Verified ' + target); 27 | } else { 28 | throw new Error('Failed To Verify ' + target + ' @ ' + group + ' -> ' + assertion.toString()); 29 | } 30 | } catch (error) { 31 | console.log(error); 32 | throw new Error('Failed To Verify ' + target + ' @ ' + group + ' -> ' + assertion.toString()); 33 | } 34 | } 35 | 36 | function async_for_each(items, handler, cursor = 0, final) { 37 | if (final == undefined) return new Promise((resolve, reject) => async_for_each(items, handler, cursor, resolve)); 38 | if (cursor < items.length) return handler(items[cursor], () => async_for_each(items, handler, cursor + 1, final)); 39 | return final(); // Resolve master promise 40 | } 41 | 42 | function http_post_headers({ host, port, path, method = 'GET', body, headers = {}, silence_errors = false }) { 43 | return new Promise((resolve, reject) => { 44 | const request = HTTP.request({ 45 | host, 46 | port, 47 | path, 48 | method, 49 | headers, 50 | }); 51 | 52 | if (body) request.write(body); 53 | 54 | request.on('response', (response) => 55 | resolve({ 56 | url: response.url, 57 | status: response.statusCode, 58 | headers: response.headers, 59 | }) 60 | ); 61 | 62 | if (!silence_errors) request.on('error', reject); 63 | }); 64 | } 65 | 66 | function async_wait(delay) { 67 | return new Promise((resolve, reject) => setTimeout((res) => res(), delay, resolve)); 68 | } 69 | 70 | function md5_from_buffer(buffer) { 71 | return crypto.createHash('md5').update(buffer).digest('hex'); 72 | } 73 | 74 | module.exports = { 75 | log: log, 76 | random_string: random_string, 77 | assert_log: assert_log, 78 | async_for_each: async_for_each, 79 | http_post_headers: http_post_headers, 80 | async_wait: async_wait, 81 | md5_from_buffer: md5_from_buffer, 82 | }; 83 | -------------------------------------------------------------------------------- /tests/ssl/dummy-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIF7zCCA9egAwIBAgIUHQd5vSaP28tenjjSWoPLQ31Kf7IwDQYJKoZIhvcNAQEL 3 | BQAwgYYxCzAJBgNVBAYTAlhYMRIwEAYDVQQIDAlTdGF0ZU5hbWUxETAPBgNVBAcM 4 | CENpdHlOYW1lMRQwEgYDVQQKDAtDb21wYW55TmFtZTEbMBkGA1UECwwSQ29tcGFu 5 | eVNlY3Rpb25OYW1lMR0wGwYDVQQDDBRDb21tb25OYW1lT3JIb3N0bmFtZTAeFw0y 6 | MzA1MTYyMzMyNTVaFw0zMzA1MTMyMzMyNTVaMIGGMQswCQYDVQQGEwJYWDESMBAG 7 | A1UECAwJU3RhdGVOYW1lMREwDwYDVQQHDAhDaXR5TmFtZTEUMBIGA1UECgwLQ29t 8 | cGFueU5hbWUxGzAZBgNVBAsMEkNvbXBhbnlTZWN0aW9uTmFtZTEdMBsGA1UEAwwU 9 | Q29tbW9uTmFtZU9ySG9zdG5hbWUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK 10 | AoICAQC/48UKxs++PJD+MniBEF0yc2crUS2fPg1NA0j92sg0Vlh28maZBDtQMJw6 11 | DZDVb74paw8jLSZHdmTQEKsE0uvAl++gvN6pZulpbTWlNlLcvSG2FEYnOCb/Vxwu 12 | Mfqx3ijANtrH4fx3eYB2TV1gZkXcmncO7a048WuBlpaVFDqvmJiObKeKA6XXnqC4 13 | ih19R4Yqq+bBzIdSYeS89n8GaOrO9Y7AbMtuMT+x1YtfUPsipbz4/76qqX4WC9Ph 14 | qpLAHY71suQTT5l2N+eY3uuvqTRwxxda7Xdq/ABp1/kmStQwVTw/iRQPrCr1DxJy 15 | 8jm1Dyk3IlvBNv8oMCdrf+rfmljPIxA65LiRn/8oE+D32NbhDAo8zGzXS9BhBqtY 16 | 6Qj8HgSTiJhLw2ZzIfLWKRbfUO6dv3XsKx8vbF+kMqPZ1E+bACv3lCP6XJsihugM 17 | Y3uVyyAFIAyRuczbRsLAtrZwbxYPQ/gvKWxNAg63Iwp/kluGwO6om+pp913z9YJW 18 | Qr9pIO/ISayiKzXN7tqJ/BgTgLlD0fWexJOXkccfT9NjSV3DIq9m2McwN8GHspnh 19 | lf8aZO4X48RGIS+vkH1oxgLJaZt0zmX43aZm83Aip4ykQyYg6ixDObrrpSG76UiT 20 | H98s7+CAFFWO5usPIiHE5yQ9NTaBjHwMXm/3uLpEQ+ESKMCkwQIDAQABo1MwUTAd 21 | BgNVHQ4EFgQULzXMAU/+wT/ukDydvZF6qhMv2kwwHwYDVR0jBBgwFoAULzXMAU/+ 22 | wT/ukDydvZF6qhMv2kwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC 23 | AgEAjcDsY7fFij4iiU0KpxecTJFg3nGGz4xFsln6IedUV9K2Gw5ZoAwyGc6BO29j 24 | +bFTAbzbs+OHmExDyX1Y0t++Gj2MN6NP66yLvVaTJCQZHIshg6PTQx4u4EthwVb8 25 | k5NR0moSt2kk7rBWXlcWiZLAASruEFoZGlT1ofNMjGiRd1l1iQEm1+QQN95GTF8L 26 | M8m/s36dHz1bsRa2BdBZnN9Qv0KvS43zigjW/d6+jJLePVJ+enwSn4fLCW+Nyx3i 27 | 7XSVqAUt/lW+h4jza3TF5jkvlRPOAc5+MJOemKoS1uGS+lPg0Z8jv+nPNkEmUcd8 28 | t+KKJAI/0E1nvEMpW3Rr6XVgIqlKSketddeDG+3t3m4ZOU14tiSpp6MTS4DGcPO/ 29 | vhgWIFOi2ABeYhhzBrYs+jgpc1ogT6OHXBrhVNfLBXQ15II+cfQDBkhK526TC/EW 30 | P9oYFX5RD5TTVMWYKfSDfL9nIPH7YtA0AeNx9iNb91V0mz2Ma5+/WVi7DAijdVFn 31 | 0n0+isn52QqNcUUrXvesY8Gm/QaClZipKk/Vq6DLdPOLZuZR2YcKh5OGRlxP8ouY 32 | IrNf9XSm5yAm0qbbHQiQlTeg+pIu8UJkPpg20/vhqiLPygYOkD5YKrW0cwVZXFZq 33 | +EzEmM77LOI6sp1aRq+GAVv5gfB4AqrfhxGAXK9TYnVQ7w4= 34 | -----END CERTIFICATE----- 35 | -------------------------------------------------------------------------------- /tests/ssl/dummy-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQC/48UKxs++PJD+ 3 | MniBEF0yc2crUS2fPg1NA0j92sg0Vlh28maZBDtQMJw6DZDVb74paw8jLSZHdmTQ 4 | EKsE0uvAl++gvN6pZulpbTWlNlLcvSG2FEYnOCb/VxwuMfqx3ijANtrH4fx3eYB2 5 | TV1gZkXcmncO7a048WuBlpaVFDqvmJiObKeKA6XXnqC4ih19R4Yqq+bBzIdSYeS8 6 | 9n8GaOrO9Y7AbMtuMT+x1YtfUPsipbz4/76qqX4WC9PhqpLAHY71suQTT5l2N+eY 7 | 3uuvqTRwxxda7Xdq/ABp1/kmStQwVTw/iRQPrCr1DxJy8jm1Dyk3IlvBNv8oMCdr 8 | f+rfmljPIxA65LiRn/8oE+D32NbhDAo8zGzXS9BhBqtY6Qj8HgSTiJhLw2ZzIfLW 9 | KRbfUO6dv3XsKx8vbF+kMqPZ1E+bACv3lCP6XJsihugMY3uVyyAFIAyRuczbRsLA 10 | trZwbxYPQ/gvKWxNAg63Iwp/kluGwO6om+pp913z9YJWQr9pIO/ISayiKzXN7tqJ 11 | /BgTgLlD0fWexJOXkccfT9NjSV3DIq9m2McwN8GHspnhlf8aZO4X48RGIS+vkH1o 12 | xgLJaZt0zmX43aZm83Aip4ykQyYg6ixDObrrpSG76UiTH98s7+CAFFWO5usPIiHE 13 | 5yQ9NTaBjHwMXm/3uLpEQ+ESKMCkwQIDAQABAoICAF+mjeXdTFiroCrVxbOwEITB 14 | eb/h6zfhmoe1B4FiuUE9eUNxeSr1LQu/72AQuw1pcgT7VMRYESi2H3KHnHf/G30Z 15 | P12ESAlxPxBKW99KwOs/a7pzSLTsDKRjK6zrROe8sdt+fHf+cfasHhjaX51Z3aEl 16 | bguG9j3YOZqTEeSl/Mri6ci06J6nSte8Pqk+T4zPRlWm8pPP+/RYz8hRpufvDHy1 17 | cr8AfDclXXar15lfqI+Qxi3obYZsjmk25BstB5G0KjrXPVFS8FA5dbyCAkHBul4t 18 | H7s3e7tcemhIO+2Wh0bAdhPFpLZbP95/8NZTX+ic8hKFke8yFuZVepDfZpinO3TH 19 | LAJvmnWwKnYgn3jE8ffLytTzek2Mpo+K/87ACgr8wQn5gXLekxtyrX8OQ+zitqSf 20 | +w8Wzv8x5NOOtfcUYSRTzpPxdCjx0ZWa9CU+w23rJPJgjAGYLbNoVJmI12ZSLNGR 21 | j/ETODrSUFRKFxELNfCdZEH1QBE+PbaE/pyufUlsR1g5nY9jSg9jgG7Yco9CNvS4 22 | mf1FoOmV2nC+8ooeAAbCjHbbmS8OERqhInTJz+CrMThX+L4t89B4vXWOb/M7Yed5 23 | QXqcpWbk1Goai8Enz6hMYkXpwFkgcVVMYNiY5fOCLF5+GDOYQFUgzUeUaNK+V0Wd 24 | T4XjvbwsWQBRNuuzfU9xAoIBAQD0E+62on44lYQe4GIeBKJX5St6325sBXtAYX8D 25 | dY7d5qPsJ8RHvbXFnIXH1r+lmq6vTgGehCa5OMlBnt6yJJr9kqs9ujcrG+SBpLRN 26 | LXA8g7n2BrCeEmEj8JfgW2ekIk3Vso2IoWnYMK+zIXiquNaBw0yvh6uMpy4aqrmF 27 | cPGnuMLz/txSzGz2W6jkI3ci7WdlcBBSQLDPxIwXNzdVSTLorvfED8CzPPOke9W6 28 | cRaB4HbmdmIc70sVgBUUyn2FR1W6JKRu8t/FuOoOyyrkSwpiAwJEhNjzbJAI1zzz 29 | Kg+us7orpAm4tpat4EA7F/lavvt7A5SRuBp/r/xmvfM45ykHAoIBAQDJQ0Blp7KW 30 | hiWkJjhZCF+CQNWGOAwhbfTG456HgFhKJbg8FmhTRTuTX2wHU+jTNAd1+BcLoho3 31 | DdyDLB4g0YCtOMlwFZynblHzwirFFVfQujd/rAagq7906kfzyCe39qQyrAxd2/xw 32 | hu4vV988FrM61jq3bVGxJ+S1iDNYw/t1Mlis3lHEBo5o3weENmBVWNSvXFqCgy4M 33 | LHa/lw3TdstzHpvXj5MRyqFkDwRFFyIMeja/68MNG1Kr97q57BUCyaUutBZZ80np 34 | YzkbO7FziZ6qbuLDksVbtRBPe8xllHLr2lwxLMIU4NRZyAT86qNGsApPnr9ODahr 35 | n3MSdk4X3rn3AoIBAQDTsvIqsJe/5lcZHM+db7GLgPcMdPzmbn6voaCz1GQdLW3i 36 | Z7+D5hTiGFektCu3rIl0/bjDz6Vyo8FTzEMlykAwTeV+/aPaHTA+DihghFfD9RD3 37 | RmgsQo7EyGpCq6UiJKrT/jFqX25ZmCjcutxZX0aWeFlsKcVukpaXhJqzFfpT2hol 38 | 3Vkl669aorfDYMt1nOpAfkl5vihdnQFRJZA1xe6FCTVXdb5S+Dvu34XKV0oJTjJy 39 | xB1nMVozhMtEJDlovy2o7R0+KiRS74b7W9aQ+lFAH5H48izmPbRUJrPzyPifM733 40 | Gilgb+YTW9z6JFogDmQ7FyjmlwNM2syWJIzwPvdDAoIBAQC0z1tCODdD7X5Rixii 41 | O9h6Dz8E1sNnIP5/06vvNcmby2lJaiQNcyxDiL1nk+WeIKb3P4uMovQEM8rAeVkT 42 | yMNeW570uCXFcWHkqLJ93l/HIBSN+YD2xXU6VuOPSmkMZ2M6NsDhbanLehzvoXTm 43 | 6cnY+O9FLMvwaNOalqLygxccQb/SheRVREKaSovZJnTDGAvzAvg5OhqbSzLfipgc 44 | OyQp5vzA2raYjD8Twj3myBKJvR4Eq4zO8JYD8onpUAPMPlXMsHNIGj5zkvWR1r3j 45 | +2X03auRYgE2E2N01NZbB9N6ufCLKRevZBDCG+UHRtCqx6prv0VEnRaKoXPiyS/9 46 | V9YfAoIBAHBh1gQ67T0FZt05oenLdhH7skS/m2oSPD1qsdgOxya+e1pjnI9krtKg 47 | QY7heQNPWUtcC0YLwC29PoWe125eYWzYLH0pTCIOe1l+MJoAvL2EI/+yvXvjEoFn 48 | FkhpO3N0h9JTMmrBKpVTkPVfDQGdHb1Vtm8Jx1Qo+Gzj5gix/QUfqn0Gm4yQiHQU 49 | 5WGrp/7EQ/NR/IPkZQfLmpzq/oIlzoN5IqtSq/LmuNXZacmXZVqkI5UfjPS1oR/C 50 | QtTD60R4/QrAzWmfFp3Wo7CpbOhk6WbAMdifxY5V3JtBHuxn1vdmFkqZlEkGE7bY 51 | qK9UJyw/XeyqX7BsMcKq5pQ1ywSq6yo= 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /tests/types/Router.ts: -------------------------------------------------------------------------------- 1 | // THIS FILE CAN BE TYPE CHECKED TO ENSURE THE TYPES ARE CORRECT 2 | 3 | import { Router, Request, Response, MiddlewareNext } from '../../types/index'; 4 | 5 | // Create a new router instance 6 | const router = new Router(); 7 | 8 | // Pattern + Handler 9 | router.any('/', async (request, response) => { 10 | const body = await request.json(); 11 | }); 12 | 13 | // Pattern + Options + Handler 14 | router.all( 15 | '/', 16 | { 17 | max_body_length: 250, 18 | }, 19 | async (request, response) => { 20 | const body = await request.json(); 21 | } 22 | ); 23 | 24 | const middleware = (request: Request, response: Response, next: MiddlewareNext) => {}; 25 | 26 | // Pattern + 2 Middlewares + Handler 27 | router.connect( 28 | '/', 29 | middleware, 30 | async (request, repsonse, next) => { 31 | await request.text(); 32 | next(); 33 | }, 34 | async (request, response) => { 35 | const body = await request.json(); 36 | } 37 | ); 38 | 39 | // Pattern + options + 4 Middlewares + Handler 40 | router.post( 41 | '/', 42 | { 43 | max_body_length: 250, 44 | }, 45 | middleware, 46 | middleware, 47 | middleware, 48 | async (request, repsonse, next) => { 49 | await request.text(); 50 | next(); 51 | }, 52 | async (request, response) => { 53 | const body = await request.json(); 54 | } 55 | ); 56 | 57 | // Pattern + 4 Middlewares (Array) + Handler 58 | router.put( 59 | '/', 60 | [ 61 | middleware, 62 | middleware, 63 | middleware, 64 | async (request, repsonse, next) => { 65 | await request.text(); 66 | next(); 67 | }, 68 | ], 69 | async (request, response) => { 70 | const body = await request.json(); 71 | } 72 | ); 73 | 74 | // Pattern + options + 4 Middlewares (Array) + Handler 75 | router.delete( 76 | '/', 77 | { 78 | max_body_length: 250, 79 | }, 80 | [ 81 | middleware, 82 | middleware, 83 | middleware, 84 | async (request, repsonse, next) => { 85 | await request.text(); 86 | next(); 87 | }, 88 | ], 89 | async (request, response) => { 90 | const body = await request.json(); 91 | } 92 | ); 93 | 94 | // Handler 95 | router 96 | .route('/api/v1') 97 | .get(async (request, response) => { 98 | const body = await request.json(); 99 | }) 100 | .post( 101 | { 102 | max_body_length: 250, 103 | }, 104 | async (request, response, next) => { 105 | const body = await request.json(); 106 | }, 107 | async (request, response) => { 108 | const body = await request.json(); 109 | } 110 | ) 111 | .delete( 112 | { 113 | max_body_length: 250, 114 | }, 115 | middleware, 116 | [middleware, middleware], 117 | async (request, response) => { 118 | const body = await request.json(); 119 | } 120 | ); 121 | 122 | // Ensures router usage is valid in all possible forms 123 | router.use(router); 124 | router.use('/something', router); 125 | router.use('/something', middleware); 126 | router.use(middleware, middleware, middleware); 127 | router.use('else', middleware, [middleware, middleware, middleware], middleware); 128 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2021", 4 | "moduleResolution": "node", 5 | "typeRoots": [ 6 | "./types/*.d.ts", 7 | "./node_modules/@types" 8 | ], 9 | "types": [ 10 | "node", 11 | "express" 12 | ] 13 | }, 14 | "exclude": [ 15 | "./src" 16 | ] 17 | } -------------------------------------------------------------------------------- /types/components/Server.d.ts: -------------------------------------------------------------------------------- 1 | import { ReadableOptions, WritableOptions } from 'stream'; 2 | import * as uWebsockets from 'uWebSockets.js'; 3 | import { SendableData } from './http/Response'; 4 | import { Request } from './http/Request'; 5 | import { Response } from './http/Response'; 6 | import { Router } from './router/Router'; 7 | import { HostManager } from './plugins/HostManager'; 8 | 9 | export interface ServerConstructorOptions { 10 | key_file_name?: string; 11 | cert_file_name?: string; 12 | passphrase?: string; 13 | dh_params_file_name?: string; 14 | ssl_prefer_low_memory_usage?: boolean; 15 | auto_close?: boolean; 16 | fast_buffers?: boolean; 17 | fast_abort?: boolean; 18 | trust_proxy?: boolean; 19 | max_body_buffer?: number; 20 | max_body_length?: number; 21 | streaming?: { 22 | readable?: ReadableOptions; 23 | writable?: WritableOptions; 24 | }; 25 | } 26 | 27 | export type GlobalErrorHandler = (request: Request, response: Response, error: Error) => void; 28 | export type GlobalNotFoundHandler = (request: Request, response: Response) => void; 29 | 30 | export class Server extends Router { 31 | constructor(options?: ServerConstructorOptions); 32 | 33 | /** 34 | * This object can be used to store properties/references local to this Server instance. 35 | */ 36 | locals: Object; 37 | 38 | /* Server Methods */ 39 | 40 | /** 41 | * Starts HyperExpress webserver on specified port and host. 42 | * 43 | * @param {Number} port 44 | * @param {String=} host Optional. Default: 0.0.0.0 45 | * @param {Function=} callback Optional. Callback to be called when the server is listening. Default: "0.0.0.0" 46 | * @returns {Promise} Promise 47 | */ 48 | listen( 49 | port: number, 50 | callback?: (listen_socket: uWebsockets.us_listen_socket) => void 51 | ): Promise; 52 | listen( 53 | port: number, 54 | host?: string, 55 | callback?: (listen_socket: uWebsockets.us_listen_socket) => void 56 | ): Promise; 57 | listen( 58 | unix_path: string, 59 | callback?: (listen_socket: uWebsockets.us_listen_socket) => void 60 | ): Promise; 61 | 62 | /** 63 | * Performs a graceful shutdown of the server and closes the listen socket once all pending requests have been completed. 64 | * 65 | * @param {uWebSockets.us_listen_socket=} [listen_socket] Optional 66 | * @returns {Promise} 67 | */ 68 | shutdown(listen_socket?: uWebsockets.us_listen_socket): Promise; 69 | 70 | /** 71 | * Stops/Closes HyperExpress webserver instance. 72 | * 73 | * @param {uWebSockets.us_listen_socket=} [listen_socket] Optional 74 | * @returns {Boolean} 75 | */ 76 | close(listen_socket?: uWebsockets.us_listen_socket): boolean; 77 | 78 | /** 79 | * Sets a global error handler which will catch most uncaught errors across all routes/middlewares. 80 | * 81 | * @param {GlobalErrorHandler} handler 82 | */ 83 | set_error_handler(handler: GlobalErrorHandler): void; 84 | 85 | /** 86 | * Sets a global not found handler which will handle all requests that are unhandled by any registered route. 87 | * Note! This handler must be registered after all routes and routers. 88 | * 89 | * @param {GlobalNotFoundHandler} handler 90 | */ 91 | set_not_found_handler(handler: GlobalNotFoundHandler): void; 92 | 93 | /** 94 | * Publish a message to a topic in MQTT syntax to all WebSocket connections on this Server instance. 95 | * You cannot publish using wildcards, only fully specified topics. 96 | * 97 | * @param {String} topic 98 | * @param {String|Buffer|ArrayBuffer} message 99 | * @param {Boolean=} is_binary 100 | * @param {Boolean=} compress 101 | * @returns {Boolean} 102 | */ 103 | publish(topic: string, message: SendableData, is_binary?: boolean, compress?: boolean): boolean; 104 | 105 | /** 106 | * Returns the number of subscribers to a topic across all WebSocket connections on this Server instance. 107 | * 108 | * @param {String} topic 109 | * @returns {Number} 110 | */ 111 | num_of_subscribers(topic: string): number; 112 | 113 | /* Server Properties */ 114 | 115 | /** 116 | * Returns the local server listening port of the server instance. 117 | * @returns {Number} 118 | */ 119 | get port(): number; 120 | 121 | /** 122 | * Returns the server's internal uWS listening socket. 123 | * @returns {uWebSockets.us_listen_socket=} 124 | */ 125 | get socket(): uWebsockets.us_listen_socket | null; 126 | 127 | /** 128 | * Underlying uWS instance. 129 | * @returns {uWebSockets.TemplatedApp} 130 | */ 131 | get uws_instance(): uWebsockets.TemplatedApp; 132 | 133 | /** 134 | * Server instance global handlers. 135 | * @returns {Object} 136 | */ 137 | get handlers(): Object; 138 | 139 | /** 140 | * Returns the Server Hostnames manager for this instance. 141 | * Use this to support multiple hostnames on the same server with different SSL configurations. 142 | * @returns {HostManager} 143 | */ 144 | get hosts(): HostManager; 145 | } 146 | -------------------------------------------------------------------------------- /types/components/middleware/MiddlewareHandler.d.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareNext } from './MiddlewareNext'; 2 | import { Request, DefaultRequestLocals } from '../http/Request'; 3 | import { Response, DefaultResponseLocals } from '../http/Response'; 4 | 5 | export type MiddlewarePromise = Promise; 6 | export type MiddlewareHandler = ( 7 | request: Request, 8 | response: Response, 9 | next: MiddlewareNext 10 | ) => MiddlewarePromise | any; 11 | -------------------------------------------------------------------------------- /types/components/middleware/MiddlewareNext.d.ts: -------------------------------------------------------------------------------- 1 | export type MiddlewareNext = (error?: Error) => void; -------------------------------------------------------------------------------- /types/components/plugins/HostManager.d.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | 3 | interface HostOptions { 4 | passphrase?: string, 5 | cert_file_name?: string, 6 | key_file_name?: string, 7 | dh_params_file_name?: string, 8 | ssl_prefer_low_memory_usage?: boolean, 9 | } 10 | 11 | export class HostManager extends EventEmitter { 12 | /** 13 | * Registers the unique host options to use for the specified hostname for incoming requests. 14 | * 15 | * @param {String} hostname 16 | * @param {HostOptions} options 17 | * @returns {HostManager} 18 | */ 19 | add(hostname: string, options: HostOptions): HostManager; 20 | 21 | /** 22 | * Un-Registers the unique host options to use for the specified hostname for incoming requests. 23 | * 24 | * @param {String} hostname 25 | * @returns {HostManager} 26 | */ 27 | remove(hostname: string): HostManager; 28 | 29 | /* HostManager Getters & Properties */ 30 | 31 | /** 32 | * Returns all of the registered hostname options. 33 | * @returns {Object.} 34 | */ 35 | get registered(): {[hostname: string]: HostOptions}; 36 | } -------------------------------------------------------------------------------- /types/components/plugins/LiveFile.d.ts: -------------------------------------------------------------------------------- 1 | import * as FileSystem from 'fs'; 2 | import { EventEmitter} from 'events'; 3 | 4 | export interface LiveFileOptions { 5 | path: string, 6 | retry: { 7 | every: number, 8 | max: number 9 | } 10 | } 11 | 12 | export class LiveFile extends EventEmitter { 13 | constructor(options: LiveFileOptions) 14 | 15 | /** 16 | * Reloads buffer/content for file asynchronously with retry policy. 17 | * 18 | * @private 19 | * @param {Boolean} fresh 20 | * @param {Number} count 21 | * @returns {Promise} 22 | */ 23 | reload(fresh: boolean, count: number): Promise; 24 | 25 | /** 26 | * Returns a promise which resolves once first reload is complete. 27 | * 28 | * @returns {Promise} 29 | */ 30 | ready(): Promise 31 | 32 | /* LiveFile Getters */ 33 | get is_ready(): boolean; 34 | 35 | get name(): string; 36 | 37 | get path(): string; 38 | 39 | get extension(): string; 40 | 41 | get content(): string; 42 | 43 | get buffer(): Buffer; 44 | 45 | get last_update(): number; 46 | 47 | get watcher(): FileSystem.FSWatcher; 48 | } 49 | -------------------------------------------------------------------------------- /types/components/plugins/MultipartField.d.ts: -------------------------------------------------------------------------------- 1 | import { Readable, WritableOptions } from 'stream'; 2 | 3 | export type MultipartFile = { 4 | name?: string, 5 | stream: Readable 6 | } 7 | 8 | export type Truncations = { 9 | name: boolean, 10 | value: boolean 11 | } 12 | 13 | export class MultipartField { 14 | /* MultipartField Methods */ 15 | 16 | /** 17 | * Saves this multipart file content to the specified path. 18 | * Note! You must specify the file name and extension in the path itself. 19 | * 20 | * @param {String} path Path with file name to which you would like to save this file. 21 | * @param {WritableOptions} options Writable stream options 22 | * @returns {Promise} 23 | */ 24 | write(path: string, options?: WritableOptions): Promise; 25 | 26 | /* MultipartField Properties */ 27 | 28 | /** 29 | * Field name as specified in the multipart form. 30 | * @returns {String} 31 | */ 32 | get name(): string; 33 | 34 | /** 35 | * Field encoding as specified in the multipart form. 36 | * @returns {String} 37 | */ 38 | get encoding(): string; 39 | 40 | /** 41 | * Field mime type as specified in the multipart form. 42 | * @returns {String} 43 | */ 44 | get mime_type(): string; 45 | 46 | /** 47 | * Returns file information about this field if it is a file type. 48 | * Note! This property will ONLY be defined if this field is a file type. 49 | * 50 | * @returns {MultipartFile} 51 | */ 52 | get file(): MultipartFile | void; 53 | 54 | /** 55 | * Returns field value if this field is a non-file type. 56 | * Note! This property will ONLY be defined if this field is a non-file type. 57 | * 58 | * @returns {String} 59 | */ 60 | get value(): string | void; 61 | 62 | /** 63 | * Returns information about truncations in this field. 64 | * Note! This property will ONLY be defined if this field is a non-file type. 65 | * 66 | * @returns {Truncations} 67 | */ 68 | get truncated(): Truncations | void; 69 | } 70 | 71 | export type MultipartHandler = (field: MultipartField) => void | Promise; 72 | 73 | export type MultipartLimitReject = "PARTS_LIMIT_REACHED" | "FILES_LIMIT_REACHED" | "FIELDS_LIMIT_REACHED"; -------------------------------------------------------------------------------- /types/components/plugins/SSEventStream.d.ts: -------------------------------------------------------------------------------- 1 | import { Response } from "../http/Response"; 2 | 3 | export class SSEventStream { 4 | constructor(response: Response) 5 | 6 | /** 7 | * Opens the "Server-Sent Events" connection to the client. 8 | * 9 | * @returns {Boolean} 10 | */ 11 | open(): boolean; 12 | 13 | /** 14 | * Closes the "Server-Sent Events" connection to the client. 15 | * 16 | * @returns {Boolean} 17 | */ 18 | close(): boolean; 19 | 20 | /** 21 | * Sends a comment-type message to the client that will not be emitted by EventSource. 22 | * This can be useful as a keep-alive mechanism if messages might not be sent regularly. 23 | * 24 | * @param {String} data 25 | * @returns {Boolean} 26 | */ 27 | comment(data: string): boolean; 28 | 29 | /** 30 | * Sends a message to the client based on the specified event and data. 31 | * Note! You must retry failed messages if you receive a false output from this method. 32 | * 33 | * @param {String} id 34 | * @param {String=} event 35 | * @param {String=} data 36 | * @returns {Boolean} 37 | */ 38 | send(data: string): boolean; 39 | send(event: string, data: string): boolean; 40 | send(id: string, event: string, data: string): boolean; 41 | 42 | /* SSEventStream properties */ 43 | 44 | /** 45 | * Whether this Server-Sent Events stream is still active. 46 | * 47 | * @returns {Boolean} 48 | */ 49 | get active(): boolean; 50 | } -------------------------------------------------------------------------------- /types/components/router/Route.d.ts: -------------------------------------------------------------------------------- 1 | // Since this a private component no types have been defined -------------------------------------------------------------------------------- /types/components/ws/WebsocketRoute.d.ts: -------------------------------------------------------------------------------- 1 | // Since this a private component no types have been defined -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Server } from './components/Server'; 2 | 3 | export * as compressors from 'uWebSockets.js'; 4 | export * from './components/Server'; 5 | export * from './components/router/Router'; 6 | export * from './components/http/Request'; 7 | export * from './components/http/Response'; 8 | export * from './components/middleware/MiddlewareHandler'; 9 | export * from './components/middleware/MiddlewareNext'; 10 | export * from './components/plugins/LiveFile'; 11 | export * from './components/plugins/MultipartField'; 12 | export * from './components/plugins/SSEventStream'; 13 | export * from './components/ws/Websocket'; 14 | 15 | export const express: (...args: ConstructorParameters) => Server; 16 | -------------------------------------------------------------------------------- /types/shared/operators.d.ts: -------------------------------------------------------------------------------- 1 | export function wrap_object(original: object, target: object): void; 2 | 3 | export type PathKeyItem = [key: string, index: number]; 4 | export function parse_path_parameters(pattern: string): PathKeyItem[]; 5 | 6 | export function array_buffer_to_string(array_buffer: ArrayBuffer, encoding?: string): string; 7 | 8 | export function async_wait(delay: number): Promise; 9 | 10 | export function merge_relative_paths(base_path: string, new_path: string): string; --------------------------------------------------------------------------------