@fullerstack/nax-ipware
Advanced tools
Comparing version 0.0.2 to 0.0.3-dev-38256e136c
{ | ||
"version": "0.0.3-dev-38256e136c", | ||
"name": "@fullerstack/nax-ipware", | ||
"version": "0.0.2", | ||
"license": "MIT", | ||
"description": "A node library for server applications retrieving user's real IP address", | ||
@@ -24,4 +25,4 @@ "homepage": "https://github.com/neekware/fullerstack/blob/main/libs/nax-ipware/README.md", | ||
"dependencies": { | ||
"tslib": "^2.0.0", | ||
"ts-essentials": "^7.0.2" | ||
"ts-essentials": "^7.0.2", | ||
"tslib": "^2.0.0" | ||
}, | ||
@@ -31,3 +32,2 @@ "main": "./src/index.js", | ||
"author": "Val Neekman", | ||
"license": "MIT", | ||
"repository": { | ||
@@ -40,2 +40,2 @@ "type": "git", | ||
} | ||
} | ||
} |
@@ -35,3 +35,3 @@ # NAX IPware (A Node Application Agnostic Library) | ||
console.log(clientIp); | ||
// { ip: '177.139.100.100'', routable: true, trustedProxies: false } | ||
// { ip: '177.139.100.100'', isPublic: true, isRouteTrusted: false } | ||
// do something with the ip address (e.g. pass it within the request) | ||
@@ -42,3 +42,5 @@ // note: ip address doesn't change often, so better cache it for performance | ||
// Order of precedence is (Public, Private, Loopback, None) | ||
// `publicOnly` is `false` by default, if so, the order of precedence is (Public, Private, Loopback, None) | ||
// if `publicOnly` is set to `true`, then a public ip is returned or an empty IpwareIpInfo | ||
// `isRouteTrusted` is set to `true` if the proxy info matched the proxy configurations | ||
``` | ||
@@ -93,3 +95,3 @@ | ||
A default list that holds the private address prefixes is called `IPWARE_PRIVATE_IP_PREFIX`. | ||
This list is used to determine if an IP address is `public & routable` or `private`. | ||
This list is used to determine if an IP address is `public` or `private`. | ||
@@ -136,6 +138,5 @@ ```typescript | ||
// In the above scenario, use your load balancer IP address as a way to filter out unwanted requests. | ||
const ipInfo = ipware.getClientIpByTrustedProxies(request, { | ||
const ipInfo = ipware.getClientIP(request, { | ||
proxy: { | ||
enabled: true, | ||
proxyIpPrefixes: ['177.139.233.132'] | ||
proxyList: ['177.139.233.132'] | ||
}, | ||
@@ -145,6 +146,5 @@ }); | ||
// If you have multiple proxies, simply add them to the list | ||
const ipInfo = ipware.getClientIpByTrustedProxies(request, { | ||
const ipInfo = ipware.getClientIP(request, { | ||
proxy: { | ||
enabled: true, | ||
proxyIpPrefixes: ['177.139.233.100', '177.139.233.132'] | ||
proxyList: ['177.139.233.100', '177.139.233.132'] | ||
}, | ||
@@ -154,13 +154,11 @@ }); | ||
// For proxy servers with fixed sub-domain and dynamic IP, use the following pattern. | ||
const ipInfo = ipware.getClientIpByTrustedProxies(request, { | ||
const ipInfo = ipware.getClientIP(request, { | ||
proxy: { | ||
enabled: true, | ||
proxyIpPrefixes: ['177.139.', '177.140'] | ||
proxyList: ['177.139.', '177.140'] | ||
}, | ||
}); | ||
const ipInfo = ipware.getClientIpByTrustedProxies(request, { | ||
const ipInfo = ipware.getClientIP(request, { | ||
proxy: { | ||
enabled: true, | ||
proxyIpPrefixes: ['177.139.233.', '177.139.240'] | ||
proxyList: ['177.139.233.', '177.139.240'] | ||
}, | ||
@@ -170,9 +168,17 @@ }); | ||
// For proxy by ip address, count will be ignored | ||
const ipInfo = ipware.getClientIpByTrustedProxies(request, { | ||
const ipInfo = ipware.getClientIP(request, { | ||
proxy: { | ||
enabled: true, | ||
proxyIpPrefixes: ['177.139.', '177.140'], | ||
proxyList: ['177.139.', '177.140'], | ||
proxyCount: 2 // will be ignored | ||
}, | ||
}); | ||
// For strict mode, we either return the ip that matches the proxy info, or none | ||
const ipInfo = ipware.getClientIP(request, { | ||
proxy: { | ||
strict: true, | ||
proxyList: ['177.139.233.', '177.139.240'] | ||
}, | ||
}); | ||
``` | ||
@@ -198,6 +204,5 @@ | ||
// In the above scenario, the total number of proxies can be used as a way to filter out unwanted requests. | ||
const ipInfo = ipware.getClientIpByProxyCount(request, { | ||
const ipInfo = ipware.getClientIP(request, { | ||
proxy: { | ||
enabled: true, | ||
proxyCount: 1 | ||
count: 1 | ||
}, | ||
@@ -207,9 +212,17 @@ }); | ||
// For proxy by count, proxy prefixes will be ignored | ||
const ipInfo = ipware.getClientIpByProxyCount(request, { | ||
const ipInfo = ipware.getClientIP(request, { | ||
proxy: { | ||
enabled: true, | ||
proxyCount: 1, | ||
proxyIpPrefixes: ['177.139.233.'] // will be ignored | ||
count: 1 | ||
proxyList: ['177.139.233.'] // will be ignored | ||
}, | ||
}); | ||
// For strict mode, we either return the ip that matches the proxy info, or none | ||
const ipInfo = ipware.getClientIP(request, { | ||
proxy: { | ||
strict: true, | ||
count: 1 | ||
}, | ||
}); | ||
``` | ||
@@ -226,2 +239,12 @@ | ||
### Public IP Address ONLY (routable on the internet) | ||
```typescript | ||
// For publicOnly mode, we either return the first public IP address based on order or none | ||
const ipInfo = ipware.getClientIP(request, { | ||
publicOnly: true | ||
}); | ||
``` | ||
### Originating Request | ||
@@ -228,0 +251,0 @@ |
@@ -18,7 +18,2 @@ /** | ||
/** | ||
* Given two IP addresses, it returns the the best match ip | ||
* Best match order: precedence is (Public, Private, Loopback, null) | ||
*/ | ||
private bestMatched; | ||
/** | ||
* Determines if IP is loopback | ||
@@ -42,16 +37,2 @@ * @param {string} ip Ip address | ||
/** | ||
* Return the client IP address as per proxies count configuration | ||
* @param request HTTP request | ||
* @param options ipware call options | ||
* @returns IpwareIpInfo | ||
*/ | ||
getClientIpByProxyCount(request: any, callOptions?: IpwareCallOptions): IpwareIpInfo; | ||
/** | ||
* Return the client IP address as per proxies ip prefixes configuration | ||
* @param request HTTP request | ||
* @param options ipware call options | ||
* @returns IpwareIpInfo | ||
*/ | ||
getClientIpByTrustedProxies(request: any, callOptions?: IpwareCallOptions): IpwareIpInfo; | ||
/** | ||
* Return the client IP address as per best matched IP address | ||
@@ -58,0 +39,0 @@ * @param request HTTP request |
@@ -19,7 +19,1 @@ /** | ||
export declare const IpwareCallOptionsDefault: DeepReadonly<IpwareCallOptions>; | ||
export declare const IPWARE_ERROR_MESSAGE: { | ||
proxyDisabledOnProxyAwareApi: string; | ||
proxyEnabledWithoutProxyCount: string; | ||
proxyEnabledWithoutTrustedProxies: string; | ||
proxyEnabledOnNonProxyAwareApi: string; | ||
}; |
@@ -10,3 +10,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.IPWARE_ERROR_MESSAGE = exports.IpwareCallOptionsDefault = exports.IpwareConfigOptionsDefault = exports.IpwareProxyOptionsDefault = exports.IPWARE_DEFAULT_IP_INFO = exports.IPWARE_CLIENT_IP_ORDER_DEFAULT = exports.IPWARE_NON_PUBLIC_IP_PREFIX = exports.IPWARE_LOOPBACK_PREFIX = exports.IPWARE_PRIVATE_IP_PREFIX = exports.IPWARE_HEADERS_IP_ATTRIBUTES_ORDER = void 0; | ||
exports.IpwareCallOptionsDefault = exports.IpwareConfigOptionsDefault = exports.IpwareProxyOptionsDefault = exports.IPWARE_DEFAULT_IP_INFO = exports.IPWARE_CLIENT_IP_ORDER_DEFAULT = exports.IPWARE_NON_PUBLIC_IP_PREFIX = exports.IPWARE_LOOPBACK_PREFIX = exports.IPWARE_PRIVATE_IP_PREFIX = exports.IPWARE_HEADERS_IP_ATTRIBUTES_ORDER = void 0; | ||
// Search for the real IP address in the following order (user configurable) | ||
@@ -203,10 +203,10 @@ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For | ||
ip: '', | ||
routable: false, | ||
trustedRoute: false, | ||
isPublic: false, | ||
isRouteTrusted: false, | ||
}; | ||
exports.IpwareProxyOptionsDefault = { | ||
enabled: false, | ||
proxyIpPrefixes: [], | ||
proxyList: [], | ||
count: 0, | ||
order: exports.IPWARE_CLIENT_IP_ORDER_DEFAULT, | ||
strict: false, | ||
}; | ||
@@ -218,2 +218,3 @@ exports.IpwareConfigOptionsDefault = { | ||
proxy: exports.IpwareProxyOptionsDefault, | ||
publicOnly: false, | ||
}; | ||
@@ -223,9 +224,4 @@ exports.IpwareCallOptionsDefault = { | ||
proxy: exports.IpwareProxyOptionsDefault, | ||
publicOnly: false, | ||
}; | ||
exports.IPWARE_ERROR_MESSAGE = { | ||
proxyDisabledOnProxyAwareApi: 'Proxy check disabled, on calls to proxy-aware api', | ||
proxyEnabledWithoutProxyCount: 'Proxy check enabled, yet no proxy count not provided', | ||
proxyEnabledWithoutTrustedProxies: 'Proxy check enabled, yet no proxy prefixes provided', | ||
proxyEnabledOnNonProxyAwareApi: 'Proxy check enabled, yet wrong API called', | ||
}; | ||
//# sourceMappingURL=ipware.default.js.map |
@@ -17,4 +17,4 @@ "use strict"; | ||
constructor(options) { | ||
this.options = lodash_1.cloneDeep(ipware_default_1.IpwareConfigOptionsDefault); | ||
this.options = lodash_1.mergeWith(this.options, options, (dest, src) => Array.isArray(dest) ? src : undefined); | ||
this.options = ipware_default_1.IpwareConfigOptionsDefault; | ||
this.options = lodash_1.mergeWith(lodash_1.cloneDeep(this.options), options, (dest, src) => Array.isArray(dest) ? src : undefined); | ||
} | ||
@@ -27,4 +27,4 @@ /** | ||
if (ipware_util_1.isValidIP(cleanedIp)) { | ||
const routable = this.isPublic(cleanedIp); | ||
return { ip: cleanedIp, routable, trustedRoute: false }; | ||
const isPublic = this.isPublic(cleanedIp); | ||
return { ip: cleanedIp, isPublic, isRouteTrusted: false }; | ||
} | ||
@@ -34,18 +34,2 @@ return ipware_default_1.IPWARE_DEFAULT_IP_INFO; | ||
/** | ||
* Given two IP addresses, it returns the the best match ip | ||
* Best match order: precedence is (Public, Private, Loopback, null) | ||
*/ | ||
bestMatched(lastIP, nextIp) { | ||
if (!lastIP) { | ||
return nextIp; | ||
} | ||
if (this.isPublic(lastIP) && this.isPrivate(nextIp)) { | ||
return lastIP; | ||
} | ||
if (this.isPrivate(lastIP) && this.isLoopback(nextIp)) { | ||
return lastIP; | ||
} | ||
return nextIp; | ||
} | ||
/** | ||
* Determines if IP is loopback | ||
@@ -89,3 +73,3 @@ * @param {string} ip Ip address | ||
/** | ||
* Return the client IP address as per proxies count configuration | ||
* Return the client IP address as per best matched IP address | ||
* @param request HTTP request | ||
@@ -95,4 +79,6 @@ * @param options ipware call options | ||
*/ | ||
getClientIpByProxyCount(request, callOptions) { | ||
getClientIP(request, callOptions) { | ||
const options = lodash_1.mergeWith(lodash_1.cloneDeep(this.options), callOptions, (dest, src) => Array.isArray(dest) ? src : undefined); | ||
const privateIPList = []; | ||
const loopbackIPList = []; | ||
let ipInfo; | ||
@@ -103,110 +89,45 @@ for (const key of options.requestHeadersOrder) { | ||
// process the header attribute, we can have multiple ip addresses in the same attribute | ||
const ipData = ipware_util_1.getIPsFromString(ipString); | ||
// proxy options not configured, we can't continue | ||
if (!options.proxy.enabled) { | ||
throw new Error(ipware_default_1.IPWARE_ERROR_MESSAGE.proxyDisabledOnProxyAwareApi); | ||
const ipData = ipware_util_1.getIPsFromString(ipString, options.proxy.order); | ||
// we are expecting at least `1` ip address | ||
if (ipData.count < 1) { | ||
continue; | ||
} | ||
// proxy check enabled, but count is not configured properly, we can't continue | ||
if (options.proxy.count < 1) { | ||
throw new Error(ipware_default_1.IPWARE_ERROR_MESSAGE.proxyEnabledWithoutProxyCount); | ||
} | ||
// we are expecting requests via `x` number of proxies, but the IP counts don't match | ||
const clientIp = ipData.ips[0]; | ||
// we are expecting `x` number of ips as per `proxy.count` | ||
if (options.proxy.count > 0 && options.proxy.count !== ipData.count - 1) { | ||
continue; | ||
} | ||
// some configuration may be `custom` & reverse in direction (`proxy2, proxy1, client`) | ||
// the default configuration for most servers is `left-most` (`client, <proxy1, proxy2`) | ||
if (options.proxy.order !== ipware_default_1.IPWARE_CLIENT_IP_ORDER_DEFAULT) { | ||
ipData.ips = ipData.ips.reverse(); | ||
} | ||
// we matched the proxy information, however, the client IP still may be private | ||
// we let the caller to decide what to do with a private client IP | ||
ipInfo = this.getInfo(ipData.ips[0]); | ||
ipInfo.trustedRoute = true; | ||
return ipInfo; | ||
} | ||
} | ||
// we did not find any ip address based on the caller requirement | ||
return ipware_default_1.IPWARE_DEFAULT_IP_INFO; | ||
} | ||
/** | ||
* Return the client IP address as per proxies ip prefixes configuration | ||
* @param request HTTP request | ||
* @param options ipware call options | ||
* @returns IpwareIpInfo | ||
*/ | ||
getClientIpByTrustedProxies(request, callOptions) { | ||
const options = lodash_1.mergeWith(lodash_1.cloneDeep(this.options), callOptions, (dest, src) => Array.isArray(dest) ? src : undefined); | ||
let ipInfo; | ||
for (const key of options.requestHeadersOrder) { | ||
const ipString = ipware_util_1.getHeadersAttribute(request.headers, key); | ||
if (ipString) { | ||
// process the header attribute, we can have multiple ip addresses in the same attribute | ||
const ipData = ipware_util_1.getIPsFromString(ipString); | ||
// proxy options not configured, we can't continue | ||
if (!options.proxy.enabled) { | ||
throw new Error(ipware_default_1.IPWARE_ERROR_MESSAGE.proxyDisabledOnProxyAwareApi); | ||
} | ||
// proxy check enabled, but not configured properly, we can't continue | ||
if (options.proxy.proxyIpPrefixes.length < 1) { | ||
throw new Error(ipware_default_1.IPWARE_ERROR_MESSAGE.proxyEnabledWithoutTrustedProxies); | ||
} | ||
// we are expecting requests via specific trusted proxies, but specified proxies are more available IP addresses | ||
if (options.proxy.proxyIpPrefixes.length > ipData.count - 1) { | ||
// we are expecting at least `1` ip address as per `proxy.proxyList` | ||
if (options.proxy.proxyList.length > 0 && ipData.count < 2) { | ||
continue; | ||
} | ||
// some configuration may be `custom` & reverse in direction (`proxy2, proxy1, client`) | ||
// the default configuration for most servers is `left-most` (`client, <proxy1, proxy2`) | ||
if (options.proxy.order !== ipware_default_1.IPWARE_CLIENT_IP_ORDER_DEFAULT) { | ||
ipData.ips = ipData.ips.reverse(); | ||
} | ||
for (let idx = options.proxy.proxyIpPrefixes.length - 1; idx > 1; idx--) { | ||
// using startWith to allow for partial matches (e.g. `10.`, `10.0.`) | ||
// we match all proxy prefixes in the array, if so we can take the first ip as client IP | ||
if (!ipData.ips[idx].startsWith(options.proxy.proxyIpPrefixes[idx])) { | ||
return ipware_default_1.IPWARE_DEFAULT_IP_INFO; | ||
if (options.proxy.proxyList.length > 0) { | ||
for (const proxy of options.proxy.proxyList) { | ||
// the right most ip address is the most trusted proxy | ||
// ip spoofing is always possible if the hacker guess our proxy's ip address or subnet, and sends in a fake ip address | ||
// to prevent ip spoofing, ipware must be combined with ip filtering at firewall level | ||
// alternatively you can configure your proxy to send a customer header attribute that is hard to guess, but your server is aware of it | ||
if (ipData.ips[ipData.count - 1].startsWith(proxy)) { | ||
ipInfo = this.getInfo(clientIp); | ||
if (ipInfo.ip) { | ||
ipInfo.isRouteTrusted = true; | ||
// configuration is strictly looking for a public ip address only, or none at all, continue processing ... | ||
if (options.publicOnly && !ipInfo.isPublic) { | ||
continue; | ||
} | ||
return ipInfo; | ||
} | ||
} | ||
} | ||
} | ||
// we matched the proxy information, however, the client IP still may be private | ||
// we let the caller to decide what to do a private client IP | ||
ipInfo = this.getInfo(ipData.ips[0]); | ||
ipInfo.trustedRoute = true; | ||
return ipInfo; | ||
} | ||
} | ||
// we did not find any ip address based on the caller requirement | ||
return ipware_default_1.IPWARE_DEFAULT_IP_INFO; | ||
} | ||
/** | ||
* Return the client IP address as per best matched IP address | ||
* @param request HTTP request | ||
* @param options ipware call options | ||
* @returns IpwareIpInfo | ||
*/ | ||
getClientIP(request, callOptions) { | ||
const options = lodash_1.mergeWith(lodash_1.cloneDeep(this.options), callOptions, (dest, src) => Array.isArray(dest) ? src : undefined); | ||
let ipInfo; | ||
for (const key of options.requestHeadersOrder) { | ||
const ipString = ipware_util_1.getHeadersAttribute(request.headers, key); | ||
if (ipString) { | ||
// process the header attribute, we can have multiple ip addresses in the same attribute | ||
const ipData = ipware_util_1.getIPsFromString(ipString); | ||
// expecting at least one IP address, let's look for the next header | ||
if (ipData.count < 1) { | ||
continue; | ||
} | ||
// proxy check enabled, but not wrong api is called, better not continue for maximum security | ||
if (options.proxy.enabled) { | ||
throw new Error(ipware_default_1.IPWARE_ERROR_MESSAGE.proxyEnabledOnNonProxyAwareApi); | ||
} | ||
// handle custom ip order | ||
if (options.proxy.order !== ipware_default_1.IPWARE_CLIENT_IP_ORDER_DEFAULT && ipData.count > 1) { | ||
ipData.ips = ipData.ips.reverse(); | ||
} | ||
// we return the first public and routable IP address, based on headers precedence order | ||
for (const ip of ipData.ips) { | ||
ipInfo = this.getInfo(ip); | ||
if (ipInfo.ip && ipInfo.routable) { | ||
ipInfo.trustedRoute = true; | ||
return ipInfo; | ||
else { | ||
ipInfo = this.getInfo(clientIp); | ||
if (ipInfo.ip) { | ||
// configuration is strictly looking for a public ip address only, or none at all | ||
if (options.publicOnly && !ipInfo.isPublic) { | ||
this.isLoopback(ipInfo.ip) ? loopbackIPList.push(ipInfo) : privateIPList.push(ipInfo); | ||
} | ||
else { | ||
return ipInfo; | ||
} | ||
} | ||
@@ -216,8 +137,31 @@ } | ||
} | ||
// in strict mode, we either return an ip that comes through the matching proxy/count or none | ||
if (options.proxy.strict && (options.proxy.proxyList.length > 0 || options.proxy.count > 0)) { | ||
return ipware_default_1.IPWARE_DEFAULT_IP_INFO; | ||
} | ||
// no ip address from headers, let's fallback to the request itself | ||
const reqIp = ipware_util_1.getIpFromRequest(request); | ||
ipInfo = this.getInfo(reqIp); | ||
if (ipInfo.ip && ipInfo.routable) { | ||
return ipInfo; | ||
if (ipInfo.ip) { | ||
// configuration is strictly looking for a public ip address only, or none at all | ||
if (options.publicOnly && ipInfo.isPublic) { | ||
return ipInfo; | ||
} | ||
else { | ||
this.isLoopback(ipInfo.ip) ? loopbackIPList.push(ipInfo) : privateIPList.push(ipInfo); | ||
} | ||
} | ||
// no public ip address at this point, return empty ip info if configuration is publicOnly | ||
if (options.publicOnly) { | ||
return ipware_default_1.IPWARE_DEFAULT_IP_INFO; | ||
} | ||
// the best private ip address is the first one in the list | ||
if (privateIPList.length > 0) { | ||
return privateIPList[0]; | ||
} | ||
// the best loopback ip address is the first one in the list | ||
if (loopbackIPList.length > 0) { | ||
return loopbackIPList[0]; | ||
} | ||
// unable to find any ip, return empty and let the caller decide what to do | ||
return ipware_default_1.IPWARE_DEFAULT_IP_INFO; | ||
@@ -224,0 +168,0 @@ } |
@@ -13,4 +13,4 @@ /** | ||
ip: string; | ||
routable: boolean; | ||
trustedRoute?: boolean; | ||
isPublic: boolean; | ||
isRouteTrusted?: boolean; | ||
} | ||
@@ -23,6 +23,6 @@ export interface IpwareData { | ||
export interface IpwareProxyOptions { | ||
enabled: boolean; | ||
proxyIpPrefixes?: string[]; | ||
proxyList?: string[]; | ||
count?: number; | ||
order?: string; | ||
order?: IpwareClientIpOrder; | ||
strict?: boolean; | ||
} | ||
@@ -34,2 +34,3 @@ export interface IpwareConfigOptions { | ||
proxy?: IpwareProxyOptions; | ||
publicOnly?: boolean; | ||
} | ||
@@ -39,2 +40,3 @@ export interface IpwareCallOptions { | ||
proxy?: IpwareProxyOptions; | ||
publicOnly?: boolean; | ||
} |
@@ -8,3 +8,3 @@ /** | ||
*/ | ||
import { IpwareData, IpwareHeaders } from './ipware.model'; | ||
import { IpwareClientIpOrder, IpwareData, IpwareHeaders } from './ipware.model'; | ||
/** | ||
@@ -28,4 +28,6 @@ * Check the validity of an IPv4 address | ||
* Given a string, it returns a list of one or more valid IP addresses | ||
* @param str - string to be parsed | ||
* @param order - client ip order (default is `left-most`) | ||
*/ | ||
export declare function getIPsFromString(str: string): IpwareData; | ||
export declare function getIPsFromString(str: string, order?: IpwareClientIpOrder): IpwareData; | ||
/** | ||
@@ -32,0 +34,0 @@ * Returns HTTP request headers attribute by key |
@@ -47,9 +47,15 @@ "use strict"; | ||
* Given a string, it returns a list of one or more valid IP addresses | ||
* @param str - string to be parsed | ||
* @param order - client ip order (default is `left-most`) | ||
*/ | ||
function getIPsFromString(str) { | ||
function getIPsFromString(str, order = 'left-most') { | ||
const ipList = { ips: [], count: 0 }; | ||
for (const ip of str.toLowerCase().split(',').map(cleanUpIP).filter(isValidIP)) { | ||
ipList.ips.push(ip); | ||
for (const ip of str | ||
.toLowerCase() | ||
.split(',') | ||
.map(cleanUpIP) | ||
.filter((ip) => ip)) { | ||
order === 'left-most' ? ipList.ips.push(ip) : ipList.ips.unshift(ip); | ||
} | ||
ipList.count = ipList.ips.length || 0; | ||
ipList.count = ipList.ips.length; | ||
return ipList; | ||
@@ -56,0 +62,0 @@ } |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Unidentified License
License(Experimental) Something that seems like a license was found, but its contents could not be matched with a known license.
Found 2 instances in 1 package
Unidentified License
License(Experimental) Something that seems like a license was found, but its contents could not be matched with a known license.
Found 2 instances in 1 package
278
44705
694