@hyperwatch/hyperwatch 4.2.0 → 5.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/README.md +9 -4
- package/config/apache_syslog_example.js +29 -0
- package/config/default.js +2 -2
- package/config/example.js +5 -5
- package/config/express_websocket_example.js +28 -0
- package/config/websocket_client_example.js +52 -0
- package/docs/configuration.md +67 -6
- package/docs/express-embedding.md +137 -0
- package/docs/input.md +50 -24
- package/docs/tutorials/apache_input.md +16 -14
- package/docs/tutorials/express_input.md +10 -8
- package/package.json +42 -35
- package/scripts/fetch-anthropic-ips.js +60 -0
- package/scripts/fetch-cloudflare-ips.js +27 -0
- package/scripts/fetch-cloudfront-ips.js +29 -0
- package/scripts/fetch-openai-ips.js +34 -0
- package/src/app/api.js +149 -8
- package/src/app/index.js +8 -4
- package/src/app/mount.js +115 -0
- package/src/app/websocket.js +39 -10
- package/src/app/ws-server.js +123 -0
- package/src/constants.js +17 -5
- package/src/data/amazon-searchbot-ips.json +818 -0
- package/src/data/amazon-user-ips.json +1025 -0
- package/src/data/amazonbot-ips.json +1294 -0
- package/src/data/chatgpt-user-ips.json +231 -0
- package/src/data/claude-bot-ips.json +28 -0
- package/src/data/cloudflare-ips.json +24 -0
- package/src/data/cloudfront-ips.json +245 -0
- package/src/data/gptbot-ips.json +20 -0
- package/src/data/openai-searchbot-ips.json +41 -0
- package/src/index.js +24 -3
- package/src/input/http.js +4 -0
- package/src/input/syslog.js +5 -1
- package/src/input/websocket.js +35 -19
- package/src/lib/aggregator.js +169 -20
- package/src/lib/formatter.js +10 -1
- package/src/lib/index.js +2 -0
- package/src/lib/log-buffer.js +45 -0
- package/src/lib/persistence.js +82 -0
- package/src/lib/pipeline.js +122 -11
- package/src/lib/recent-map.js +23 -0
- package/src/lib/speed.js +43 -3
- package/src/lib/util.js +17 -1
- package/src/modules/address.js +59 -2
- package/src/modules/agent.js +1 -1
- package/src/modules/dnsbl.js +11 -1
- package/src/modules/history.js +62 -0
- package/src/modules/identity.js +154 -26
- package/src/modules/index.js +16 -5
- package/src/modules/language.js +1 -1
- package/src/modules/signature.js +64 -18
- package/src/modules/sparkline.js +7 -3
- package/src/modules/status.js +13 -8
- package/src/plugins/proxy.js +10 -203
- package/src/script.js +0 -1
- package/scripts/cloudfront-ips.js +0 -56
package/src/lib/pipeline.js
CHANGED
|
@@ -128,35 +128,79 @@ class Builder {
|
|
|
128
128
|
this.children = [];
|
|
129
129
|
this.name = null;
|
|
130
130
|
this.pipeline = pipeline;
|
|
131
|
+
this.parent = null;
|
|
132
|
+
this.op = null;
|
|
133
|
+
this.module = null;
|
|
134
|
+
this.fnName = null;
|
|
135
|
+
this._label = null;
|
|
136
|
+
// Lightweight per-node counter for throughput tracking
|
|
137
|
+
this.counter = 0;
|
|
138
|
+
// Fixed one-second buckets keep rate tracking bounded and O(1) per event.
|
|
139
|
+
this.rateBuckets = Array.from({ length: 10 }, () => ({
|
|
140
|
+
second: null,
|
|
141
|
+
count: 0,
|
|
142
|
+
}));
|
|
131
143
|
}
|
|
132
144
|
|
|
133
|
-
add(xf) {
|
|
145
|
+
add(xf, { op = null, fnName = null, label = null } = {}) {
|
|
134
146
|
const child = new Builder(xf, this.pipeline);
|
|
147
|
+
child.parent = this;
|
|
148
|
+
child.op = op;
|
|
149
|
+
child.module = this.pipeline ? this.pipeline.currentModule : null;
|
|
150
|
+
child.fnName = fnName || null;
|
|
151
|
+
child._label = label || null;
|
|
135
152
|
this.children.push(child);
|
|
136
153
|
return child;
|
|
137
154
|
}
|
|
138
155
|
|
|
139
|
-
map(f) {
|
|
140
|
-
return this.add(map(f));
|
|
156
|
+
map(f, label) {
|
|
157
|
+
return this.add(map(f), { op: 'map', fnName: f.name, label });
|
|
141
158
|
}
|
|
142
159
|
|
|
143
|
-
filter(pred) {
|
|
144
|
-
return this.add(filter(pred));
|
|
160
|
+
filter(pred, label) {
|
|
161
|
+
return this.add(filter(pred), { op: 'filter', fnName: pred.name, label });
|
|
145
162
|
}
|
|
146
163
|
|
|
147
|
-
split(pred) {
|
|
148
|
-
return [
|
|
164
|
+
split(pred, labels) {
|
|
165
|
+
return [
|
|
166
|
+
this.add(filter(pred), {
|
|
167
|
+
op: 'split-true',
|
|
168
|
+
fnName: pred.name,
|
|
169
|
+
label: labels && labels[0],
|
|
170
|
+
}),
|
|
171
|
+
this.add(filter(complement(pred)), {
|
|
172
|
+
op: 'split-false',
|
|
173
|
+
fnName: pred.name,
|
|
174
|
+
label: labels && labels[1],
|
|
175
|
+
}),
|
|
176
|
+
];
|
|
149
177
|
}
|
|
150
178
|
|
|
151
|
-
by(f) {
|
|
152
|
-
return this.add(by(f));
|
|
179
|
+
by(f, label) {
|
|
180
|
+
return this.add(by(f), { op: 'by', fnName: f.name, label });
|
|
153
181
|
}
|
|
154
182
|
|
|
155
183
|
create() {
|
|
184
|
+
const self = this;
|
|
185
|
+
const countXf = (stream) => (event) => {
|
|
186
|
+
self.counter++;
|
|
187
|
+
const second = Math.floor(Date.now() / 1000);
|
|
188
|
+
const bucket = self.rateBuckets[second % self.rateBuckets.length];
|
|
189
|
+
if (bucket.second !== second) {
|
|
190
|
+
bucket.second = second;
|
|
191
|
+
bucket.count = 0;
|
|
192
|
+
}
|
|
193
|
+
bucket.count++;
|
|
194
|
+
forward(stream, event);
|
|
195
|
+
};
|
|
156
196
|
if (this.children.length === 0) {
|
|
157
|
-
return this.xf;
|
|
197
|
+
return comp([this.xf, countXf]);
|
|
158
198
|
}
|
|
159
|
-
return comp([
|
|
199
|
+
return comp([
|
|
200
|
+
this.xf,
|
|
201
|
+
countXf,
|
|
202
|
+
multiplex(this.children.map((b) => b.create())),
|
|
203
|
+
]);
|
|
160
204
|
}
|
|
161
205
|
|
|
162
206
|
// Helpers
|
|
@@ -194,6 +238,9 @@ class Pipeline extends Builder {
|
|
|
194
238
|
main: this,
|
|
195
239
|
};
|
|
196
240
|
this.pipeline = this;
|
|
241
|
+
this.name = 'raw';
|
|
242
|
+
this.currentModule = null;
|
|
243
|
+
this.op = 'identity';
|
|
197
244
|
}
|
|
198
245
|
|
|
199
246
|
registerInput(input) {
|
|
@@ -204,6 +251,16 @@ class Pipeline extends Builder {
|
|
|
204
251
|
type: 'input',
|
|
205
252
|
});
|
|
206
253
|
this.monitors.push(monitor);
|
|
254
|
+
// Create a tap node so this input gets its own /logs/ WebSocket endpoint
|
|
255
|
+
const inputIndex = this.inputs.length;
|
|
256
|
+
const nodeName = `input-${inputIndex}`;
|
|
257
|
+
const tap = new Builder(identity(), this);
|
|
258
|
+
tap.name = nodeName;
|
|
259
|
+
tap.op = 'input';
|
|
260
|
+
tap.module = null;
|
|
261
|
+
tap.fnName = null;
|
|
262
|
+
this.registerNode(nodeName, tap);
|
|
263
|
+
input._tap = tap;
|
|
207
264
|
}
|
|
208
265
|
|
|
209
266
|
start() {
|
|
@@ -213,6 +270,8 @@ class Pipeline extends Builder {
|
|
|
213
270
|
const monitor = this.monitors.find(
|
|
214
271
|
(monitor) => monitor.type === 'input' && monitor.name === input.name
|
|
215
272
|
);
|
|
273
|
+
// Build the tap stream so it can forward logs to WebSocket clients
|
|
274
|
+
const tapStream = input._tap ? input._tap.create()(() => {}) : null;
|
|
216
275
|
input.start({
|
|
217
276
|
success: (log) => {
|
|
218
277
|
const valid = validate(log.toJS());
|
|
@@ -224,6 +283,9 @@ class Pipeline extends Builder {
|
|
|
224
283
|
data: log,
|
|
225
284
|
});
|
|
226
285
|
monitor.hit('accepted');
|
|
286
|
+
if (tapStream) {
|
|
287
|
+
forward(tapStream, event);
|
|
288
|
+
}
|
|
227
289
|
forward(stream, event);
|
|
228
290
|
} else {
|
|
229
291
|
monitor.hit('rejected');
|
|
@@ -256,12 +318,61 @@ class Pipeline extends Builder {
|
|
|
256
318
|
}
|
|
257
319
|
|
|
258
320
|
registerNode(name, node) {
|
|
321
|
+
const previous = this.nodes[name];
|
|
322
|
+
if (previous && previous !== node && previous.name === name) {
|
|
323
|
+
previous.name = null;
|
|
324
|
+
}
|
|
259
325
|
this.nodes[name] = node;
|
|
260
326
|
}
|
|
261
327
|
|
|
262
328
|
getNode(name) {
|
|
263
329
|
return this.nodes[name];
|
|
264
330
|
}
|
|
331
|
+
|
|
332
|
+
getTree(node = this) {
|
|
333
|
+
const second = Math.floor(Date.now() / 1000);
|
|
334
|
+
const windowStart = second - node.rateBuckets.length + 1;
|
|
335
|
+
const rateCount = node.rateBuckets.reduce(
|
|
336
|
+
(count, bucket) =>
|
|
337
|
+
bucket.second >= windowStart && bucket.second <= second
|
|
338
|
+
? count + bucket.count
|
|
339
|
+
: count,
|
|
340
|
+
0
|
|
341
|
+
);
|
|
342
|
+
const obj = {
|
|
343
|
+
name: node.name || null,
|
|
344
|
+
op: node.op || null,
|
|
345
|
+
module: node.module || null,
|
|
346
|
+
fnName: node.fnName || null,
|
|
347
|
+
label: node._label || null,
|
|
348
|
+
count: node.counter,
|
|
349
|
+
rate: rateCount / node.rateBuckets.length,
|
|
350
|
+
children: node.children.map((child) => this.getTree(child)),
|
|
351
|
+
};
|
|
352
|
+
// Include inputs at the root level
|
|
353
|
+
if (node === this) {
|
|
354
|
+
obj.inputs = this.inputs.map((input) => {
|
|
355
|
+
const monitor = this.monitors.find(
|
|
356
|
+
(m) => m.type === 'input' && m.name === input.name
|
|
357
|
+
);
|
|
358
|
+
const accepted = monitor
|
|
359
|
+
? monitor.speeds.accepted.per_minute.compute()
|
|
360
|
+
: null;
|
|
361
|
+
const rejected = monitor
|
|
362
|
+
? monitor.speeds.rejected.per_minute.compute()
|
|
363
|
+
: null;
|
|
364
|
+
return {
|
|
365
|
+
name: input.name,
|
|
366
|
+
node: input._tap ? input._tap.name : null,
|
|
367
|
+
tree: input._tap ? this.getTree(input._tap) : null,
|
|
368
|
+
status: monitor ? monitor.status : null,
|
|
369
|
+
accepted: accepted ? accepted.reduce((a, b) => a + b, 0) : 0,
|
|
370
|
+
rejected: rejected ? rejected.reduce((a, b) => a + b, 0) : 0,
|
|
371
|
+
};
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
return obj;
|
|
375
|
+
}
|
|
265
376
|
}
|
|
266
377
|
|
|
267
378
|
module.exports = new Pipeline();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Track distinct keys with the time they were last seen, bounded to a
|
|
3
|
+
* rolling window. Backs the addressCount15m/24h and signatureCount15m/24h
|
|
4
|
+
* counters.
|
|
5
|
+
*/
|
|
6
|
+
const { Map } = require('immutable');
|
|
7
|
+
|
|
8
|
+
const { now } = require('./util');
|
|
9
|
+
|
|
10
|
+
// Rolling window in seconds
|
|
11
|
+
const WINDOW = 24 * 3600;
|
|
12
|
+
|
|
13
|
+
// Record `key` as seen at `time` in an Immutable Map<key, unix seconds>
|
|
14
|
+
const touch = (map = Map(), key, time = now()) => map.set(key, time);
|
|
15
|
+
|
|
16
|
+
// Drop keys not seen in the last WINDOW seconds
|
|
17
|
+
const prune = (map, time = now()) => map.filter((seen) => seen > time - WINDOW);
|
|
18
|
+
|
|
19
|
+
// Number of keys seen in the last `window` seconds
|
|
20
|
+
const countRecent = (map, window = WINDOW, time = now()) =>
|
|
21
|
+
map ? map.count((seen) => seen > time - window) : 0;
|
|
22
|
+
|
|
23
|
+
module.exports = { WINDOW, touch, prune, countRecent };
|
package/src/lib/speed.js
CHANGED
|
@@ -10,6 +10,7 @@ class Speed {
|
|
|
10
10
|
this.windowSize = windowSize;
|
|
11
11
|
this.size = size;
|
|
12
12
|
this.counters = Map();
|
|
13
|
+
this.sums = Map();
|
|
13
14
|
this.started = null;
|
|
14
15
|
this.latest = null;
|
|
15
16
|
}
|
|
@@ -18,23 +19,26 @@ class Speed {
|
|
|
18
19
|
gc() {
|
|
19
20
|
const cutoff = now() - this.size * this.windowSize;
|
|
20
21
|
this.counters = this.counters.filter((c, t) => parseInt(t) > cutoff);
|
|
22
|
+
this.sums = this.sums.filter((c, t) => parseInt(t) > cutoff);
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
hit(time = now()) {
|
|
25
|
+
hit(time = now(), value) {
|
|
24
26
|
this.started = !this.started ? time : Math.min(this.started, time);
|
|
25
27
|
this.latest = !this.latest ? time : Math.max(this.latest, time);
|
|
26
28
|
const idx = time - (time % this.windowSize);
|
|
27
29
|
this.counters = this.counters.update(`${idx}`, 0, (n) => n + 1);
|
|
30
|
+
if (value !== undefined) {
|
|
31
|
+
this.sums = this.sums.update(`${idx}`, 0, (n) => n + value);
|
|
32
|
+
}
|
|
28
33
|
this.gc();
|
|
29
34
|
return this;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
compute() {
|
|
37
|
+
compute(time = now()) {
|
|
33
38
|
this.gc();
|
|
34
39
|
if (!this.started) {
|
|
35
40
|
return List();
|
|
36
41
|
}
|
|
37
|
-
const time = now();
|
|
38
42
|
return Range(0, this.size)
|
|
39
43
|
.map((n) => {
|
|
40
44
|
let t = time - n * this.windowSize;
|
|
@@ -45,6 +49,42 @@ class Speed {
|
|
|
45
49
|
})
|
|
46
50
|
.filter((v) => v !== undefined);
|
|
47
51
|
}
|
|
52
|
+
|
|
53
|
+
computeSum(time = now()) {
|
|
54
|
+
this.gc();
|
|
55
|
+
if (!this.started) {
|
|
56
|
+
return List();
|
|
57
|
+
}
|
|
58
|
+
return Range(0, this.size)
|
|
59
|
+
.map((n) => {
|
|
60
|
+
let t = time - n * this.windowSize;
|
|
61
|
+
t = t - (t % this.windowSize);
|
|
62
|
+
if (t >= this.started - (this.started % this.windowSize)) {
|
|
63
|
+
return this.sums.get(`${t}`, 0);
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
.filter((v) => v !== undefined);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
toJSON() {
|
|
70
|
+
return {
|
|
71
|
+
windowSize: this.windowSize,
|
|
72
|
+
size: this.size,
|
|
73
|
+
counters: this.counters.toObject(),
|
|
74
|
+
sums: this.sums.toObject(),
|
|
75
|
+
started: this.started,
|
|
76
|
+
latest: this.latest,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static fromJSON(data) {
|
|
81
|
+
const speed = new Speed(data.windowSize, data.size);
|
|
82
|
+
speed.counters = Map(data.counters);
|
|
83
|
+
speed.sums = Map(data.sums);
|
|
84
|
+
speed.started = data.started;
|
|
85
|
+
speed.latest = data.latest;
|
|
86
|
+
return speed;
|
|
87
|
+
}
|
|
48
88
|
}
|
|
49
89
|
|
|
50
90
|
module.exports = {
|
package/src/lib/util.js
CHANGED
|
@@ -34,12 +34,28 @@ exports.createLog = (req, res) => {
|
|
|
34
34
|
});
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
exports.
|
|
37
|
+
exports.aggregateCount = (entry, key) =>
|
|
38
38
|
entry
|
|
39
39
|
.getIn(['speed', key])
|
|
40
40
|
.compute()
|
|
41
41
|
.reduce((p, c) => p + c, 0);
|
|
42
42
|
|
|
43
|
+
exports.aggregateSum = (entry, key) =>
|
|
44
|
+
entry
|
|
45
|
+
.getIn(['speed', key])
|
|
46
|
+
.computeSum()
|
|
47
|
+
.reduce((p, c) => p + c, 0);
|
|
48
|
+
|
|
49
|
+
exports.formatDuration = (ms) => {
|
|
50
|
+
const totalSeconds = ms / 1000;
|
|
51
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
52
|
+
const seconds = Math.round(totalSeconds % 60);
|
|
53
|
+
if (minutes > 0) {
|
|
54
|
+
return seconds > 0 ? `${minutes}m${seconds}s` : `${minutes}m`;
|
|
55
|
+
}
|
|
56
|
+
return `${totalSeconds.toFixed(1)}s`;
|
|
57
|
+
};
|
|
58
|
+
|
|
43
59
|
exports.formatTable = (data) => {
|
|
44
60
|
if (!data || data.length === 0) {
|
|
45
61
|
return '';
|
package/src/modules/address.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
const { is } = require('immutable');
|
|
2
|
+
|
|
1
3
|
const api = require('../app/api');
|
|
2
4
|
const { Aggregator } = require('../lib/aggregator');
|
|
3
5
|
const pipeline = require('../lib/pipeline');
|
|
6
|
+
const { touch, prune, countRecent } = require('../lib/recent-map');
|
|
4
7
|
|
|
5
8
|
const identifier = (log) => log.getIn(['address', 'value']);
|
|
6
9
|
|
|
10
|
+
const signatureCount15m = (entry) =>
|
|
11
|
+
countRecent(entry.get('signatures'), 15 * 60);
|
|
12
|
+
const signatureCount24h = (entry) => countRecent(entry.get('signatures'));
|
|
13
|
+
|
|
14
|
+
let aggregator;
|
|
15
|
+
|
|
7
16
|
function fill(log) {
|
|
8
17
|
if (!log.hasIn(['address', 'value'])) {
|
|
9
18
|
log = log.setIn(['address', 'value'], log.getIn(['request', 'address']));
|
|
@@ -16,11 +25,56 @@ function init() {
|
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
function start() {
|
|
19
|
-
|
|
28
|
+
aggregator = new Aggregator();
|
|
20
29
|
|
|
21
30
|
aggregator.setIdentifier(identifier);
|
|
22
31
|
|
|
23
|
-
|
|
32
|
+
const enricher = (entry, log) => {
|
|
33
|
+
for (const field of [
|
|
34
|
+
'address',
|
|
35
|
+
'identity',
|
|
36
|
+
'cloudflare',
|
|
37
|
+
'dnsbl',
|
|
38
|
+
'geoip',
|
|
39
|
+
'hostname',
|
|
40
|
+
'agent',
|
|
41
|
+
'language',
|
|
42
|
+
'signature',
|
|
43
|
+
]) {
|
|
44
|
+
if (log.has(field) && !is(log.get(field), entry.get(field))) {
|
|
45
|
+
entry = entry.set(field, log.get(field));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Distinct signature IDs with last-seen time, pruned to 24h; backs both
|
|
50
|
+
// the 15m and 24h counts
|
|
51
|
+
const signatureId = log.getIn(['signature', 'id']);
|
|
52
|
+
if (signatureId) {
|
|
53
|
+
entry = entry.update('signatures', (map) => touch(map, signatureId));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return entry;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
aggregator.setEnricher(enricher);
|
|
60
|
+
|
|
61
|
+
aggregator.setEntryGc((entry) =>
|
|
62
|
+
entry.has('signatures') ? entry.update('signatures', prune) : entry
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
aggregator.formatter.insertFormat('signatureCount15m', signatureCount15m, {
|
|
66
|
+
before: 'count15m',
|
|
67
|
+
});
|
|
68
|
+
aggregator.formatter.insertFormat('signatureCount24h', signatureCount24h, {
|
|
69
|
+
before: 'count15m',
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
aggregator.sorters.signatureCount15m = signatureCount15m;
|
|
73
|
+
aggregator.sorters.signatureCount24h = signatureCount24h;
|
|
74
|
+
|
|
75
|
+
pipeline
|
|
76
|
+
.getNode('main')
|
|
77
|
+
.map((log) => aggregator.processLog(log), 'aggregator');
|
|
24
78
|
|
|
25
79
|
api.registerAggregator('addresses', aggregator);
|
|
26
80
|
}
|
|
@@ -28,4 +82,7 @@ function start() {
|
|
|
28
82
|
module.exports = {
|
|
29
83
|
init,
|
|
30
84
|
start,
|
|
85
|
+
get aggregator() {
|
|
86
|
+
return aggregator;
|
|
87
|
+
},
|
|
31
88
|
};
|
package/src/modules/agent.js
CHANGED
|
@@ -70,7 +70,7 @@ function init() {
|
|
|
70
70
|
pipeline.getNode('main').map(augment).registerNode('main');
|
|
71
71
|
|
|
72
72
|
aggregator.defaultFormatter.insertFormat('agent', agentFormat, {
|
|
73
|
-
before: '
|
|
73
|
+
before: 'count15m',
|
|
74
74
|
color: 'grey',
|
|
75
75
|
});
|
|
76
76
|
aggregator.defaultFormatter.insertFormat('os', osFormat, {
|
package/src/modules/dnsbl.js
CHANGED
|
@@ -4,11 +4,19 @@ const aggregator = require('../lib/aggregator');
|
|
|
4
4
|
const cache = require('../lib/cache');
|
|
5
5
|
const pipeline = require('../lib/pipeline');
|
|
6
6
|
|
|
7
|
+
// Indirection so tests can swap the DNS lookup without hitting the network.
|
|
8
|
+
const defaultLookup = (ip, blacklist) => dnsbl.lookup(ip, blacklist);
|
|
9
|
+
let lookup = defaultLookup;
|
|
10
|
+
|
|
11
|
+
function setLookup(fn = defaultLookup) {
|
|
12
|
+
lookup = fn;
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
async function xblLookup(ip) {
|
|
8
16
|
if (await cache.has(`xbl-${ip}`)) {
|
|
9
17
|
return cache.get(`xbl-${ip}`);
|
|
10
18
|
}
|
|
11
|
-
const result = await
|
|
19
|
+
const result = await lookup(ip, 'xbl.spamhaus.org');
|
|
12
20
|
cache.set(`xbl-${ip}`, result);
|
|
13
21
|
return result;
|
|
14
22
|
}
|
|
@@ -45,4 +53,6 @@ function init() {
|
|
|
45
53
|
module.exports = {
|
|
46
54
|
augment,
|
|
47
55
|
init,
|
|
56
|
+
setLookup,
|
|
57
|
+
xblFormat,
|
|
48
58
|
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const { api } = require('../app');
|
|
2
|
+
const constants = require('../constants');
|
|
3
|
+
const LogBuffer = require('../lib/log-buffer');
|
|
4
|
+
const persistence = require('../lib/persistence');
|
|
5
|
+
const pipeline = require('../lib/pipeline');
|
|
6
|
+
|
|
7
|
+
function start() {
|
|
8
|
+
const capacity =
|
|
9
|
+
(constants.modules.history && constants.modules.history.capacity) || 1000;
|
|
10
|
+
const buffers = {};
|
|
11
|
+
|
|
12
|
+
function registerNodeHistory(name, node) {
|
|
13
|
+
const buffer = new LogBuffer(capacity);
|
|
14
|
+
buffers[name] = buffer;
|
|
15
|
+
persistence.register(`history-${persistence.safeName(name)}`, buffer);
|
|
16
|
+
|
|
17
|
+
node.map((log) => {
|
|
18
|
+
buffer.push(log);
|
|
19
|
+
return log;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
api.get(`/history/${name}.json`, (req, res) => {
|
|
23
|
+
const { identity, signature, address } = req.query;
|
|
24
|
+
const limit = parseInt(req.query.limit, 10) || 100;
|
|
25
|
+
|
|
26
|
+
let logs = buffer.toArray();
|
|
27
|
+
|
|
28
|
+
if (identity) {
|
|
29
|
+
logs = logs.filter((log) => log.get('identity') === identity);
|
|
30
|
+
}
|
|
31
|
+
if (signature) {
|
|
32
|
+
logs = logs.filter(
|
|
33
|
+
(log) => log.getIn(['signature', 'id']) === signature
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
if (address) {
|
|
37
|
+
logs = logs.filter(
|
|
38
|
+
(log) => log.getIn(['address', 'value']) === address
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
logs = logs.slice(0, limit);
|
|
43
|
+
|
|
44
|
+
res.json(logs);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const [name, node] of Object.entries(pipeline.nodes)) {
|
|
49
|
+
registerNodeHistory(name, node);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Auto-register future nodes
|
|
53
|
+
const originalRegisterNode = pipeline.registerNode.bind(pipeline);
|
|
54
|
+
pipeline.registerNode = function (name, node) {
|
|
55
|
+
originalRegisterNode(name, node);
|
|
56
|
+
if (!buffers[name]) {
|
|
57
|
+
registerNodeHistory(name, node);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { start };
|