@k03mad/ip2geo 5.0.1 β 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.husky/pre-commit +2 -4
- package/README.md +21 -12
- package/app/cli.js +15 -11
- package/app/helpers/cache.js +151 -0
- package/app/lib/ip2geo.js +56 -150
- package/package.json +4 -4
- package/tests/cache-file-modify.js +1 -1
- package/tests/cache-file-subfolder.js +1 -1
- package/tests/cache-map-entries.js +2 -2
- package/tests/cache-map-max-entries.js +2 -2
- package/tests/cache-map-off.js +1 -1
- package/tests/default.js +1 -1
- package/tests/helpers/consts.js +27 -25
- package/tests/ip-multi-requests-cache.js +1 -1
- package/tests/ip-no-data.js +1 -1
- package/tests/ip-v6.js +1 -1
- package/tests/shared/fs.js +11 -6
package/.husky/pre-commit
CHANGED
package/README.md
CHANGED
|
@@ -15,18 +15,22 @@ ip2geo
|
|
|
15
15
|
ip2geo 1.1.1.1
|
|
16
16
|
# {
|
|
17
17
|
# ip: '1.1.1.1',
|
|
18
|
-
#
|
|
18
|
+
# continent: 'North America',
|
|
19
|
+
# continentCode: 'NA',
|
|
19
20
|
# country: 'United States',
|
|
20
|
-
#
|
|
21
|
+
# countryCode: 'US',
|
|
22
|
+
# countryEmoji: 'πΊπΈ',
|
|
21
23
|
# region: 'District of Columbia',
|
|
24
|
+
# regionCode: 'DC',
|
|
22
25
|
# city: 'Washington',
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
+
# connectionAsn: 13335,
|
|
27
|
+
# connectionOrg: 'APNIC and Cloudflare DNS Resolver project',
|
|
28
|
+
# connectionIsp: 'Cloudflare, Inc.',
|
|
29
|
+
# connectionDomain: 'cloudflare.com'
|
|
26
30
|
# }
|
|
27
31
|
|
|
28
32
|
ip2geo 1.1.1.1 8.8.8.8 --json
|
|
29
|
-
# [{"ip":"1.1.1.1","
|
|
33
|
+
# [{"ip":"1.1.1.1","continent":"North America","continentCode":"NA","country":"United States","countryCode":"US","countryEmoji":"πΊπΈ","region":"District of Columbia","regionCode":"DC","city":"Washington","connectionAsn":13335,"connectionOrg":"APNIC and Cloudflare DNS Resolver project","connectionIsp":"Cloudflare, Inc.","connectionDomain":"cloudflare.com"},{"ip":"8.8.8.8","continent":"North America","continentCode":"NA","country":"United States","countryCode":"US","countryEmoji":"πΊπΈ","region":"California","regionCode":"CA","city":"Mountain View","connectionAsn":15169,"connectionOrg":"Google LLC","connectionIsp":"Google LLC","connectionDomain":"google.com"}]
|
|
30
34
|
```
|
|
31
35
|
|
|
32
36
|
## API
|
|
@@ -38,7 +42,8 @@ npm i @k03mad/ip2geo
|
|
|
38
42
|
```js
|
|
39
43
|
import {ip2geo} from '@k03mad/ip2geo';
|
|
40
44
|
|
|
41
|
-
const info = await ip2geo(
|
|
45
|
+
const info = await ip2geo({
|
|
46
|
+
ip: '1.1.1.1', // make key falsy to use current external IP
|
|
42
47
|
// defaults
|
|
43
48
|
cacheDir: path.join(os.tmpdir(), '.ip2geo'),
|
|
44
49
|
cacheFileName: 'ips.log',
|
|
@@ -51,13 +56,17 @@ const info = await ip2geo('1.1.1.1', {
|
|
|
51
56
|
|
|
52
57
|
// info {
|
|
53
58
|
// ip: '1.1.1.1',
|
|
54
|
-
//
|
|
59
|
+
// continent: 'North America',
|
|
60
|
+
// continentCode: 'NA',
|
|
55
61
|
// country: 'United States',
|
|
56
|
-
//
|
|
62
|
+
// countryCode: 'US',
|
|
63
|
+
// countryEmoji: 'πΊπΈ',
|
|
57
64
|
// region: 'District of Columbia',
|
|
65
|
+
// regionCode: 'DC',
|
|
58
66
|
// city: 'Washington',
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
67
|
+
// connectionAsn: 13335,
|
|
68
|
+
// connectionOrg: 'APNIC and Cloudflare DNS Resolver project',
|
|
69
|
+
// connectionIsp: 'Cloudflare, Inc.',
|
|
70
|
+
// connectionDomain: 'cloudflare.com'
|
|
62
71
|
// }
|
|
63
72
|
```
|
package/app/cli.js
CHANGED
|
@@ -10,26 +10,30 @@ const args = process.argv.slice(2);
|
|
|
10
10
|
const argsIps = args.filter(arg => !arg.startsWith('-'));
|
|
11
11
|
|
|
12
12
|
if (args.includes('-h') || args.includes('--help')) {
|
|
13
|
-
const
|
|
14
|
-
const name = nameText('ip2geo');
|
|
15
|
-
const json = codeText('// json output for parse');
|
|
16
|
-
const multi = codeText('// multi IPs');
|
|
13
|
+
const cmd = `${codeText('$')} ${nameText('ip2geo')}`;
|
|
17
14
|
|
|
18
15
|
log([
|
|
19
16
|
'',
|
|
20
|
-
`${
|
|
21
|
-
`${
|
|
22
|
-
|
|
23
|
-
`${
|
|
24
|
-
`${
|
|
25
|
-
|
|
17
|
+
`${cmd} ${codeText('# current external ip')}`,
|
|
18
|
+
`${cmd} 1.1.1.1`,
|
|
19
|
+
'',
|
|
20
|
+
`${cmd} -h`,
|
|
21
|
+
`${cmd} --help`,
|
|
22
|
+
'',
|
|
23
|
+
`${cmd} 1.1.1.1 -j`,
|
|
24
|
+
`${cmd} 1.1.1.1 --json`,
|
|
25
|
+
'',
|
|
26
|
+
`${cmd} 1.1.1.1 8.8.8.8`,
|
|
27
|
+
`${cmd} 1.1.1.1,8.8.8.8`,
|
|
26
28
|
'',
|
|
27
29
|
]);
|
|
30
|
+
|
|
31
|
+
process.exit(0);
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
const output = argsIps.length === 0
|
|
31
35
|
? [await ip2geo()]
|
|
32
|
-
: await Promise.all(argsIps.map(arg => Promise.all(arg.split(',').map(ip => ip2geo(ip)))));
|
|
36
|
+
: await Promise.all(argsIps.map(arg => Promise.all(arg.split(',').map(ip => ip2geo({ip})))));
|
|
33
37
|
|
|
34
38
|
const flatten = output.flat();
|
|
35
39
|
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import _debug from 'debug';
|
|
5
|
+
|
|
6
|
+
const debug = _debug('mad:geoip');
|
|
7
|
+
|
|
8
|
+
const outputKeys = [
|
|
9
|
+
'ip',
|
|
10
|
+
'continent',
|
|
11
|
+
'continentCode',
|
|
12
|
+
'country',
|
|
13
|
+
'countryCode',
|
|
14
|
+
'countryEmoji',
|
|
15
|
+
'region',
|
|
16
|
+
'regionCode',
|
|
17
|
+
'city',
|
|
18
|
+
'connectionAsn',
|
|
19
|
+
'connectionOrg',
|
|
20
|
+
'connectionIsp',
|
|
21
|
+
'connectionDomain',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {Array<string>} dataArr
|
|
26
|
+
* @returns {object}
|
|
27
|
+
*/
|
|
28
|
+
export const collectOutputData = dataArr => {
|
|
29
|
+
const outputData = {};
|
|
30
|
+
|
|
31
|
+
outputKeys.forEach((key, i) => {
|
|
32
|
+
outputData[key] = dataArr[i];
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return outputData;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} ip
|
|
40
|
+
* @param {string} cacheDir
|
|
41
|
+
* @param {string} cacheFileName
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
const getCacheFileFullPath = (ip, cacheDir, cacheFileName) => {
|
|
45
|
+
const [firstOctet] = ip.split(/\.|:/);
|
|
46
|
+
return path.join(cacheDir, `${firstOctet}_${cacheFileName}`);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {string} ip
|
|
51
|
+
* @param {string} cacheDir
|
|
52
|
+
* @param {string} cacheFileName
|
|
53
|
+
* @param {string} cacheFileSeparator
|
|
54
|
+
* @param {string} cacheFileNewline
|
|
55
|
+
* @returns {Promise<object>}
|
|
56
|
+
*/
|
|
57
|
+
export const readFromFsCache = async (ip, cacheDir, cacheFileName, cacheFileSeparator, cacheFileNewline) => {
|
|
58
|
+
const cacheFileFull = getCacheFileFullPath(ip, cacheDir, cacheFileName);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
await fs.mkdir(cacheDir, {recursive: true});
|
|
62
|
+
const content = await fs.readFile(cacheFileFull, {encoding: 'utf8'});
|
|
63
|
+
|
|
64
|
+
if (content) {
|
|
65
|
+
const data = content.split(cacheFileNewline);
|
|
66
|
+
|
|
67
|
+
for (const elem of data) {
|
|
68
|
+
const fileData = elem.split(cacheFileSeparator);
|
|
69
|
+
|
|
70
|
+
if (ip === fileData[0]) {
|
|
71
|
+
const outputData = collectOutputData(fileData);
|
|
72
|
+
debug('get from fs cache: %o %o', cacheFileFull, outputData);
|
|
73
|
+
return outputData;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (err.code === 'ENOENT') {
|
|
79
|
+
await fs.appendFile(cacheFileFull, '');
|
|
80
|
+
} else {
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {string} ip
|
|
88
|
+
* @param {object} data
|
|
89
|
+
* @param {string} cacheDir
|
|
90
|
+
* @param {string} cacheFileName
|
|
91
|
+
* @param {string} cacheFileSeparator
|
|
92
|
+
* @param {string} cacheFileNewline
|
|
93
|
+
* @returns {Promise<void>}
|
|
94
|
+
*/
|
|
95
|
+
export const writeToFsCache = async (ip, data, cacheDir, cacheFileName, cacheFileSeparator, cacheFileNewline) => {
|
|
96
|
+
const cacheFileFull = getCacheFileFullPath(ip, cacheDir, cacheFileName);
|
|
97
|
+
|
|
98
|
+
await fs.mkdir(cacheDir, {recursive: true});
|
|
99
|
+
await fs.appendFile(cacheFileFull, Object.values(data).join(cacheFileSeparator) + cacheFileNewline);
|
|
100
|
+
|
|
101
|
+
debug('set to fs cache: %o %o', cacheFileFull, data);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {string} ip
|
|
106
|
+
* @param {Map} cacheMap
|
|
107
|
+
* @param {number} cacheMapMaxEntries
|
|
108
|
+
* @returns {object|undefined}
|
|
109
|
+
*/
|
|
110
|
+
export const readFromMapCache = (ip, cacheMap, cacheMapMaxEntries) => {
|
|
111
|
+
if (cacheMapMaxEntries > 0) {
|
|
112
|
+
const mapCache = cacheMap.get(ip);
|
|
113
|
+
|
|
114
|
+
if (mapCache) {
|
|
115
|
+
debug('get from map cache: %o', mapCache);
|
|
116
|
+
return mapCache;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param {object} body
|
|
123
|
+
* @param {string} body.ip
|
|
124
|
+
* @param {Map} cacheMap
|
|
125
|
+
* @param {number} cacheMapMaxEntries
|
|
126
|
+
*/
|
|
127
|
+
export const writeToMapCache = (body, cacheMap, cacheMapMaxEntries) => {
|
|
128
|
+
if (cacheMapMaxEntries > 0) {
|
|
129
|
+
cacheMap.set(body.ip, body);
|
|
130
|
+
debug('set to map cache: %o', body);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @param {Map} cacheMap
|
|
136
|
+
* @param {number} cacheMapMaxEntries
|
|
137
|
+
*/
|
|
138
|
+
export const removeFromMapCacheIfLimit = (cacheMap, cacheMapMaxEntries) => {
|
|
139
|
+
if (cacheMap.size > cacheMapMaxEntries) {
|
|
140
|
+
debug('remove from map cache by limit: size %o, limit: %o', cacheMap.size, cacheMapMaxEntries);
|
|
141
|
+
|
|
142
|
+
for (const [key] of cacheMap) {
|
|
143
|
+
debug('remove from map cache by limit: key %o', key);
|
|
144
|
+
cacheMap.delete(key);
|
|
145
|
+
|
|
146
|
+
if (cacheMap.size <= cacheMapMaxEntries) {
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
package/app/lib/ip2geo.js
CHANGED
|
@@ -1,14 +1,44 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
1
|
import os from 'node:os';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
|
|
5
4
|
import {request} from '@k03mad/request';
|
|
6
|
-
import
|
|
5
|
+
import {logErrorExit} from '@k03mad/simple-log';
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
collectOutputData,
|
|
9
|
+
readFromFsCache,
|
|
10
|
+
readFromMapCache,
|
|
11
|
+
removeFromMapCacheIfLimit,
|
|
12
|
+
writeToFsCache,
|
|
13
|
+
writeToMapCache,
|
|
14
|
+
} from '../helpers/cache.js';
|
|
15
|
+
|
|
16
|
+
const API = 'https://ipwho.is/';
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_CACHE_FILE_DIR = path.join(os.tmpdir(), '.ip2geo-cache');
|
|
19
|
+
export const DEFAULT_CACHE_FILE_NAME = 'ip.log';
|
|
20
|
+
export const DEFAULT_CACHE_FILE_SEPARATOR = ';;';
|
|
21
|
+
export const DEFAULT_CACHE_FILE_NEWLINE = '\n';
|
|
22
|
+
export const DEFAULT_CACHE_MAP_MAX_ENTRIES = Number.POSITIVE_INFINITY;
|
|
23
|
+
export const DEFAULT_RPS = 3;
|
|
24
|
+
|
|
25
|
+
export const cacheStorage = new Map();
|
|
9
26
|
|
|
10
27
|
/**
|
|
11
|
-
* @typedef {object}
|
|
28
|
+
* @typedef {object} ReqInput
|
|
29
|
+
* @property {object} [opts]
|
|
30
|
+
* @property {object} [opts.ip]
|
|
31
|
+
* @property {string} [opts.cacheDir]
|
|
32
|
+
* @property {string} [opts.cacheFileName]
|
|
33
|
+
* @property {string} [opts.cacheFileSeparator]
|
|
34
|
+
* @property {string} [opts.cacheFileNewline]
|
|
35
|
+
* @property {Map} [opts.cacheMap]
|
|
36
|
+
* @property {number} [opts.cacheMapMaxEntries]
|
|
37
|
+
* @property {number} [opts.rps]
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {object} ReqOutput
|
|
12
42
|
* @property {string} [ip]
|
|
13
43
|
* @property {string} [emoji]
|
|
14
44
|
* @property {string} [country]
|
|
@@ -20,104 +50,12 @@ const debug = _debug('mad:geoip');
|
|
|
20
50
|
* @property {string} [ispDomain]
|
|
21
51
|
*/
|
|
22
52
|
|
|
23
|
-
const API = 'https://ipwho.is/';
|
|
24
|
-
|
|
25
|
-
const DEFAULT_CACHE_FILE_DIR = path.join(os.tmpdir(), '.ip2geo');
|
|
26
|
-
const DEFAULT_CACHE_FILE_NAME = 'ips.log';
|
|
27
|
-
const DEFAULT_CACHE_FILE_SEPARATOR = ';;';
|
|
28
|
-
const DEFAULT_CACHE_FILE_NEWLINE = '\n';
|
|
29
|
-
const DEFAULT_CACHE_MAP_MAX_ENTRIES = Number.POSITIVE_INFINITY;
|
|
30
|
-
const DEFAULT_RPS = 3;
|
|
31
|
-
|
|
32
|
-
export const cacheStorage = new Map();
|
|
33
|
-
|
|
34
|
-
const outputKeys = [
|
|
35
|
-
'ip',
|
|
36
|
-
'emoji',
|
|
37
|
-
'country',
|
|
38
|
-
'countryA2',
|
|
39
|
-
'region',
|
|
40
|
-
'city',
|
|
41
|
-
'org',
|
|
42
|
-
'isp',
|
|
43
|
-
'ispDomain',
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* @param {Array<string>} dataArr
|
|
48
|
-
* @returns {GeoIpOutput}
|
|
49
|
-
*/
|
|
50
|
-
const collectOutputData = dataArr => {
|
|
51
|
-
const outputData = {};
|
|
52
|
-
|
|
53
|
-
outputKeys.forEach((key, i) => {
|
|
54
|
-
outputData[key] = dataArr[i];
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
return outputData;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* @param {string} ip
|
|
62
|
-
* @param {string} cacheDir
|
|
63
|
-
* @param {string} cacheFileName
|
|
64
|
-
* @returns {string}
|
|
65
|
-
*/
|
|
66
|
-
const getCacheFileFullPath = (ip, cacheDir, cacheFileName) => {
|
|
67
|
-
const [firstOctet] = ip.split(/\.|:/);
|
|
68
|
-
return path.join(cacheDir, `${firstOctet}_${cacheFileName}`);
|
|
69
|
-
};
|
|
70
|
-
|
|
71
53
|
/**
|
|
72
|
-
* @param {
|
|
73
|
-
* @
|
|
74
|
-
* @param {string} cacheFileName
|
|
75
|
-
* @returns {Promise<string>}
|
|
54
|
+
* @param {ReqInput} opts
|
|
55
|
+
* @returns {Promise<ReqOutput>}
|
|
76
56
|
*/
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
await fs.mkdir(cacheDir, {recursive: true});
|
|
80
|
-
return await fs.readFile(getCacheFileFullPath(ip, cacheDir, cacheFileName), {encoding: 'utf8'});
|
|
81
|
-
} catch (err) {
|
|
82
|
-
if (err.code === 'ENOENT') {
|
|
83
|
-
await fs.appendFile(getCacheFileFullPath(ip, cacheDir, cacheFileName), '');
|
|
84
|
-
} else {
|
|
85
|
-
throw err;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* @param {string} ip
|
|
92
|
-
* @param {Array[string]} data
|
|
93
|
-
* @param {string} cacheDir
|
|
94
|
-
* @param {string} cacheFileName
|
|
95
|
-
* @param {string} cacheFileSeparator
|
|
96
|
-
* @param {string} cacheFileNewline
|
|
97
|
-
* @returns {Promise<void>}
|
|
98
|
-
*/
|
|
99
|
-
const writeToFsCache = async (ip, data, cacheDir, cacheFileName, cacheFileSeparator, cacheFileNewline) => {
|
|
100
|
-
await fs.mkdir(cacheDir, {recursive: true});
|
|
101
|
-
|
|
102
|
-
await fs.appendFile(
|
|
103
|
-
getCacheFileFullPath(ip, cacheDir, cacheFileName),
|
|
104
|
-
data.join(cacheFileSeparator) + cacheFileNewline,
|
|
105
|
-
);
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* @param {string} [ip]
|
|
110
|
-
* @param {object} [opts]
|
|
111
|
-
* @param {string} [opts.cacheDir]
|
|
112
|
-
* @param {string} [opts.cacheFileName]
|
|
113
|
-
* @param {string} [opts.cacheFileSeparator]
|
|
114
|
-
* @param {string} [opts.cacheFileNewline]
|
|
115
|
-
* @param {Map} [opts.cacheMap]
|
|
116
|
-
* @param {number} [opts.cacheMapMaxEntries]
|
|
117
|
-
* @param {number} [opts.rps]
|
|
118
|
-
* @returns {Promise<GeoIpOutput>}
|
|
119
|
-
*/
|
|
120
|
-
export const ip2geo = async (ip = '', {
|
|
57
|
+
export const ip2geo = async ({
|
|
58
|
+
ip = '',
|
|
121
59
|
cacheDir = DEFAULT_CACHE_FILE_DIR,
|
|
122
60
|
cacheFileName = DEFAULT_CACHE_FILE_NAME,
|
|
123
61
|
cacheFileSeparator = DEFAULT_CACHE_FILE_SEPARATOR,
|
|
@@ -127,82 +65,50 @@ export const ip2geo = async (ip = '', {
|
|
|
127
65
|
rps = DEFAULT_RPS,
|
|
128
66
|
} = {}) => {
|
|
129
67
|
if (ip) {
|
|
130
|
-
|
|
131
|
-
const ipData = cacheMap.get(ip);
|
|
68
|
+
const mapCache = readFromMapCache(ip, cacheMap, cacheMapMaxEntries);
|
|
132
69
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return ipData;
|
|
136
|
-
}
|
|
70
|
+
if (mapCache) {
|
|
71
|
+
return mapCache;
|
|
137
72
|
}
|
|
138
73
|
|
|
139
74
|
const fsCache = await readFromFsCache(ip, cacheDir, cacheFileName);
|
|
140
75
|
|
|
141
76
|
if (fsCache) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
for (const elem of data) {
|
|
145
|
-
const fileData = elem.split(cacheFileSeparator);
|
|
146
|
-
|
|
147
|
-
if (ip === fileData[0]) {
|
|
148
|
-
const outputData = collectOutputData(fileData);
|
|
149
|
-
debug('get from fs cache: %o', outputData);
|
|
150
|
-
|
|
151
|
-
if (cacheMapMaxEntries > 0) {
|
|
152
|
-
cacheMap.set(ip, outputData);
|
|
153
|
-
debug('set to map cache: %o', outputData);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return outputData;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
77
|
+
writeToMapCache(fsCache, cacheMap, cacheMapMaxEntries);
|
|
78
|
+
return fsCache;
|
|
159
79
|
}
|
|
160
80
|
}
|
|
161
81
|
|
|
162
|
-
const
|
|
82
|
+
const reqUrl = API + ip;
|
|
83
|
+
const {body} = await request(reqUrl, {}, {rps});
|
|
163
84
|
|
|
164
85
|
if (!body?.ip) {
|
|
165
|
-
|
|
86
|
+
logErrorExit(['API error', `request: ${reqUrl}`, `response body: ${body}`]);
|
|
166
87
|
}
|
|
167
88
|
|
|
168
|
-
const
|
|
89
|
+
const outputData = collectOutputData([
|
|
169
90
|
body.ip,
|
|
170
|
-
body?.
|
|
91
|
+
body?.continent,
|
|
92
|
+
body?.continent_code,
|
|
171
93
|
body?.country,
|
|
172
94
|
body?.country_code,
|
|
95
|
+
body?.flag?.emoji,
|
|
173
96
|
body?.region,
|
|
97
|
+
body?.region_code,
|
|
174
98
|
body?.city,
|
|
99
|
+
body?.connection?.asn,
|
|
175
100
|
body?.connection?.org,
|
|
176
101
|
body?.connection?.isp,
|
|
177
102
|
body?.connection?.domain,
|
|
178
|
-
];
|
|
179
|
-
|
|
180
|
-
const outputData = collectOutputData(usedData);
|
|
103
|
+
]);
|
|
181
104
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
debug('set to map cache: %o', outputData);
|
|
185
|
-
}
|
|
105
|
+
writeToMapCache(outputData, cacheMap, cacheMapMaxEntries);
|
|
106
|
+
removeFromMapCacheIfLimit(cacheMap, cacheMapMaxEntries);
|
|
186
107
|
|
|
187
108
|
await writeToFsCache(
|
|
188
|
-
body.ip,
|
|
109
|
+
body.ip, outputData,
|
|
189
110
|
cacheDir, cacheFileName, cacheFileSeparator, cacheFileNewline,
|
|
190
111
|
);
|
|
191
112
|
|
|
192
|
-
debug('set to fs cache: %o', outputData);
|
|
193
|
-
|
|
194
|
-
if (cacheMap.size > cacheMapMaxEntries) {
|
|
195
|
-
debug('remove from map cache by limit: size %o, limit: %o', cacheMap.size, cacheMapMaxEntries);
|
|
196
|
-
|
|
197
|
-
for (const [key] of cacheMap) {
|
|
198
|
-
debug('remove from map cache by limit: key %o', key);
|
|
199
|
-
cacheMap.delete(key);
|
|
200
|
-
|
|
201
|
-
if (cacheMap.size <= cacheMapMaxEntries) {
|
|
202
|
-
break;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
113
|
return outputData;
|
|
208
114
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k03mad/ip2geo",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "GeoIP library",
|
|
5
5
|
"maintainers": [
|
|
6
6
|
"Kirill Molchanov <k03.mad@gmail.com"
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"debug": "4.3.4"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@k03mad/eslint-config": "
|
|
28
|
+
"@k03mad/eslint-config": "20.2.0",
|
|
29
29
|
"eslint": "8.56.0",
|
|
30
|
-
"husky": "
|
|
30
|
+
"husky": "9.0.5",
|
|
31
31
|
"mocha": "10.2.0"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
@@ -38,6 +38,6 @@
|
|
|
38
38
|
"clean:modules": "rm -rf ./node_modules || true",
|
|
39
39
|
"clean:eslint:cache": "rm -rf .eslintcache || true",
|
|
40
40
|
"setup": "npm run clean && pnpm i",
|
|
41
|
-
"prepare": "husky
|
|
41
|
+
"prepare": "husky || true"
|
|
42
42
|
}
|
|
43
43
|
}
|
|
@@ -22,7 +22,7 @@ describe(testName, () => {
|
|
|
22
22
|
it('should remove fs cache dir if exist', () => removeCacheFolder(opts.cacheDir));
|
|
23
23
|
|
|
24
24
|
it(`should return correct response for IP: "${REQUEST_IPV4.ip}"`, async () => {
|
|
25
|
-
const data = await ip2geo(REQUEST_IPV4.ip, opts);
|
|
25
|
+
const data = await ip2geo({ip: REQUEST_IPV4.ip, ...opts});
|
|
26
26
|
assert.deepEqual(data, REQUEST_IPV4);
|
|
27
27
|
});
|
|
28
28
|
|
|
@@ -26,7 +26,7 @@ describe(testName, () => {
|
|
|
26
26
|
it('should remove fs cache dir if exist', () => removeCacheFolder(opts.cacheDir));
|
|
27
27
|
|
|
28
28
|
it(`should return correct response for IP: "${REQUEST_IPV4.ip}"`, async () => {
|
|
29
|
-
const data = await ip2geo(REQUEST_IPV4.ip, opts);
|
|
29
|
+
const data = await ip2geo({ip: REQUEST_IPV4.ip, ...opts});
|
|
30
30
|
assert.deepEqual(data, REQUEST_IPV4);
|
|
31
31
|
});
|
|
32
32
|
|
|
@@ -19,7 +19,7 @@ describe(testName, () => {
|
|
|
19
19
|
it('should remove fs cache dir if exist', () => removeCacheFolder(opts.cacheDir));
|
|
20
20
|
|
|
21
21
|
it(`should return correct response for IP: "${REQUEST_IPV4.ip}"`, async () => {
|
|
22
|
-
const data = await ip2geo(REQUEST_IPV4.ip, opts);
|
|
22
|
+
const data = await ip2geo({ip: REQUEST_IPV4.ip, ...opts});
|
|
23
23
|
assert.deepEqual(data, REQUEST_IPV4);
|
|
24
24
|
});
|
|
25
25
|
|
|
@@ -29,7 +29,7 @@ describe(testName, () => {
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
it(`should return correct response for IP: "${REQUEST_IPV6.ip}"`, async () => {
|
|
32
|
-
const data = await ip2geo(REQUEST_IPV6.ip, opts);
|
|
32
|
+
const data = await ip2geo({ip: REQUEST_IPV6.ip, ...opts});
|
|
33
33
|
assert.deepEqual(data, REQUEST_IPV6);
|
|
34
34
|
});
|
|
35
35
|
|
|
@@ -30,7 +30,7 @@ describe(testName, () => {
|
|
|
30
30
|
|
|
31
31
|
firstReqIps.forEach(ip => {
|
|
32
32
|
it(`should request geo for IP: "${ip}"`, async () => {
|
|
33
|
-
await ip2geo(ip, opts);
|
|
33
|
+
await ip2geo({ip, ...opts});
|
|
34
34
|
});
|
|
35
35
|
});
|
|
36
36
|
|
|
@@ -41,7 +41,7 @@ describe(testName, () => {
|
|
|
41
41
|
|
|
42
42
|
secondReqIps.forEach(ip => {
|
|
43
43
|
it(`should request geo for IP: "${ip}"`, async () => {
|
|
44
|
-
await ip2geo(ip, opts);
|
|
44
|
+
await ip2geo({ip, ...opts});
|
|
45
45
|
});
|
|
46
46
|
});
|
|
47
47
|
|
package/tests/cache-map-off.js
CHANGED
|
@@ -19,7 +19,7 @@ describe(testName, () => {
|
|
|
19
19
|
it('should remove fs cache dir if exist', () => removeCacheFolder(opts.cacheDir));
|
|
20
20
|
|
|
21
21
|
it(`should return correct response for IP: "${REQUEST_IPV4_MAP_OFF_ONLY.ip}"`, async () => {
|
|
22
|
-
const data = await ip2geo(REQUEST_IPV4_MAP_OFF_ONLY.ip, opts);
|
|
22
|
+
const data = await ip2geo({ip: REQUEST_IPV4_MAP_OFF_ONLY.ip, ...opts});
|
|
23
23
|
assert.deepEqual(data, REQUEST_IPV4_MAP_OFF_ONLY);
|
|
24
24
|
});
|
|
25
25
|
|
package/tests/default.js
CHANGED
|
@@ -15,7 +15,7 @@ describe(testName, () => {
|
|
|
15
15
|
|
|
16
16
|
describe('with ip arg', () => {
|
|
17
17
|
it(`should return correct response for IP: "${REQUEST_IPV4.ip}"`, async () => {
|
|
18
|
-
const data = await ip2geo(REQUEST_IPV4.ip);
|
|
18
|
+
const data = await ip2geo({ip: REQUEST_IPV4.ip});
|
|
19
19
|
assert.deepEqual(data, REQUEST_IPV4);
|
|
20
20
|
});
|
|
21
21
|
|
package/tests/helpers/consts.js
CHANGED
|
@@ -1,45 +1,47 @@
|
|
|
1
|
-
import os from 'node:os';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
export const DEFAULT_OPTS = {
|
|
5
|
-
cacheDir: path.join(os.tmpdir(), '.ip2geo'),
|
|
6
|
-
cacheFileName: 'ips.log',
|
|
7
|
-
cacheFileSeparator: ';;',
|
|
8
|
-
cacheFileNewline: '\n',
|
|
9
|
-
};
|
|
10
|
-
|
|
11
1
|
export const REQUEST_IPV4 = {
|
|
12
2
|
ip: '8.8.8.8',
|
|
13
|
-
|
|
3
|
+
continent: 'North America',
|
|
4
|
+
continentCode: 'NA',
|
|
14
5
|
country: 'United States',
|
|
15
|
-
|
|
6
|
+
countryCode: 'US',
|
|
7
|
+
countryEmoji: 'πΊπΈ',
|
|
16
8
|
region: 'California',
|
|
9
|
+
regionCode: 'CA',
|
|
17
10
|
city: 'Mountain View',
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
connectionAsn: 15_169,
|
|
12
|
+
connectionOrg: 'Google LLC',
|
|
13
|
+
connectionIsp: 'Google LLC',
|
|
14
|
+
connectionDomain: 'google.com',
|
|
21
15
|
};
|
|
22
16
|
|
|
23
17
|
export const REQUEST_IPV4_MAP_OFF_ONLY = {
|
|
24
18
|
ip: '1.1.1.1',
|
|
25
|
-
|
|
19
|
+
continent: 'North America',
|
|
20
|
+
continentCode: 'NA',
|
|
26
21
|
country: 'United States',
|
|
27
|
-
|
|
22
|
+
countryCode: 'US',
|
|
23
|
+
countryEmoji: 'πΊπΈ',
|
|
28
24
|
region: 'District of Columbia',
|
|
25
|
+
regionCode: 'DC',
|
|
29
26
|
city: 'Washington',
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
connectionAsn: 13_335,
|
|
28
|
+
connectionOrg: 'APNIC and Cloudflare DNS Resolver project',
|
|
29
|
+
connectionIsp: 'Cloudflare, Inc.',
|
|
30
|
+
connectionDomain: 'cloudflare.com',
|
|
33
31
|
};
|
|
34
32
|
|
|
35
33
|
export const REQUEST_IPV6 = {
|
|
36
34
|
ip: '2a00:dd80:40:100::',
|
|
37
|
-
|
|
35
|
+
continent: 'Europe',
|
|
36
|
+
continentCode: 'EU',
|
|
38
37
|
country: 'Netherlands',
|
|
39
|
-
|
|
38
|
+
countryCode: 'NL',
|
|
39
|
+
countryEmoji: 'π³π±',
|
|
40
40
|
region: 'North Holland',
|
|
41
|
+
regionCode: 'NH',
|
|
41
42
|
city: 'Amsterdam',
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
connectionAsn: 0,
|
|
44
|
+
connectionOrg: '',
|
|
45
|
+
connectionIsp: 'NetActuate Inc',
|
|
46
|
+
connectionDomain: '',
|
|
45
47
|
};
|
|
@@ -21,7 +21,7 @@ describe(testName, () => {
|
|
|
21
21
|
|
|
22
22
|
Array.from({length: opts.requestsCount}).forEach(() => {
|
|
23
23
|
it(`should return correct response for IP: "${REQUEST_IPV4.ip}"`, async () => {
|
|
24
|
-
const data = await ip2geo(REQUEST_IPV4.ip, opts);
|
|
24
|
+
const data = await ip2geo({ip: REQUEST_IPV4.ip, ...opts});
|
|
25
25
|
assert.deepEqual(data, REQUEST_IPV4);
|
|
26
26
|
});
|
|
27
27
|
});
|
package/tests/ip-no-data.js
CHANGED
|
@@ -31,7 +31,7 @@ describe(testName, () => {
|
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
it(`should return empty response for IP: "${response.ip}"`, async () => {
|
|
34
|
-
const data = await ip2geo(response.ip, opts);
|
|
34
|
+
const data = await ip2geo({ip: response.ip, ...opts});
|
|
35
35
|
assert.deepEqual(data, response);
|
|
36
36
|
});
|
|
37
37
|
});
|
package/tests/ip-v6.js
CHANGED
|
@@ -19,7 +19,7 @@ describe(testName, () => {
|
|
|
19
19
|
it('should remove fs cache dir if exist', () => removeCacheFolder(opts.cacheDir));
|
|
20
20
|
|
|
21
21
|
it(`should return correct response for IP: "${REQUEST_IPV6.ip}"`, async () => {
|
|
22
|
-
const data = await ip2geo(REQUEST_IPV6.ip, opts);
|
|
22
|
+
const data = await ip2geo({ip: REQUEST_IPV6.ip, ...opts});
|
|
23
23
|
assert.deepEqual(data, REQUEST_IPV6);
|
|
24
24
|
});
|
|
25
25
|
|
package/tests/shared/fs.js
CHANGED
|
@@ -2,12 +2,17 @@ import assert from 'node:assert/strict';
|
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_CACHE_FILE_DIR,
|
|
7
|
+
DEFAULT_CACHE_FILE_NAME,
|
|
8
|
+
DEFAULT_CACHE_FILE_NEWLINE,
|
|
9
|
+
DEFAULT_CACHE_FILE_SEPARATOR,
|
|
10
|
+
} from '../../app/index.js';
|
|
6
11
|
|
|
7
12
|
/**
|
|
8
13
|
* @param {string} cacheDir
|
|
9
14
|
*/
|
|
10
|
-
export const removeCacheFolder = async (cacheDir =
|
|
15
|
+
export const removeCacheFolder = async (cacheDir = DEFAULT_CACHE_FILE_DIR) => {
|
|
11
16
|
try {
|
|
12
17
|
await fs.rm(cacheDir, {recursive: true, force: true});
|
|
13
18
|
} catch (err) {
|
|
@@ -26,10 +31,10 @@ export const removeCacheFolder = async (cacheDir = DEFAULT_OPTS.cacheDir) => {
|
|
|
26
31
|
* @param {object} opts.response
|
|
27
32
|
*/
|
|
28
33
|
export const checkCacheFile = async ({
|
|
29
|
-
cacheDir =
|
|
30
|
-
cacheFileName =
|
|
31
|
-
cacheFileSeparator =
|
|
32
|
-
cacheFileNewline =
|
|
34
|
+
cacheDir = DEFAULT_CACHE_FILE_DIR,
|
|
35
|
+
cacheFileName = DEFAULT_CACHE_FILE_NAME,
|
|
36
|
+
cacheFileSeparator = DEFAULT_CACHE_FILE_SEPARATOR,
|
|
37
|
+
cacheFileNewline = DEFAULT_CACHE_FILE_NEWLINE,
|
|
33
38
|
response,
|
|
34
39
|
}) => {
|
|
35
40
|
const cacheFile = `${response.ip.split(/\.|:/)[0]}_${cacheFileName}`;
|