@smpx/koa-request 1.0.4 → 1.2.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/GeoIP.js +190 -56
- package/package.json +4 -4
package/GeoIP.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const mmdb = require('mmdb-lib');
|
|
2
2
|
const https = require('https');
|
|
3
3
|
const zlib = require('zlib');
|
|
4
4
|
const path = require('path');
|
|
@@ -9,17 +9,89 @@ const fileName = 'GeoLite2-City';
|
|
|
9
9
|
const fileUrl = `https://raw.githubusercontent.com/GitSquared/node-geolite2-redist/master/redist/${fileName}.tar.gz`;
|
|
10
10
|
const fileDir = __dirname;
|
|
11
11
|
const filePath = `${fileDir}/${fileName}.mmdb`;
|
|
12
|
+
const ONE_DAY = 24 * 3600 * 1000;
|
|
13
|
+
// eslint-disable-next-line max-len
|
|
14
|
+
const processMs = ((process.env.pm_id || process.pid || Math.floor(Math.random() * 60)) % 60) * 60 * 1000;
|
|
12
15
|
let geoInitPromise;
|
|
16
|
+
let geoInitSynced;
|
|
13
17
|
let geoip;
|
|
18
|
+
let updateTimer;
|
|
14
19
|
|
|
15
|
-
|
|
20
|
+
class LRU {
|
|
21
|
+
constructor() {
|
|
22
|
+
this.maxItems = 10000;
|
|
23
|
+
this.cache = new Map();
|
|
24
|
+
this.oldCache = new Map();
|
|
25
|
+
this._size = 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_set(key, value) {
|
|
29
|
+
this.cache.set(key, value);
|
|
30
|
+
if (this.cache.size >= this.maxItems) {
|
|
31
|
+
this._size = this.cache.size;
|
|
32
|
+
this.oldCache = this.cache;
|
|
33
|
+
this.cache = new Map();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get(key) {
|
|
38
|
+
const value = this.cache.get(key);
|
|
39
|
+
if (value !== undefined) return value;
|
|
40
|
+
const oldValue = this.oldCache.get(key);
|
|
41
|
+
if (oldValue) {
|
|
42
|
+
this._set(key, oldValue);
|
|
43
|
+
return oldValue;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
set(key, value) {
|
|
49
|
+
if (this.cache.has(key)) {
|
|
50
|
+
this.cache.set(key, value);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
this._size++;
|
|
54
|
+
this._set(key, value);
|
|
55
|
+
}
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const readFile = fs.promises.readFile;
|
|
61
|
+
async function mtime(file) {
|
|
62
|
+
try {
|
|
63
|
+
const stats = await fs.promises.stat(file);
|
|
64
|
+
return stats.mtimeMs || stats.ctimeMs;
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getMmdbReader(buffer) {
|
|
72
|
+
const cache = new LRU();
|
|
73
|
+
return new mmdb.Reader(buffer, {cache});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function validateReader(geoIPReader) {
|
|
77
|
+
try {
|
|
78
|
+
const country = geoIPReader.get('1.2.3.4')?.country?.iso_code;
|
|
79
|
+
return !!country;
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
console.error(e);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function downloadNewDatabase() {
|
|
16
88
|
return new Promise((resolve, reject) => {
|
|
17
|
-
https.get(
|
|
89
|
+
https.get(fileUrl, (res) => {
|
|
18
90
|
try {
|
|
19
91
|
const untar = res.pipe(zlib.createGunzip({})).pipe(tar.t());
|
|
20
92
|
untar.on('entry', (entry) => {
|
|
21
93
|
if (entry.path.endsWith('.mmdb')) {
|
|
22
|
-
const dstFilename = path.join(
|
|
94
|
+
const dstFilename = path.join(fileDir, path.basename(entry.path) + '-tmp');
|
|
23
95
|
try {
|
|
24
96
|
entry.pipe(fs.createWriteStream(dstFilename));
|
|
25
97
|
}
|
|
@@ -30,98 +102,151 @@ function save(url, outDir) {
|
|
|
30
102
|
});
|
|
31
103
|
untar.on('error', e => reject(e));
|
|
32
104
|
untar.on('finish', () => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
105
|
+
setTimeout(async () => {
|
|
106
|
+
try {
|
|
107
|
+
const newGeoIp = getMmdbReader(await readFile(filePath + '-tmp'));
|
|
108
|
+
if (validateReader(newGeoIp)) {
|
|
109
|
+
resolve(newGeoIp);
|
|
110
|
+
fs.promises.rename(filePath + '-tmp', filePath).catch(console.error);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
reject(new Error('Invalid GeoIP Database!'));
|
|
114
|
+
}
|
|
36
115
|
}
|
|
37
|
-
|
|
38
|
-
|
|
116
|
+
catch (e) {
|
|
117
|
+
reject(e);
|
|
39
118
|
}
|
|
40
|
-
});
|
|
119
|
+
}, 100);
|
|
41
120
|
});
|
|
42
121
|
}
|
|
43
122
|
catch (error) {
|
|
44
|
-
|
|
123
|
+
reject(error);
|
|
45
124
|
}
|
|
46
125
|
});
|
|
47
126
|
});
|
|
48
127
|
}
|
|
49
128
|
|
|
50
|
-
function mtime(file) {
|
|
51
|
-
return new Promise((resolve) => {
|
|
52
|
-
try {
|
|
53
|
-
fs.stat(file, (err, stats) => {
|
|
54
|
-
if (err) {
|
|
55
|
-
resolve(false);
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
resolve(stats.mtimeMs || stats.ctimeMs);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
catch (e) {
|
|
63
|
-
resolve(false);
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
129
|
async function download() {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
await save(fileUrl, fileDir);
|
|
130
|
+
console.log(`[Request::GeoIP] Downloading ${fileName} ...`);
|
|
131
|
+
try {
|
|
132
|
+
const newGeoIp = await downloadNewDatabase();
|
|
73
133
|
console.log(`[Request::GeoIP] Downloaded ${fileName} !`);
|
|
134
|
+
return newGeoIp;
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
console.error(e);
|
|
138
|
+
return null;
|
|
74
139
|
}
|
|
75
140
|
}
|
|
76
141
|
|
|
77
142
|
async function updateDb() {
|
|
143
|
+
clearTimeout(updateTimer);
|
|
78
144
|
const modifyTime = await mtime(filePath);
|
|
79
|
-
if (modifyTime < Date.now() -
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
145
|
+
if (modifyTime < Date.now() - 3 * ONE_DAY) {
|
|
146
|
+
const newGeoIp = await download();
|
|
147
|
+
if (newGeoIp) {
|
|
148
|
+
console.log('[Request::GeoIP] Updated GeoIP Database!');
|
|
149
|
+
geoip = newGeoIp;
|
|
150
|
+
}
|
|
84
151
|
}
|
|
85
|
-
|
|
152
|
+
// add some random time in timeout to avoid race condition between multiple processes
|
|
153
|
+
const randomMs = Math.floor(Math.random() * 10000000);
|
|
154
|
+
updateTimer = setTimeout(updateDb, 3 * ONE_DAY + randomMs + processMs);
|
|
155
|
+
updateTimer.unref();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function updateDbOnInit() {
|
|
159
|
+
if (process.env.NODE_ENV !== 'production') return;
|
|
160
|
+
clearTimeout(updateTimer);
|
|
161
|
+
// add some random time in timeout to avoid race condition between multiple processes
|
|
162
|
+
const randomMs = Math.floor(Math.random() * 1000000);
|
|
163
|
+
updateTimer = setTimeout(updateDb, randomMs + processMs);
|
|
164
|
+
updateTimer.unref();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function _geoIpInit() {
|
|
168
|
+
try {
|
|
169
|
+
const newGeoIp = getMmdbReader(await readFile(filePath));
|
|
170
|
+
if (!validateReader(newGeoIp)) {
|
|
171
|
+
throw new Error('Invalid GeoIP file');
|
|
172
|
+
}
|
|
173
|
+
geoip = newGeoIp;
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
console.error(e);
|
|
177
|
+
const newGeoIp = await download();
|
|
178
|
+
if (newGeoIp) {
|
|
179
|
+
geoip = newGeoIp;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
updateDbOnInit();
|
|
86
183
|
}
|
|
87
184
|
|
|
88
185
|
async function geoIpInit() {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
186
|
+
if (!geoInitPromise) {
|
|
187
|
+
geoInitPromise = _geoIpInit();
|
|
188
|
+
}
|
|
189
|
+
return geoInitPromise;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function _geoIpInitSync() {
|
|
193
|
+
try {
|
|
194
|
+
const newGeoIp = getMmdbReader(fs.readFileSync(filePath));
|
|
195
|
+
if (!validateReader(newGeoIp)) {
|
|
196
|
+
throw new Error('Invalid GeoIP file');
|
|
197
|
+
}
|
|
198
|
+
geoip = newGeoIp;
|
|
199
|
+
updateDbOnInit();
|
|
200
|
+
}
|
|
201
|
+
catch (e) {
|
|
202
|
+
console.error(e);
|
|
203
|
+
geoIpInit().catch((err) => {
|
|
204
|
+
console.error(err);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function geoIpInitSync() {
|
|
210
|
+
if (!geoInitSynced) {
|
|
211
|
+
geoInitSynced = true;
|
|
212
|
+
_geoIpInitSync();
|
|
93
213
|
}
|
|
214
|
+
return geoInitSynced;
|
|
94
215
|
}
|
|
95
216
|
|
|
96
217
|
async function getGeoIp() {
|
|
97
218
|
if (!geoip) {
|
|
98
|
-
|
|
99
|
-
geoInitPromise = geoIpInit();
|
|
100
|
-
}
|
|
101
|
-
await geoInitPromise;
|
|
102
|
-
geoInitPromise = null;
|
|
219
|
+
await geoIpInit();
|
|
103
220
|
}
|
|
104
221
|
return geoip;
|
|
105
222
|
}
|
|
106
223
|
|
|
107
224
|
function getGeoIpSync() {
|
|
108
225
|
if (!geoip) {
|
|
109
|
-
|
|
110
|
-
geoip = new maxmind.Reader(buffer);
|
|
111
|
-
if (process.env.NODE_ENV === 'production') {
|
|
112
|
-
updateDb();
|
|
113
|
-
}
|
|
226
|
+
geoIpInitSync();
|
|
114
227
|
}
|
|
115
228
|
return geoip;
|
|
116
229
|
}
|
|
117
230
|
|
|
118
231
|
class GeoIP {
|
|
119
232
|
static async get(ip) {
|
|
120
|
-
|
|
233
|
+
try {
|
|
234
|
+
return (await getGeoIp()).get(ip);
|
|
235
|
+
}
|
|
236
|
+
catch (e) {
|
|
237
|
+
console.error('Error getting geoip location', e);
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
121
240
|
}
|
|
122
241
|
|
|
123
242
|
static getSync(ip) {
|
|
124
|
-
|
|
243
|
+
try {
|
|
244
|
+
return getGeoIpSync().get(ip);
|
|
245
|
+
}
|
|
246
|
+
catch (e) {
|
|
247
|
+
console.error('Error getting geoip location', e);
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
125
250
|
}
|
|
126
251
|
|
|
127
252
|
static async init() {
|
|
@@ -141,7 +266,7 @@ async function main() {
|
|
|
141
266
|
ips.push(`${random(1, 255)}.${random(1, 255)}.${random(1, 255)}.${random(1, 255)}`);
|
|
142
267
|
}
|
|
143
268
|
|
|
144
|
-
console.log((await GeoIP.get('1.2.3.4')).
|
|
269
|
+
console.log((await GeoIP.get('1.2.3.4')).location);
|
|
145
270
|
|
|
146
271
|
console.time('ipLookup');
|
|
147
272
|
for (let i = 0; i < 100000; i++) {
|
|
@@ -150,6 +275,15 @@ async function main() {
|
|
|
150
275
|
}
|
|
151
276
|
console.timeEnd('ipLookup');
|
|
152
277
|
// takes approx ~ 900ms for 100,000 lookups
|
|
278
|
+
|
|
279
|
+
console.log((GeoIP.getSync('1.2.3.4')).location);
|
|
280
|
+
|
|
281
|
+
console.time('ipLookupSync');
|
|
282
|
+
for (let i = 0; i < 100000; i++) {
|
|
283
|
+
GeoIP.getSync(ips[i]);
|
|
284
|
+
}
|
|
285
|
+
console.timeEnd('ipLookupSync');
|
|
286
|
+
// takes approx ~ 900ms for 100,000 lookups
|
|
153
287
|
}
|
|
154
288
|
|
|
155
289
|
if (require.main === module) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smpx/koa-request",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Handle basic tasks for koajs",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"koa-body": "^5.0.0",
|
|
36
36
|
"koa-send": "^5.0.1",
|
|
37
37
|
"koa2-ratelimit": "^1.1.3",
|
|
38
|
-
"
|
|
39
|
-
"tar": "^
|
|
40
|
-
"ua-parser-js": "^1.0.
|
|
38
|
+
"mmdb-lib": "^3.0.1",
|
|
39
|
+
"tar": "^7.4.3",
|
|
40
|
+
"ua-parser-js": "^1.0.40"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"eslint": "^5.7.0",
|