@smpx/koa-request 1.1.0 → 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 +167 -55
  2. package/package.json +2 -2
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,88 +102,128 @@ 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
+ }
151
+ }
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
+ }
84
181
  }
85
- const timer = setTimeout(updateDb, 3 * 24 * 3600 * 1000);
86
- timer.unref();
182
+ updateDbOnInit();
87
183
  }
88
184
 
89
185
  async function geoIpInit() {
90
- await download();
91
- geoip = await maxmind.open(filePath);
92
- if (process.env.NODE_ENV === 'production') {
93
- 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
+ });
94
206
  }
95
207
  }
96
208
 
209
+ function geoIpInitSync() {
210
+ if (!geoInitSynced) {
211
+ geoInitSynced = true;
212
+ _geoIpInitSync();
213
+ }
214
+ return geoInitSynced;
215
+ }
216
+
97
217
  async function getGeoIp() {
98
218
  if (!geoip) {
99
- if (!geoInitPromise) {
100
- geoInitPromise = geoIpInit();
101
- }
102
- await geoInitPromise;
103
- geoInitPromise = null;
219
+ await geoIpInit();
104
220
  }
105
221
  return geoip;
106
222
  }
107
223
 
108
224
  function getGeoIpSync() {
109
225
  if (!geoip) {
110
- const buffer = fs.readFileSync(filePath);
111
- geoip = new maxmind.Reader(buffer);
112
- if (process.env.NODE_ENV === 'production') {
113
- updateDb();
114
- }
226
+ geoIpInitSync();
115
227
  }
116
228
  return geoip;
117
229
  }
@@ -119,7 +231,7 @@ function getGeoIpSync() {
119
231
  class GeoIP {
120
232
  static async get(ip) {
121
233
  try {
122
- return await (await getGeoIp()).get(ip);
234
+ return (await getGeoIp()).get(ip);
123
235
  }
124
236
  catch (e) {
125
237
  console.error('Error getting geoip location', e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smpx/koa-request",
3
- "version": "1.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,7 +35,7 @@
35
35
  "koa-body": "^5.0.0",
36
36
  "koa-send": "^5.0.1",
37
37
  "koa2-ratelimit": "^1.1.3",
38
- "maxmind": "^5.0.0",
38
+ "mmdb-lib": "^3.0.1",
39
39
  "tar": "^7.4.3",
40
40
  "ua-parser-js": "^1.0.40"
41
41
  },