├── .gitignore
├── .prettierrc
├── .eslintrc.js
├── rollup.config.js
├── README.md
├── tsconfig.json
├── package.json
├── LICENSE
└── src
└── bot.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | *.sh
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "trailingComma": "all",
4 | "singleQuote": true,
5 | "tabWidth": 4
6 | }
7 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | extends: ['plugin:prettier/recommended'],
4 | rules: {},
5 | };
6 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import typescript from '@rollup/plugin-typescript';
2 |
3 | export default {
4 | input: 'src/bot.ts',
5 | output: {
6 | dir: 'dist',
7 | },
8 | plugins: [typescript()],
9 | };
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NGINX NJS Bot Protection 🤖⛔
2 |
3 | > Source code for the NJS bot protection module post
4 |
5 | ## Installation
6 |
7 | 1. Build the module:
8 |
9 | ```bash
10 | npm run build
11 | ```
12 |
13 | 2. Follow the [Building a Simple Bot Protection With NGINX JavaScript Module (NJS) and TypeScript](https://fsjohnny.medium.com/building-a-simple-bot-protection-with-nginx-javascript-module-njs-and-typescript-386b2207ba90) post.
14 |
15 | 
16 |
17 | via GIPHY
18 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES5",
4 | "module": "es2015",
5 | "lib": [
6 | "ES2015",
7 | "ES2016.Array.Include",
8 | "ES2017.Object",
9 | "ES2017.String"
10 | ],
11 |
12 | "strict": true,
13 | "noImplicitAny": true,
14 | "strictNullChecks": true,
15 | "strictFunctionTypes": true,
16 | "strictBindCallApply": true,
17 | "strictPropertyInitialization": true,
18 | "noImplicitThis": true,
19 | "alwaysStrict": true,
20 |
21 | "moduleResolution": "node",
22 |
23 | "skipLibCheck": true,
24 | "forceConsistentCasingInFileNames": true,
25 | },
26 | "include": [
27 | "./src",
28 | ],
29 | "files": [
30 | "./node_modules/njs-types/ngx_http_js_module.d.ts",
31 | ],
32 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nginx-njs",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "build:clean": "rm -rf dist && mkdir dist",
8 | "build:rollup": "rollup -c",
9 | "build": "npm run build:clean && npm run build:rollup",
10 | "format": "prettier --write \"src/**/*.ts\" ",
11 | "lint": "eslint 'src/**/*.ts' --fix"
12 | },
13 | "keywords": [],
14 | "author": "",
15 | "license": "ISC",
16 | "devDependencies": {
17 | "@rollup/plugin-typescript": "^8.2.1",
18 | "@types/node": "^15.0.1",
19 | "@typescript-eslint/eslint-plugin": "^4.22.0",
20 | "@typescript-eslint/parser": "^4.22.0",
21 | "eslint": "^7.25.0",
22 | "eslint-config-prettier": "^8.3.0",
23 | "eslint-plugin-prettier": "^3.4.0",
24 | "njs-types": "^0.5.3",
25 | "prettier": "^2.2.1",
26 | "rollup": "^2.46.0",
27 | "typescript": "^4.2.4"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Johnny Tordgeman
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.
22 |
--------------------------------------------------------------------------------
/src/bot.ts:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const crypto = require('crypto');
3 |
4 | const noCookiesFileName = '/var/lib/njs/cookies.txt';
5 | const badReputationIPs = loadFile('/var/lib/njs/ips.txt');
6 | const noCookieIPs = loadFile(noCookiesFileName);
7 |
8 | let response = '';
9 | const topSecretKey = 'YouWillNeverGuessMe';
10 | const blockedHoursMS = 1 * 1000 * 60 * 60;
11 |
12 | function getCookiePayload(name: string, value: string, validHours: number): string {
13 | const date = new Date();
14 | date.setTime(date.getTime() + validHours * 1000 * 60 * 60);
15 | const dateTime = date.getTime();
16 |
17 | const payload = `${value}${dateTime}`;
18 |
19 | // hash the cookie payload
20 | const hmac = crypto.createHmac('sha256', topSecretKey);
21 | const cookieValue = hmac.update(payload).digest('hex');
22 |
23 | // return the cookie
24 | return `${name}=${dateTime}:${cookieValue}; expires=${date.toUTCString()}; path=/`;
25 | }
26 |
27 | function loadFile(file: string): string[] {
28 | let data: string[] = [];
29 | try {
30 | data = fs.readFileSync(file).toString().split('\n');
31 | } catch (e) {
32 | // unable to read file
33 | }
34 | return data;
35 | }
36 |
37 | function updateFile(file: string, dataArray: string[]): void {
38 | try {
39 | fs.writeFileSync(file, dataArray.join('\n'));
40 | } catch (e) {
41 | // unable to write file
42 | }
43 | }
44 |
45 | function verifyIP(r: NginxHTTPRequest): boolean {
46 | return badReputationIPs.some((ip: string) => ip === r.remoteAddress);
47 | }
48 |
49 | function verifyJSCookie(r: NginxHTTPRequest): boolean {
50 | const cookies = r.headersIn.Cookie;
51 | const njsCookie =
52 | cookies &&
53 | cookies
54 | .split(';')
55 | .map((v) => v.split('='))
56 | .find((x) => x[0] === 'njs');
57 |
58 | try {
59 | if (!njsCookie || njsCookie.length < 2) {
60 | // no njs cookie or wrong cookie array length
61 | const foundIP = noCookieIPs.find((ip: string) => ip.match(r.remoteAddress));
62 | if (foundIP && Date.now() - parseInt(foundIP.split(':')[1]) <= blockedHoursMS) {
63 | return false;
64 | } else {
65 | const ipIndex = noCookieIPs.findIndex((item: string) => item === foundIP);
66 | if (ipIndex) {
67 | noCookieIPs.splice(ipIndex, 1);
68 | updateFile(noCookiesFileName, noCookieIPs);
69 | }
70 | return true;
71 | }
72 | }
73 | // njs cookie found, validate it
74 | const cookieValue = njsCookie && njsCookie[1];
75 | if (cookieValue) {
76 | const [cookieTimestamp, cookiePayload] = cookieValue.split(':');
77 | const requestSignature = `${r.headersIn['User-Agent']}${r.remoteAddress}${cookieTimestamp}`;
78 | const requestSignatureHmac = crypto.createHmac('sha256', topSecretKey);
79 | const requestSignatureHex = requestSignatureHmac.update(requestSignature).digest('hex');
80 | return requestSignatureHex === cookiePayload;
81 | }
82 |
83 | return false; // if all fails - block the request
84 | } catch (e) {
85 | // something went wrong - block the request
86 | return true; // if all fails - fail open
87 | }
88 | }
89 |
90 | function addSnippet(r: NginxHTTPRequest, data: string | Buffer, flags: NginxHTTPSendBufferOptions) {
91 | response += data;
92 |
93 | if (flags.last) {
94 | const signature = `${r.headersIn['User-Agent']}${r.remoteAddress}`;
95 | const injectedResponse = response.replace(
96 | /<\/head>/,
97 | ``,
98 | );
99 | r.sendBuffer(injectedResponse, flags);
100 | }
101 | }
102 |
103 | function verify(r: NginxHTTPRequest): void {
104 | if (!verifyIP(r) || !verifyJSCookie(r)) {
105 | r.return(302, '/block.html');
106 | return;
107 | }
108 | r.internalRedirect('@pages');
109 | }
110 |
111 | export default { addSnippet, verify };
112 |
--------------------------------------------------------------------------------