@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.
Files changed (2) hide show
  1. package/GeoIP.js +190 -56
  2. package/package.json +4 -4
package/GeoIP.js CHANGED
@@ -1,4 +1,4 @@
1
- const maxmind = require('maxmind');
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
- function save(url, outDir) {
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(url, (res) => {
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(outDir, path.basename(entry.path) + '-tmp');
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
- fs.rename(filePath + '-tmp', filePath, (err) => {
34
- if (err) {
35
- reject(err);
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
- else {
38
- resolve();
116
+ catch (e) {
117
+ reject(e);
39
118
  }
40
- });
119
+ }, 100);
41
120
  });
42
121
  }
43
122
  catch (error) {
44
- throw new Error(`Could not fetch ${url}\n\nError:\n${error}`);
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
- const modifyTime = await mtime(filePath);
70
- if (!modifyTime) {
71
- console.log(`[Request::GeoIP] Downloading ${fileName} ...`);
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() - 2 * 24 * 3600 * 1000) {
80
- console.log(`[Request::GeoIP] Downloading ${fileName} ...`);
81
- await save(fileUrl, fileDir);
82
- console.log(`[Request::GeoIP] Downloaded ${fileName} !`);
83
- geoip = await maxmind.open(filePath);
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
- setTimeout(updateDb, 3 * 24 * 3600 * 1000);
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
- await download();
90
- geoip = await maxmind.open(filePath);
91
- if (process.env.NODE_ENV === 'production') {
92
- updateDb();
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
- if (!geoInitPromise) {
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
- const buffer = fs.readFileSync(filePath);
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
- return (await getGeoIp()).get(ip);
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
- return getGeoIpSync().get(ip);
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')).subdivisions);
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.4",
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
- "maxmind": "^4.3.20",
39
- "tar": "^6.2.1",
40
- "ua-parser-js": "^1.0.38"
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",