├── .eslintrc.json ├── .github └── workflows │ ├── publish-in-github-package-registry.yml │ └── run-tests.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── jest.config.js ├── package.json ├── src ├── index.ts ├── interacts-with-headers.ts └── request.ts ├── test └── index.js └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true, 5 | "jest": true 6 | }, 7 | "extends": [ 8 | "standard-with-typescript" 9 | ], 10 | "parserOptions": { 11 | "project": "./tsconfig.json" 12 | }, 13 | "rules": { 14 | "@typescript-eslint/strict-boolean-expressions": 0 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/publish-in-github-package-registry.yml: -------------------------------------------------------------------------------- 1 | name: Publish in GitHub Package Registry 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2 14 | with: 15 | node-version: 14 16 | - run: npm install 17 | - run: npm test 18 | 19 | publish-gpr: 20 | needs: test 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Git checkout 24 | uses: actions/checkout@v2 25 | 26 | - name: Set Node.js Version 27 | uses: actions/setup-node@v2 28 | with: 29 | node-version: 14 30 | 31 | - name: Install dependencies 32 | run: npm install 33 | 34 | - name: Build package files 35 | run: npm run build 36 | 37 | - name: Configure GitHub Package Registry as Publish Target 38 | uses: actions/setup-node@v1 39 | with: 40 | registry-url: 'https://npm.pkg.github.com' 41 | scope: '@supercharge' 42 | 43 | - name: Publish to GitHub Package Registry 44 | run: npm publish 45 | env: 46 | NODE_AUTH_TOKEN: ${{ secrets.GH_PUBLISH_GPR_TOKEN }} 47 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [12.x, 14.x, 16.x, 18.x] 12 | 13 | name: Node ${{ matrix.node-version }} 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | 23 | - name: Install Dependencies 24 | run: npm install 25 | 26 | - name: Run tests 27 | run: npm test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | haters 2 | 3 | lib-cov 4 | *.seed 5 | *.log 6 | *.csv 7 | *.dat 8 | *.out 9 | *.pid 10 | *.gz 11 | .github-todos 12 | 13 | pids 14 | results 15 | 16 | dist 17 | node_modules 18 | npm-debug.log 19 | package-lock.json 20 | 21 | # code coverage folder 22 | coverage 23 | .nyc_output 24 | 25 | # Secrets 26 | .env 27 | .env.** 28 | 29 | # IDEs and editors 30 | .idea 31 | .vscode 32 | 33 | .vagrant 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | ## [1.2.0](https://github.com/supercharge/request-ip/compare/v1.1.2...v1.2.0) - 2022-03-07 5 | 6 | ### Updated 7 | - bump dependencies 8 | - prefer the IP address from `request.socket` over `request.connection` 9 | - `request.connection` is deprecated since Node.js v13.0.0 10 | - move tests from @hapi/lab and @hapi/code to jest 11 | 12 | 13 | ## [1.1.2](https://github.com/supercharge/request-ip/compare/v1.1.1...v1.1.2) - 2020-08-21 14 | 15 | ### Updated 16 | - bump dependencies 17 | - change `main` entrypoint in `package.json` to `dist` folder 18 | 19 | ### Removed 20 | - remove `index.js` file which acted as a middleman to export from `dist` folder 21 | 22 | 23 | ## [1.1.1](https://github.com/supercharge/request-ip/compare/v1.1.0...v1.1.1) - 2020-08-11 24 | 25 | ### Fixed 26 | - changed package exports to explicit, named exports. This addresses issue with bundlers like rollup 27 | 28 | 29 | ## [1.1.0](https://github.com/supercharge/request-ip/compare/v1.0.0...v1.1.0) - 2020-08-11 30 | 31 | ### Added 32 | - default export to make the exports seamlessly work in ES modules 33 | 34 | 35 | ## 1.0.0 - 2020-08-04 36 | 37 | ### Added 38 | - `1.0.0` release 🚀 🎉 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) The Supercharge Node.js Framework 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 |
7 |

8 |

Request IP

9 |

10 |

11 | Retrieve a request’s IP address 12 |

13 |
14 |

15 | Installation · 16 | Usage · 17 | Detecting the IP Address 18 |

19 |
20 |
21 |

22 | Latest Version 23 | Monthly downloads 24 |

25 |

26 | Follow @marcuspoehls and @superchargejs for updates! 27 |

28 |
29 | 30 | --- 31 | 32 | ## Introduction 33 | The `@supercharge/request-ip` package provides a function to retrieve a request’s IP address. 34 | 35 | 36 | ## Installation 37 | 38 | ``` 39 | npm i @supercharge/request-ip 40 | ``` 41 | 42 | 43 | ## Usage 44 | 45 | ```js 46 | const RequestIp = require('@supercharge/request-ip') 47 | 48 | const ip = RequestIp.getClientIp(request) 49 | 50 | // for example '213.211.254.97' as an IP v4 address 51 | // or '2001:0db8:85a3:0000:0000:8a2e:0370:7334' as an IP v6 address 52 | // or 'undefined' if no IP address is available on the given request 53 | ``` 54 | 55 | Depending on your used web framework, you may use it in a middleware or route handler: 56 | 57 | 58 | **simple Express example** 59 | 60 | ```js 61 | const { getClientIp } = require('@supercharge/request-ip') 62 | 63 | const expressMiddleware = function (req, res, next) { 64 | req.ip = getClientIp(req) 65 | 66 | next() 67 | } 68 | ``` 69 | 70 | **simple hapi route handler example:** 71 | 72 | ```js 73 | const Hapi = require('@hapi/hapi') 74 | const { getClientIp } = require('@supercharge/request-ip') 75 | 76 | const server = new Hapi.Server({ 77 | host: 'localhost' 78 | }) 79 | 80 | server.route({ 81 | method: 'GET', 82 | path: '/login', 83 | handler: (request, h) => { 84 | const ip = getClientIp(request) 85 | 86 | return h.response(ip) 87 | } 88 | }) 89 | ``` 90 | 91 | 92 | ## Detecting the IP Address 93 | The client’s IP address may be stored in different locations of the request instance varying between services. 94 | 95 | Here’s the order of locations in which the packages searches for the requesting IP address: 96 | 97 | 1. Checks HTTP request headers 98 | 1. `x-forwarded-for`: this header may contain multiple IP address for (client/proxies/hops). This package extracts and returns the first IP address. 99 | 2. `x-forwarded`, `forwarded`, `forwarded-for` as variants from `x-forwarded-for` possibly configured by proxies 100 | 3. `x-client-ip` possibly configured by nginx 101 | 4. `x-real-ip` possibly configured in nginx 102 | 5. `cf-connecting-ip` from Cloudflare 103 | 6. `fastly-client-ip` from Fastly and Firebase 104 | 7. `true-client-ip` from Akamai and Cloudflare 105 | 8. `x-cluster-client-ip` from Rackspace 106 | 2. Checks the HTTP connection in `request.connection` and `request.connection.socket` 107 | 3. Checks the HTTP socket in `request.socket` 108 | 4. Checks the HTTP info in `request.info` 109 | 5. Checks the raw HTTP request instance in `request.raw` 110 | 6. Checks the request context used by AWS API Gateway/Lambda in `request.requestContext` 111 | 112 | 113 | ## Credits 114 | A huge thank you to [Petar Bojinov](https://github.com/pbojinov) for his [request-ip](https://github.com/pbojinov/request-ip) package. I was using Petar’s package for two years in my [hapi-rate-limitor](https://github.com/futurestudio/hapi-rate-limitor) plugin. It seems Petar is busy with other work and I felt the need to create my own package providing the functionality to retrieve a request’s IP address. 115 | 116 | 117 | ## Contributing 118 | Do you miss a way to find the request’s IP? We very much appreciate your contribution! Please send in a pull request 😊 119 | 120 | 1. Create a fork 121 | 2. Create your feature branch: `git checkout -b my-feature` 122 | 3. Commit your changes: `git commit -am 'Add some feature'` 123 | 4. Push to the branch: `git push origin my-new-feature` 124 | 5. Submit a pull request 🚀 125 | 126 | 127 | ## License 128 | MIT © [Supercharge](https://superchargejs.com) 129 | 130 | --- 131 | 132 | > [superchargejs.com](https://superchargejs.com)  ·  133 | > GitHub [@supercharge](https://github.com/supercharge/)  ·  134 | > Twitter [@superchargejs](https://twitter.com/superchargejs) 135 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | collectCoverage: true, 5 | testEnvironment: 'node', 6 | coverageReporters: ['text', 'html'], 7 | testMatch: ['**/test/**/*.[jt]s?(x)'] 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supercharge/request-ip", 3 | "description": "Retrieve a request’s IP address in Node.js", 4 | "version": "1.2.0", 5 | "author": "Marcus Pöhls ", 6 | "bugs": { 7 | "url": "https://github.com/supercharge/request-ip/issues" 8 | }, 9 | "devDependencies": { 10 | "@supercharge/tsconfig": "~2.0.0", 11 | "@types/jest": "~27.4.1", 12 | "@typescript-eslint/eslint-plugin": "~4.29.2", 13 | "eslint": "~7.32.0", 14 | "eslint-config-standard-with-typescript": "~21.0.1", 15 | "eslint-plugin-import": "~2.24.1", 16 | "eslint-plugin-node": "~11.1.0", 17 | "eslint-plugin-promise": "~5.1.0", 18 | "jest": "~27.5.1", 19 | "typescript": "~4.4.4" 20 | }, 21 | "files": [ 22 | "dist" 23 | ], 24 | "homepage": "https://github.com/supercharge/request-ip", 25 | "keywords": [ 26 | "nodejs", 27 | "request", 28 | "ip", 29 | "request-ip", 30 | "supercharge", 31 | "superchargejs" 32 | ], 33 | "license": "MIT", 34 | "main": "dist", 35 | "publishConfig": { 36 | "access": "public" 37 | }, 38 | "repository": { 39 | "type": "git", 40 | "url": "git+https://github.com/supercharge/request-ip.git" 41 | }, 42 | "scripts": { 43 | "build": "tsc", 44 | "lint": "eslint src --ext .js,.ts", 45 | "lint:fix": "npm run lint --fix --fix", 46 | "test": "npm run build && npm run lint && npm run test:run", 47 | "test:run": "jest" 48 | }, 49 | "types": "dist" 50 | } 51 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { Request } from './request' 4 | 5 | /** 6 | * Retrieve the client IP address for the given `request`. 7 | * 8 | * @param {Object} request 9 | * 10 | * @returns {String} 11 | */ 12 | export function getClientIp (request: any): string | undefined { 13 | return new Request(request).getClientIp() 14 | } 15 | -------------------------------------------------------------------------------- /src/interacts-with-headers.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | export class InteractsWithHeaders { 4 | /** 5 | * The wrapped request instance. 6 | */ 7 | private readonly _headers: Map 8 | 9 | /** 10 | * Create a new instance for the given `request`. 11 | * 12 | * @param {Object} headers 13 | */ 14 | constructor (headers: any) { 15 | this._headers = this.asMap(headers || {}) 16 | } 17 | 18 | /** 19 | * Returns the request headers. 20 | * 21 | * @param {Object} requestHeaders 22 | * 23 | * @returns {Object} 24 | */ 25 | private asMap (requestHeaders: any): Map { 26 | const headers = new Map() 27 | 28 | Object.keys(requestHeaders).forEach(name => { 29 | const value = requestHeaders[name] 30 | 31 | headers.set(this.lower(name), value ? this.lower(value) : value) 32 | }) 33 | 34 | return headers 35 | } 36 | 37 | /** 38 | * Returns the lowercased string of `str`. 39 | * 40 | * @param {String} str 41 | * 42 | * @returns {String} 43 | */ 44 | lower (str: string): string { 45 | return String(str).toLowerCase() 46 | } 47 | 48 | /** 49 | * Returns the request headers. 50 | * 51 | * @returns {Map} 52 | */ 53 | headers (): Map { 54 | return this._headers 55 | } 56 | 57 | /** 58 | * Determine whether the request comes with headers. 59 | * 60 | * @returns {Boolean} 61 | */ 62 | hasHeaders (): boolean { 63 | return this.headers().size > 0 64 | } 65 | 66 | /** 67 | * Returns a request header if available, undefined otherwise. 68 | * 69 | * @param {String} name - the header name 70 | * 71 | * @returns {String} 72 | */ 73 | header (name: string): string | undefined { 74 | return this.headers().get(name) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/request.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import Net from 'net' 4 | import { InteractsWithHeaders } from './interacts-with-headers' 5 | 6 | export class Request extends InteractsWithHeaders { 7 | /** 8 | * The wrapped request instance. 9 | */ 10 | private readonly request: any 11 | 12 | /** 13 | * Create a new instance for the given `request`. 14 | * 15 | * @param {Object} request 16 | */ 17 | constructor (request: any) { 18 | request = request || {} 19 | 20 | super(request.headers) 21 | 22 | this.request = request 23 | } 24 | 25 | /** 26 | * Returns the client IP address. 27 | * 28 | * @returns {String|undefined} 29 | */ 30 | getClientIp (): string | undefined { 31 | return this.fromHeaders() ?? 32 | this.fromSocket() ?? 33 | this.fromConnection() ?? 34 | this.fromInfo() ?? 35 | this.fromRaw() ?? 36 | this.fromRequestContext() 37 | } 38 | 39 | /** 40 | * Returns the IP address if available in the HTTP request headers. 41 | * 42 | * @returns {String|undefined} 43 | */ 44 | fromHeaders (): string | undefined { 45 | if (this.hasHeaders()) { 46 | // nginx (if configured), load balancers (AWS ELB), and other proxies 47 | if (this.hasIpInForwardedFor()) { 48 | return this.getFromForwardedFor() 49 | } 50 | 51 | // Heroku, AWS EC2, nginx (if configured), and others 52 | if (this.hasIpInHeader('x-client-ip')) { 53 | return this.ipInHeader('x-client-ip') 54 | } 55 | 56 | // used by some proxies, like nginx 57 | if (this.hasIpInHeader('x-real-ip')) { 58 | return this.header('x-real-ip') 59 | } 60 | 61 | // Cloudflare 62 | if (this.hasIpInHeader('cf-connecting-ip')) { 63 | return this.header('cf-connecting-ip') 64 | } 65 | 66 | // Fastly and Firebase 67 | if (this.hasIpInHeader('fastly-client-ip')) { 68 | return this.header('fastly-client-ip') 69 | } 70 | 71 | // Akamai, Cloudflare 72 | if (this.hasIpInHeader('true-client-ip')) { 73 | return this.header('true-client-ip') 74 | } 75 | 76 | // Rackspace 77 | if (this.hasIpInHeader('x-cluster-client-ip')) { 78 | return this.header('x-cluster-client-ip') 79 | } 80 | } 81 | } 82 | 83 | /** 84 | * Determine whether a valid IP address is available in the “x-forwarded-for” HTTP header. 85 | * 86 | * @returns {Boolean} 87 | */ 88 | hasIpInForwardedFor (): boolean { 89 | return this.isIp( 90 | this.getFromForwardedFor() 91 | ) 92 | } 93 | 94 | /** 95 | * Returns the IP address if available from the “x-forwarded-for” HTTP header. 96 | * 97 | * @returns {String|undefined} 98 | */ 99 | getFromForwardedFor (): string | undefined { 100 | if (this.hasIpInHeader('x-forwarded-for')) { 101 | return this.ipInHeader('x-forwarded-for') 102 | } 103 | 104 | if (this.hasIpInHeader('x-forwarded')) { 105 | return this.ipInHeader('x-forwarded') 106 | } 107 | 108 | if (this.hasIpInHeader('forwarded-for')) { 109 | return this.ipInHeader('forwarded-for') 110 | } 111 | 112 | if (this.hasIpInHeader('forwarded')) { 113 | return this.ipInHeader('forwarded') 114 | } 115 | } 116 | 117 | /** 118 | * Determine whether the request IP comes from the given header `name`. 119 | * 120 | * @param {String} name - the header name 121 | * 122 | * @returns {Boolean} 123 | */ 124 | hasIpInHeader (name: string): boolean { 125 | return !!this.ipInHeader(name) 126 | } 127 | 128 | /** 129 | * Returns the first IP address from the `name`d header. 130 | * 131 | * @param {String} name 132 | * 133 | * @returns {String|undefined} 134 | */ 135 | ipInHeader (name: string): string | undefined { 136 | return this.findIp( 137 | this.header(name)?.split(',') 138 | ) 139 | } 140 | 141 | /** 142 | * Returns the first valid IP address from the list of IP address candidates. 143 | * 144 | * @param {Array} ips 145 | * 146 | * @returns {String|undefined} 147 | */ 148 | findIp (ips: string[] = []): string | undefined { 149 | return ips 150 | .map(ip => ip.trim()) 151 | .map(ip => this.removePortFrom(ip)) 152 | .find(ip => this.isIp(ip)) 153 | } 154 | 155 | /** 156 | * Returns the plain IP v4 address without the port number. 157 | * 158 | * @param {String} ip 159 | * 160 | * @returns {String} 161 | */ 162 | removePortFrom (ip: string): string { 163 | if (this.isIpv6(ip)) { 164 | return ip 165 | } 166 | 167 | return ip.includes(':') 168 | ? ip.split(':')[0] 169 | : ip 170 | } 171 | 172 | /** 173 | * Returns the IP address if available in the request connection. 174 | * 175 | * @returns {String|undefined} 176 | */ 177 | fromConnection (): string | undefined { 178 | if (!this.hasConnection()) { 179 | return 180 | } 181 | 182 | if (this.isIp(this.request.connection.remoteAddress)) { 183 | return this.request.connection.remoteAddress 184 | } 185 | 186 | if (!this.request.connection.socket) { 187 | return 188 | } 189 | 190 | if (this.isIp(this.request.connection.socket.remoteAddress)) { 191 | return this.request.connection.socket.remoteAddress 192 | } 193 | } 194 | 195 | /** 196 | * Determine whether the request has a `connection` object assigned. 197 | * 198 | * @returns {Boolean} 199 | */ 200 | hasConnection (): boolean { 201 | return !!this.request.connection 202 | } 203 | 204 | /** 205 | * Returns the IP address if available in the request socket. 206 | * 207 | * @returns {String|undefined} 208 | */ 209 | fromSocket (): string | undefined { 210 | if (!this.hasSocket()) { 211 | return 212 | } 213 | 214 | if (this.isIp(this.request.socket.remoteAddress)) { 215 | return this.request.socket.remoteAddress 216 | } 217 | } 218 | 219 | /** 220 | * Determine whether the request has a `socket` object assigned. 221 | * 222 | * @returns {Boolean} 223 | */ 224 | hasSocket (): boolean { 225 | return !!this.request.socket 226 | } 227 | 228 | /** 229 | * Returns the IP address if available in the request info object. 230 | * 231 | * @returns {String|undefined} 232 | */ 233 | fromInfo (): string | undefined { 234 | if (!this.hasInfo()) { 235 | return 236 | } 237 | 238 | if (this.isIp(this.request.info.remoteAddress)) { 239 | return this.request.info.remoteAddress 240 | } 241 | } 242 | 243 | /** 244 | * Determine whether the request has an `info` object assigned. 245 | * 246 | * @returns {Boolean} 247 | */ 248 | hasInfo (): boolean { 249 | return !!this.request.info 250 | } 251 | 252 | /** 253 | * Returns the IP address if available from the raw request object. The 254 | * `raw` request object is typically available in web frameworks like 255 | * Fastify or hapi providing the original Node.js request instance. 256 | * 257 | * @returns {String|undefined} 258 | */ 259 | fromRaw (): string | undefined { 260 | if (this.hasRaw()) { 261 | return new Request(this.request.raw).getClientIp() 262 | } 263 | } 264 | 265 | /** 266 | * Determine whether the request has a `requestContext` object assigned. 267 | * 268 | * @returns {Boolean} 269 | */ 270 | hasRaw (): boolean { 271 | return !!this.raw() 272 | } 273 | 274 | /** 275 | * Returns the raw request object. 276 | * 277 | * @returns {Object} 278 | */ 279 | raw (): object | undefined { 280 | return this.request.raw 281 | } 282 | 283 | /** 284 | * Returns the IP address if available in the request context. The request 285 | * context is typically available in serverless functions, like AWS Lambda. 286 | * 287 | * @returns {String|undefined} 288 | */ 289 | fromRequestContext (): string | undefined { 290 | // AWS API Gateway/Lambda 291 | if (!this.hasRequestContext()) { 292 | return 293 | } 294 | 295 | if (!this.requestContext().identity) { 296 | return 297 | } 298 | 299 | if (this.isIp(this.requestContext().identity.sourceIp)) { 300 | return this.requestContext().identity.sourceIp 301 | } 302 | } 303 | 304 | /** 305 | * Determine whether the request has a `requestContext` object assigned. 306 | * 307 | * @returns {Boolean} 308 | */ 309 | hasRequestContext (): boolean { 310 | return !!this.requestContext() 311 | } 312 | 313 | /** 314 | * Returns the request context. 315 | * 316 | * @returns {*} 317 | */ 318 | requestContext (): any { 319 | return this.request.requestContext 320 | } 321 | 322 | /** 323 | * Determine whether it’s a valid `ip` address. 324 | * 325 | * @param {String} ip 326 | * 327 | * @returns {Boolean} 328 | */ 329 | isIp (ip?: string): boolean { 330 | return this.isIpv4(ip) || this.isIpv6(ip) 331 | } 332 | 333 | /** 334 | * Determine whether the given `ip` address is a valid IP v4 address. 335 | * 336 | * @param {String} ip 337 | * 338 | * @returns {Boolean} 339 | */ 340 | isIpv4 (ip?: string): boolean { 341 | return Net.isIP(ip ?? '') === 4 342 | } 343 | 344 | /** 345 | * Determine whether the given `ip` address is a valid IP v4 address. 346 | * 347 | * @param {String} ip 348 | * 349 | * @returns {Boolean} 350 | */ 351 | isIpv6 (ip?: string): boolean { 352 | return Net.isIP(ip ?? '') === 6 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const RequestIp = require('../dist') 4 | const { getClientIp } = RequestIp 5 | 6 | describe('Request IP: ', () => { 7 | it('exports a function', async () => { 8 | expect(getClientIp).toBeInstanceOf(Function) 9 | }) 10 | 11 | it('request headers is undefined', async () => { 12 | expect(RequestIp.getClientIp()).toBeUndefined() 13 | expect(RequestIp.getClientIp(null)).toBeUndefined() 14 | 15 | expect(RequestIp.getClientIp({})).toBeUndefined() 16 | expect(RequestIp.getClientIp(123)).toBeUndefined() 17 | expect(RequestIp.getClientIp('request')).toBeUndefined() 18 | }) 19 | 20 | it('handles null values', async () => { 21 | expect( 22 | RequestIp.getClientIp({ info: { remoteAddress: undefined } }) 23 | ).toBeUndefined() 24 | 25 | expect( 26 | RequestIp.getClientIp({ info: { remoteAddress: null } }) 27 | ).toBeUndefined() 28 | 29 | expect( 30 | RequestIp.getClientIp({ headers: { remoteAddress: null } }) 31 | ).toBeUndefined() 32 | }) 33 | 34 | it('x-client-ip', async () => { 35 | expect( 36 | RequestIp.getClientIp({ headers: { 'x-client-ip': '8.8.8.8' } }) 37 | ).toEqual('8.8.8.8') 38 | 39 | expect( 40 | RequestIp.getClientIp({ headers: { 'x-client-ip': 'not-an-ip' } }) 41 | ).toBeUndefined() 42 | }) 43 | 44 | it('fastly-client-ip', async () => { 45 | expect( 46 | RequestIp.getClientIp({ headers: { 'fastly-client-ip': '8.8.8.8' } }) 47 | ).toEqual('8.8.8.8') 48 | }) 49 | 50 | it('cf-connecting-ip', () => { 51 | expect( 52 | RequestIp.getClientIp({ headers: { 'cf-connecting-ip': '8.8.8.8' } }) 53 | ).toEqual('8.8.8.8') 54 | }) 55 | 56 | it('true-client-ip', () => { 57 | expect( 58 | RequestIp.getClientIp({ headers: { 'true-client-ip': '8.8.8.8' } }) 59 | ).toEqual('8.8.8.8') 60 | }) 61 | 62 | it('x-real-ip', () => { 63 | expect( 64 | RequestIp.getClientIp({ headers: { 'x-real-ip': '8.8.8.8' } }) 65 | ).toEqual('8.8.8.8') 66 | }) 67 | 68 | it('x-cluster-client-ip', () => { 69 | expect( 70 | RequestIp.getClientIp({ headers: { 'x-cluster-client-ip': '8.8.8.8' } }) 71 | ).toEqual('8.8.8.8') 72 | }) 73 | 74 | it('x-forwarded-for', () => { 75 | expect( 76 | RequestIp.getClientIp({ headers: { 'x-forwarded-for': null } }) 77 | ).toBeUndefined() 78 | expect( 79 | RequestIp.getClientIp({ headers: { 'x-forwarded-for': undefined } }) 80 | ).toBeUndefined() 81 | 82 | expect( 83 | RequestIp.getClientIp({ headers: { 'x-forwarded-for': '8.8.8.8' } }) 84 | ).toEqual('8.8.8.8') 85 | 86 | expect( 87 | RequestIp.getClientIp({ headers: { 'x-forwarded-for': '8.8.8.8, 4.4.4.4' } }) 88 | ).toEqual('8.8.8.8') 89 | 90 | expect( 91 | RequestIp.getClientIp({ headers: { 'x-forwarded-for': '8.8.8.8, 4.4.4.4, 1.1.1.1' } }) 92 | ).toEqual('8.8.8.8') 93 | }) 94 | 95 | it('x-forwarded-for with masked (unknown) IPs', () => { 96 | expect( 97 | RequestIp.getClientIp({ headers: { 'x-forwarded-for': 'unknown, [redacted], 8.8.8.8, 4.4.4.4' } }) 98 | ).toEqual('8.8.8.8') 99 | }) 100 | 101 | it('x-forwarded-for with port', () => { 102 | expect( 103 | RequestIp.getClientIp({ headers: { 'x-forwarded-for': 'unknown, 8.8.8.8:443, 4.4.4.4:443' } }) 104 | ).toEqual('8.8.8.8') 105 | }) 106 | 107 | it('forwarded-for', () => { 108 | expect( 109 | RequestIp.getClientIp({ headers: { 'forwarded-for': '8.8.8.8' } }) 110 | ).toEqual('8.8.8.8') 111 | 112 | expect( 113 | RequestIp.getClientIp({ headers: { 'forwarded-for': '8.8.8.8, 4.4.4.4' } }) 114 | ).toEqual('8.8.8.8') 115 | 116 | expect( 117 | RequestIp.getClientIp({ headers: { 'forwarded-for': 'unknown, unknown, 8.8.8.8, 4.4.4.4' } }) 118 | ).toEqual('8.8.8.8') 119 | }) 120 | 121 | it('x-forwarded', () => { 122 | expect( 123 | RequestIp.getClientIp({ headers: { 'x-forwarded': '8.8.8.8' } }) 124 | ).toEqual('8.8.8.8') 125 | 126 | expect( 127 | RequestIp.getClientIp({ headers: { 'x-forwarded': '8.8.8.8, 4.4.4.4' } }) 128 | ).toEqual('8.8.8.8') 129 | 130 | expect( 131 | RequestIp.getClientIp({ headers: { 'x-forwarded': 'unknown, unknown, 8.8.8.8, 4.4.4.4' } }) 132 | ).toEqual('8.8.8.8') 133 | }) 134 | 135 | it('forwarded', () => { 136 | expect( 137 | RequestIp.getClientIp({ headers: { forwarded: '8.8.8.8' } }) 138 | ).toEqual('8.8.8.8') 139 | 140 | expect( 141 | RequestIp.getClientIp({ headers: { forwarded: '8.8.8.8, 4.4.4.4' } }) 142 | ).toEqual('8.8.8.8') 143 | 144 | expect( 145 | RequestIp.getClientIp({ headers: { forwarded: 'unknown, unknown, 8.8.8.8, 4.4.4.4' } }) 146 | ).toEqual('8.8.8.8') 147 | }) 148 | 149 | it('request.connection', () => { 150 | expect( 151 | RequestIp.getClientIp({ connection: { remoteAddress: '8.8.8.8' } })).toEqual('8.8.8.8') 152 | expect( 153 | RequestIp.getClientIp({ connection: { } }) 154 | ).toBeUndefined() 155 | 156 | expect( 157 | RequestIp.getClientIp({ connection: { remoteAddress: 'not-an-ip-address' } })).toBeUndefined() 158 | }) 159 | 160 | it('request.connection.socket', () => { 161 | expect( 162 | RequestIp.getClientIp({ connection: { socket: { remoteAddress: '8.8.8.8' } } })).toEqual('8.8.8.8') 163 | expect( 164 | RequestIp.getClientIp({ connection: { socket: { } } })).toBeUndefined() 165 | expect( 166 | RequestIp.getClientIp({ connection: { socket: { remoteAddress: 'invalid-ip' } } })).toBeUndefined() 167 | }) 168 | 169 | it('request.socket', () => { 170 | expect( 171 | RequestIp.getClientIp({ socket: { remoteAddress: '8.8.8.8' } })).toEqual('8.8.8.8') 172 | expect( 173 | RequestIp.getClientIp({ socket: { remoteAddress: 'invalid-ip' } }) 174 | ).toBeUndefined() 175 | 176 | expect( 177 | RequestIp.getClientIp({ socket: { } }) 178 | ).toBeUndefined() 179 | }) 180 | 181 | it('request.info', () => { 182 | expect( 183 | RequestIp.getClientIp({ info: { remoteAddress: '8.8.8.8' } })).toEqual('8.8.8.8') 184 | expect( 185 | RequestIp.getClientIp({ info: { remoteAddress: 'invalid-ip' } }) 186 | ).toBeUndefined() 187 | 188 | expect( 189 | RequestIp.getClientIp({ info: { } }) 190 | ).toBeUndefined() 191 | }) 192 | 193 | it('request.requestContext', () => { 194 | expect( 195 | RequestIp.getClientIp({ requestContext: { identity: { sourceIp: '8.8.8.8' } } }) 196 | ).toEqual('8.8.8.8') 197 | 198 | expect( 199 | RequestIp.getClientIp({ requestContext: { identity: { sourceIp: 'invalid-ip' } } }) 200 | ).toBeUndefined() 201 | 202 | expect( 203 | RequestIp.getClientIp({ requestContext: { } }) 204 | ).toBeUndefined() 205 | 206 | expect( 207 | RequestIp.getClientIp({ requestContext: { identity: {} } }) 208 | ).toBeUndefined() 209 | }) 210 | 211 | it('request.raw', () => { 212 | expect( 213 | RequestIp.getClientIp({ raw: { info: { remoteAddress: '8.8.8.8' } } }) 214 | ).toEqual('8.8.8.8') 215 | 216 | expect( 217 | RequestIp.getClientIp({ raw: { info: { remoteAddress: 'invalid-ip' } } }) 218 | ).toBeUndefined() 219 | 220 | expect( 221 | RequestIp.getClientIp({ raw: { info: {} } }) 222 | ).toBeUndefined() 223 | 224 | expect( 225 | RequestIp.getClientIp({ raw: { } }) 226 | ).toBeUndefined() 227 | }) 228 | 229 | it('supports IPv6 addresses', () => { 230 | expect( 231 | RequestIp.getClientIp({ 232 | connection: { remoteAddress: '2001:0db8:85a3:0000:0000:8a2e:0370:7334' } 233 | }) 234 | ).toEqual('2001:0db8:85a3:0000:0000:8a2e:0370:7334') 235 | 236 | expect( 237 | RequestIp.getClientIp({ 238 | headers: { 'x-forwarded-for': '2001:0db8:85a3:0000:0000:8a2e:0370:7334' } 239 | }) 240 | ).toEqual('2001:0db8:85a3:0000:0000:8a2e:0370:7334') 241 | }) 242 | 243 | it('supports shortened IPv6 addresses', () => { 244 | expect( 245 | RequestIp.getClientIp({ connection: { remoteAddress: '2001:db8::2:1' } }) 246 | ).toEqual('2001:db8::2:1') 247 | }) 248 | }) 249 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@supercharge/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": [ 7 | "./**/*" 8 | ], 9 | "exclude": [ 10 | "./node_modules", 11 | "./test/**/*", 12 | "./dist" 13 | ] 14 | } 15 | --------------------------------------------------------------------------------