hapi-terminator 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/dist/index.d.ts +111 -0
- package/dist/index.js +56 -0
- package/dist/package.json +52 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kamaal Farah
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# hapi-terminator
|
|
2
|
+
|
|
3
|
+
A Hapi plugin that terminates requests with payloads that exceed a specified size limit. This plugin helps protect your server from excessively large payloads by destroying the socket connection before the entire payload is processed.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🛡️ Protects against large payload attacks
|
|
8
|
+
- 🔀 Different limits for registered vs unregistered routes
|
|
9
|
+
- 🎯 Configurable thresholds using numbers or custom functions
|
|
10
|
+
- ⚡ Terminates connections early to save resources
|
|
11
|
+
- 📦 TypeScript support included
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install hapi-terminator
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
or with other package managers:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
yarn add hapi-terminator
|
|
23
|
+
bun add hapi-terminator
|
|
24
|
+
pnpm add hapi-terminator
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### Basic Example
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import Hapi from '@hapi/hapi';
|
|
33
|
+
import terminatorPlugin, { type TerminatorOptions } from 'hapi-terminator';
|
|
34
|
+
|
|
35
|
+
const server = Hapi.server({ port: 3000, host: '127.0.0.1' });
|
|
36
|
+
|
|
37
|
+
const requestTerminateOptions: TerminatorOptions = {
|
|
38
|
+
unregisteredLimit: 500 * 1024, // 500KB for unregistered routes
|
|
39
|
+
registeredLimit: 500 * 1024, // 500KB for registered routes
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
await server.register({
|
|
43
|
+
plugin: terminatorPlugin,
|
|
44
|
+
options: requestTerminateOptions,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
server.route({
|
|
48
|
+
method: ['GET', 'POST'],
|
|
49
|
+
path: '/',
|
|
50
|
+
handler: () => 'Hello World!',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await server.start();
|
|
54
|
+
console.log('Server running on %s', server.info.uri);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Using Custom Functions
|
|
58
|
+
|
|
59
|
+
You can provide custom functions to determine whether a request should be terminated:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
const requestTerminateOptions: TerminatorOptions = {
|
|
63
|
+
registeredLimit: (request, size) => {
|
|
64
|
+
// Custom logic based on request properties
|
|
65
|
+
if (request.path === '/upload') {
|
|
66
|
+
return size > 10 * 1024 * 1024; // 10MB for upload route
|
|
67
|
+
}
|
|
68
|
+
return size > 500 * 1024; // 500KB for other routes
|
|
69
|
+
},
|
|
70
|
+
unregisteredLimit: (request, size) => {
|
|
71
|
+
return size > 100 * 1024; // 100KB for unregistered routes
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Configuration
|
|
77
|
+
|
|
78
|
+
### TerminatorOptions
|
|
79
|
+
|
|
80
|
+
| Option | Type | Description |
|
|
81
|
+
| ------------------- | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
82
|
+
| `registeredLimit` | `number \| ((request: Request, size: number) => boolean)` | Maximum payload size for registered routes. Can be a number in bytes or a function that returns `true` if the request should be terminated. |
|
|
83
|
+
| `unregisteredLimit` | `number \| ((request: Request, size: number) => boolean)` | Maximum payload size for unregistered routes. Can be a number in bytes or a function that returns `true` if the request should be terminated. |
|
|
84
|
+
|
|
85
|
+
### Behavior
|
|
86
|
+
|
|
87
|
+
- **Registered Routes**: When a payload exceeds the limit on a registered route, the socket is destroyed and a `413 Payload Too Large` error is thrown.
|
|
88
|
+
- **Unregistered Routes**: When a payload exceeds the limit on an unregistered route, the socket is destroyed and a `404 Not Found` error is thrown.
|
|
89
|
+
- **Disabled**: Set to `null`, `undefined`, or a negative number to disable termination for that category.
|
|
90
|
+
|
|
91
|
+
## How It Works
|
|
92
|
+
|
|
93
|
+
The plugin hooks into Hapi's `onRequest` extension point and checks the `Content-Length` header of incoming requests. If the content length exceeds the configured threshold:
|
|
94
|
+
|
|
95
|
+
1. The socket connection is immediately destroyed
|
|
96
|
+
2. An appropriate error response is thrown (413 for registered routes, 404 for unregistered routes)
|
|
97
|
+
3. No further processing occurs, saving server resources
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
|
102
|
+
|
|
103
|
+
## Author
|
|
104
|
+
|
|
105
|
+
Kamaal Farah
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { Server, Request } from '@hapi/hapi';
|
|
2
|
+
export type TerminatorOptions = {
|
|
3
|
+
registeredLimit?: number | ((request: Request, size: number) => boolean);
|
|
4
|
+
unregisteredLimit?: number | ((request: Request, size: number) => boolean);
|
|
5
|
+
};
|
|
6
|
+
export declare const plugin: {
|
|
7
|
+
pkg: {
|
|
8
|
+
name: string;
|
|
9
|
+
main: string;
|
|
10
|
+
typings: string;
|
|
11
|
+
type: string;
|
|
12
|
+
version: string;
|
|
13
|
+
license: string;
|
|
14
|
+
author: {
|
|
15
|
+
name: string;
|
|
16
|
+
};
|
|
17
|
+
devDependencies: {
|
|
18
|
+
"@eslint/js": string;
|
|
19
|
+
"@hapi/hapi": string;
|
|
20
|
+
"@kamaalio/prettier-config": string;
|
|
21
|
+
"@types/bun": string;
|
|
22
|
+
eslint: string;
|
|
23
|
+
globals: string;
|
|
24
|
+
husky: string;
|
|
25
|
+
jiti: string;
|
|
26
|
+
"lint-staged": string;
|
|
27
|
+
prettier: string;
|
|
28
|
+
typescript: string;
|
|
29
|
+
"typescript-eslint": string;
|
|
30
|
+
};
|
|
31
|
+
dependencies: {
|
|
32
|
+
"@hapi/boom": string;
|
|
33
|
+
};
|
|
34
|
+
prettier: string;
|
|
35
|
+
"lint-staged": {
|
|
36
|
+
"**/*.{js,ts,tsx}": string[];
|
|
37
|
+
"**/*": string;
|
|
38
|
+
};
|
|
39
|
+
scripts: {
|
|
40
|
+
compile: string;
|
|
41
|
+
format: string;
|
|
42
|
+
"format:check": string;
|
|
43
|
+
lint: string;
|
|
44
|
+
prepare: string;
|
|
45
|
+
prepack: string;
|
|
46
|
+
quality: string;
|
|
47
|
+
test: string;
|
|
48
|
+
typecheck: string;
|
|
49
|
+
};
|
|
50
|
+
publishConfig: {
|
|
51
|
+
access: string;
|
|
52
|
+
};
|
|
53
|
+
files: string[];
|
|
54
|
+
};
|
|
55
|
+
register: typeof register;
|
|
56
|
+
};
|
|
57
|
+
declare function register(server: Server, options: TerminatorOptions): Promise<void>;
|
|
58
|
+
declare const _default: {
|
|
59
|
+
plugin: {
|
|
60
|
+
pkg: {
|
|
61
|
+
name: string;
|
|
62
|
+
main: string;
|
|
63
|
+
typings: string;
|
|
64
|
+
type: string;
|
|
65
|
+
version: string;
|
|
66
|
+
license: string;
|
|
67
|
+
author: {
|
|
68
|
+
name: string;
|
|
69
|
+
};
|
|
70
|
+
devDependencies: {
|
|
71
|
+
"@eslint/js": string;
|
|
72
|
+
"@hapi/hapi": string;
|
|
73
|
+
"@kamaalio/prettier-config": string;
|
|
74
|
+
"@types/bun": string;
|
|
75
|
+
eslint: string;
|
|
76
|
+
globals: string;
|
|
77
|
+
husky: string;
|
|
78
|
+
jiti: string;
|
|
79
|
+
"lint-staged": string;
|
|
80
|
+
prettier: string;
|
|
81
|
+
typescript: string;
|
|
82
|
+
"typescript-eslint": string;
|
|
83
|
+
};
|
|
84
|
+
dependencies: {
|
|
85
|
+
"@hapi/boom": string;
|
|
86
|
+
};
|
|
87
|
+
prettier: string;
|
|
88
|
+
"lint-staged": {
|
|
89
|
+
"**/*.{js,ts,tsx}": string[];
|
|
90
|
+
"**/*": string;
|
|
91
|
+
};
|
|
92
|
+
scripts: {
|
|
93
|
+
compile: string;
|
|
94
|
+
format: string;
|
|
95
|
+
"format:check": string;
|
|
96
|
+
lint: string;
|
|
97
|
+
prepare: string;
|
|
98
|
+
prepack: string;
|
|
99
|
+
quality: string;
|
|
100
|
+
test: string;
|
|
101
|
+
typecheck: string;
|
|
102
|
+
};
|
|
103
|
+
publishConfig: {
|
|
104
|
+
access: string;
|
|
105
|
+
};
|
|
106
|
+
files: string[];
|
|
107
|
+
};
|
|
108
|
+
register: typeof register;
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
export default _default;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import Boom from '@hapi/boom';
|
|
2
|
+
import pkg from './package.json';
|
|
3
|
+
export const plugin = { pkg, register };
|
|
4
|
+
async function register(server, options) {
|
|
5
|
+
server.ext('onRequest', onRequest(options));
|
|
6
|
+
}
|
|
7
|
+
function onRequest(options) {
|
|
8
|
+
return (request, h) => {
|
|
9
|
+
const unregisteredRouteHandler = handleUnregisteredRoute(request, h, options);
|
|
10
|
+
const registeredRouteHandler = handleRegisteredRoute(request, h, options);
|
|
11
|
+
const rawContentLength = request.headers['content-length'];
|
|
12
|
+
if (!rawContentLength) {
|
|
13
|
+
return h.continue;
|
|
14
|
+
}
|
|
15
|
+
const contentLength = Number.parseInt(rawContentLength, 10);
|
|
16
|
+
if (Number.isNaN(contentLength)) {
|
|
17
|
+
return h.continue;
|
|
18
|
+
}
|
|
19
|
+
const matchedRoute = request.server.match(request.method, request.path);
|
|
20
|
+
if (matchedRoute != null) {
|
|
21
|
+
return registeredRouteHandler(contentLength);
|
|
22
|
+
}
|
|
23
|
+
return unregisteredRouteHandler(contentLength);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function handleUnregisteredRoute(request, h, options) {
|
|
27
|
+
return (contentLength) => {
|
|
28
|
+
if (options.unregisteredLimit == null ||
|
|
29
|
+
(typeof options.unregisteredLimit === 'number' && options.unregisteredLimit < 0)) {
|
|
30
|
+
return h.continue;
|
|
31
|
+
}
|
|
32
|
+
if ((typeof options.unregisteredLimit === 'number' && contentLength > options.unregisteredLimit) ||
|
|
33
|
+
(typeof options.unregisteredLimit === 'function' && options.unregisteredLimit(request, contentLength))) {
|
|
34
|
+
request.raw.req.socket?.destroy();
|
|
35
|
+
throw Boom.notFound();
|
|
36
|
+
}
|
|
37
|
+
return h.continue;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function handleRegisteredRoute(request, h, options) {
|
|
41
|
+
return (contentLength) => {
|
|
42
|
+
if (options.registeredLimit == null ||
|
|
43
|
+
(typeof options.registeredLimit === 'number' && options.registeredLimit < 0)) {
|
|
44
|
+
return h.continue;
|
|
45
|
+
}
|
|
46
|
+
if ((typeof options.registeredLimit === 'number' && contentLength > options.registeredLimit) ||
|
|
47
|
+
(typeof options.registeredLimit === 'function' && options.registeredLimit(request, contentLength))) {
|
|
48
|
+
request.raw.req.socket?.destroy();
|
|
49
|
+
throw Boom.entityTooLarge(typeof options.registeredLimit === 'number'
|
|
50
|
+
? `Payload content length greater than maximum allowed: ${options.registeredLimit}`
|
|
51
|
+
: undefined);
|
|
52
|
+
}
|
|
53
|
+
return h.continue;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export default { plugin };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hapi-terminator",
|
|
3
|
+
"main": "index.js",
|
|
4
|
+
"typings": "index.d.ts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"version": "0.0.1",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": {
|
|
9
|
+
"name": "Kamaal Farah"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@eslint/js": "^9.39.2",
|
|
13
|
+
"@hapi/hapi": "^21.4.4",
|
|
14
|
+
"@kamaalio/prettier-config": "^0.1.2",
|
|
15
|
+
"@types/bun": "latest",
|
|
16
|
+
"eslint": "^9.39.2",
|
|
17
|
+
"globals": "^17.0.0",
|
|
18
|
+
"husky": "^9.1.7",
|
|
19
|
+
"jiti": "^2.6.1",
|
|
20
|
+
"lint-staged": "^16.2.7",
|
|
21
|
+
"prettier": "^3.7.4",
|
|
22
|
+
"typescript": "^5.9.3",
|
|
23
|
+
"typescript-eslint": "^8.52.0"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@hapi/boom": "^10.0.1"
|
|
27
|
+
},
|
|
28
|
+
"prettier": "@kamaalio/prettier-config",
|
|
29
|
+
"lint-staged": {
|
|
30
|
+
"**/*.{js,ts,tsx}": [
|
|
31
|
+
"eslint --fix"
|
|
32
|
+
],
|
|
33
|
+
"**/*": "prettier --write --ignore-unknown"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"compile": "tsc --project tsconfig.build.json",
|
|
37
|
+
"format": "prettier --write .",
|
|
38
|
+
"format:check": "prettier --check .",
|
|
39
|
+
"lint": "eslint .",
|
|
40
|
+
"prepare": "bunx husky",
|
|
41
|
+
"prepack": "rm -rf dist && bun run compile",
|
|
42
|
+
"quality": "bun run format:check && bun run lint && bun run typecheck",
|
|
43
|
+
"test": "bun test",
|
|
44
|
+
"typecheck": "tsc --noEmit"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"dist"
|
|
51
|
+
]
|
|
52
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hapi-terminator",
|
|
3
|
+
"main": "index.js",
|
|
4
|
+
"typings": "index.d.ts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"version": "0.0.1",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": {
|
|
9
|
+
"name": "Kamaal Farah"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@eslint/js": "^9.39.2",
|
|
13
|
+
"@hapi/hapi": "^21.4.4",
|
|
14
|
+
"@kamaalio/prettier-config": "^0.1.2",
|
|
15
|
+
"@types/bun": "latest",
|
|
16
|
+
"eslint": "^9.39.2",
|
|
17
|
+
"globals": "^17.0.0",
|
|
18
|
+
"husky": "^9.1.7",
|
|
19
|
+
"jiti": "^2.6.1",
|
|
20
|
+
"lint-staged": "^16.2.7",
|
|
21
|
+
"prettier": "^3.7.4",
|
|
22
|
+
"typescript": "^5.9.3",
|
|
23
|
+
"typescript-eslint": "^8.52.0"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@hapi/boom": "^10.0.1"
|
|
27
|
+
},
|
|
28
|
+
"prettier": "@kamaalio/prettier-config",
|
|
29
|
+
"lint-staged": {
|
|
30
|
+
"**/*.{js,ts,tsx}": [
|
|
31
|
+
"eslint --fix"
|
|
32
|
+
],
|
|
33
|
+
"**/*": "prettier --write --ignore-unknown"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"compile": "tsc --project tsconfig.build.json",
|
|
37
|
+
"format": "prettier --write .",
|
|
38
|
+
"format:check": "prettier --check .",
|
|
39
|
+
"lint": "eslint .",
|
|
40
|
+
"prepare": "bunx husky",
|
|
41
|
+
"prepack": "rm -rf dist && bun run compile",
|
|
42
|
+
"quality": "bun run format:check && bun run lint && bun run typecheck",
|
|
43
|
+
"test": "bun test",
|
|
44
|
+
"typecheck": "tsc --noEmit"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"dist"
|
|
51
|
+
]
|
|
52
|
+
}
|