├── .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 |
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 |
--------------------------------------------------------------------------------