├── .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 | [](https://www.npmjs.com/package/hyper-express)
7 | [](https://www.npmjs.com/package/hyper-express)
8 | [](https://github.com/kartikk221/hyper-express/issues)
9 | [](https://github.com/kartikk221/hyper-express/stargazers)
10 | [](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;
--------------------------------------------------------------------------------