securenow 7.6.6 → 7.6.7
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/firewall.js +88 -57
- package/package.json +1 -1
package/firewall.js
CHANGED
|
@@ -45,12 +45,15 @@ let _circuitOpenedAt = 0;
|
|
|
45
45
|
let _pollInflight = false;
|
|
46
46
|
let _retryAfterUntil = 0;
|
|
47
47
|
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
// Firewall control-plane traffic is low-frequency and correctness-critical.
|
|
49
|
+
// Use non-keep-alive agents so clustered apps do not reuse sockets that an
|
|
50
|
+
// upstream load balancer has silently closed between sync cycles.
|
|
51
|
+
const _httpAgent = new http.Agent({ keepAlive: false });
|
|
52
|
+
const _httpsAgent = new https.Agent({ keepAlive: false });
|
|
51
53
|
const EVENT_FLUSH_INTERVAL_MS = 2_000;
|
|
52
54
|
const EVENT_BATCH_SIZE = 25;
|
|
53
55
|
const EVENT_QUEUE_MAX = 1_000;
|
|
56
|
+
const TRANSIENT_NETWORK_CODES = new Set(['ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'EAI_AGAIN']);
|
|
54
57
|
|
|
55
58
|
// Unified sync uses /firewall/sync (v2). Falls back to legacy on 404.
|
|
56
59
|
let _useUnifiedSync = true;
|
|
@@ -119,67 +122,95 @@ function jitter(baseMs) {
|
|
|
119
122
|
return baseMs * (0.8 + Math.random() * 0.4);
|
|
120
123
|
}
|
|
121
124
|
|
|
122
|
-
function agentFor(url) {
|
|
123
|
-
return url.startsWith('https') ? _httpsAgent : _httpAgent;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function httpGet(url, extraHeaders, timeout, callback) {
|
|
127
|
-
const mod = url.startsWith('https') ? https : http;
|
|
128
|
-
const parsed = new URL(url);
|
|
129
|
-
|
|
130
|
-
const req = mod.request({
|
|
131
|
-
hostname: parsed.hostname,
|
|
132
|
-
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
133
|
-
path: parsed.pathname + parsed.search,
|
|
134
|
-
method: 'GET',
|
|
135
|
-
headers: {
|
|
136
|
-
'Authorization': `Bearer ${_options.apiKey}`,
|
|
137
|
-
'User-Agent': 'securenow-firewall-sdk',
|
|
138
|
-
...extraHeaders,
|
|
139
|
-
},
|
|
140
|
-
timeout,
|
|
141
|
-
agent: agentFor(url),
|
|
142
|
-
}, (res) => {
|
|
143
|
-
let data = '';
|
|
144
|
-
res.on('data', (chunk) => { data += chunk; });
|
|
145
|
-
res.on('end', () => { callback(null, res, data); });
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
req.on('error', (err) => callback(err));
|
|
149
|
-
req.on('timeout', () => { req.destroy(); callback(new Error('Request timed out')); });
|
|
150
|
-
req.end();
|
|
125
|
+
function agentFor(url) {
|
|
126
|
+
return url.startsWith('https') ? _httpsAgent : _httpAgent;
|
|
151
127
|
}
|
|
152
128
|
|
|
153
|
-
function
|
|
129
|
+
function isTransientNetworkError(err) {
|
|
130
|
+
if (!err) return false;
|
|
131
|
+
if (err.code && TRANSIENT_NETWORK_CODES.has(err.code)) return true;
|
|
132
|
+
return /socket hang up|connection reset|ECONNRESET/i.test(String(err.message || ''));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function formatRequestError(err) {
|
|
136
|
+
if (!err) return 'unknown error';
|
|
137
|
+
const parts = [err.message || String(err)];
|
|
138
|
+
if (err.code && !parts[0].includes(err.code)) parts.push(`code=${err.code}`);
|
|
139
|
+
if (err.syscall) parts.push(`syscall=${err.syscall}`);
|
|
140
|
+
return parts.join(' ');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function requestOnce(method, url, body, extraHeaders, timeout, callback) {
|
|
154
144
|
const mod = url.startsWith('https') ? https : http;
|
|
155
145
|
const parsed = new URL(url);
|
|
156
|
-
const payload = JSON.stringify(body || {});
|
|
146
|
+
const payload = body == null ? null : JSON.stringify(body || {});
|
|
157
147
|
|
|
158
148
|
const req = mod.request({
|
|
159
149
|
hostname: parsed.hostname,
|
|
160
150
|
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
161
151
|
path: parsed.pathname + parsed.search,
|
|
162
|
-
method
|
|
152
|
+
method,
|
|
163
153
|
headers: {
|
|
164
154
|
'Authorization': `Bearer ${_options.apiKey}`,
|
|
165
155
|
'User-Agent': 'securenow-firewall-sdk',
|
|
166
|
-
'
|
|
167
|
-
|
|
156
|
+
'Connection': 'close',
|
|
157
|
+
...(payload
|
|
158
|
+
? {
|
|
159
|
+
'Content-Type': 'application/json',
|
|
160
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
161
|
+
}
|
|
162
|
+
: {}),
|
|
163
|
+
...extraHeaders,
|
|
168
164
|
},
|
|
169
165
|
timeout,
|
|
170
166
|
agent: agentFor(url),
|
|
171
167
|
}, (res) => {
|
|
172
168
|
let data = '';
|
|
173
169
|
res.on('data', (chunk) => { data += chunk; });
|
|
174
|
-
res.on('end', () => {
|
|
170
|
+
res.on('end', () => { done(null, res, data); });
|
|
175
171
|
});
|
|
176
172
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
173
|
+
let finished = false;
|
|
174
|
+
function done(...args) {
|
|
175
|
+
if (finished) return;
|
|
176
|
+
finished = true;
|
|
177
|
+
callback(...args);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
req.on('error', (err) => done(err));
|
|
181
|
+
req.on('timeout', () => {
|
|
182
|
+
const err = new Error('Request timed out');
|
|
183
|
+
err.code = 'ETIMEDOUT';
|
|
184
|
+
done(err);
|
|
185
|
+
req.destroy(err);
|
|
186
|
+
});
|
|
187
|
+
if (payload) req.write(payload);
|
|
180
188
|
req.end();
|
|
181
189
|
}
|
|
182
190
|
|
|
191
|
+
function requestWithRetry(method, url, body, extraHeaders, timeout, callback) {
|
|
192
|
+
requestOnce(method, url, body, extraHeaders, timeout, (err, res, data) => {
|
|
193
|
+
if (!err || !isTransientNetworkError(err)) return callback(err, res, data);
|
|
194
|
+
|
|
195
|
+
if (_options && _options.log) {
|
|
196
|
+
console.warn('[securenow] Firewall: transient API socket error, retrying once:', formatRequestError(err));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const retryTimer = setTimeout(() => {
|
|
200
|
+
requestOnce(method, url, body, extraHeaders, timeout, callback);
|
|
201
|
+
}, 100);
|
|
202
|
+
if (retryTimer.unref) retryTimer.unref();
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function httpGet(url, extraHeaders, timeout, callback) {
|
|
207
|
+
requestWithRetry('GET', url, null, extraHeaders, timeout, callback);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function httpPostJson(url, body, timeout, callback) {
|
|
211
|
+
requestWithRetry('POST', url, body, {}, timeout, callback);
|
|
212
|
+
}
|
|
213
|
+
|
|
183
214
|
function scheduleEventFlush() {
|
|
184
215
|
if (_eventTimer || _eventQueue.length === 0) return;
|
|
185
216
|
_eventTimer = setTimeout(() => {
|
|
@@ -202,7 +233,7 @@ function flushFirewallEvents() {
|
|
|
202
233
|
httpPostJson(url, { events: batch }, 5000, (err, res) => {
|
|
203
234
|
if (err || !res || res.statusCode >= 400) {
|
|
204
235
|
if (_options.log) {
|
|
205
|
-
const msg = err ? err
|
|
236
|
+
const msg = err ? formatRequestError(err) : `API returned ${res.statusCode}`;
|
|
206
237
|
console.warn('[securenow] Firewall: failed to report blocked-request ledger:', msg);
|
|
207
238
|
}
|
|
208
239
|
}
|
|
@@ -448,19 +479,19 @@ function doLegacyPoll(callback) {
|
|
|
448
479
|
callback(null, { blChanged, alChanged });
|
|
449
480
|
}
|
|
450
481
|
|
|
451
|
-
if (blChanged) {
|
|
452
|
-
legacyBlocklistSync((err, changed, stats) => {
|
|
453
|
-
if (err && _options.log) console.warn('[securenow] Firewall: sync failed (using stale list):', err
|
|
454
|
-
else if (changed && stats && _options.log) console.log('[securenow] Firewall: re-synced %d blocked IPs', stats.total);
|
|
455
|
-
syncDone();
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
|
-
if (alChanged) {
|
|
459
|
-
legacyAllowlistSync((err, changed, stats) => {
|
|
460
|
-
if (err && _options.log) console.warn('[securenow] Firewall: allowlist sync failed:', err
|
|
461
|
-
else if (changed && stats && _options.log) console.log('[securenow] Firewall: re-synced %d allowed IPs', stats.total);
|
|
462
|
-
syncDone();
|
|
463
|
-
});
|
|
482
|
+
if (blChanged) {
|
|
483
|
+
legacyBlocklistSync((err, changed, stats) => {
|
|
484
|
+
if (err && _options.log) console.warn('[securenow] Firewall: sync failed (using stale list):', formatRequestError(err));
|
|
485
|
+
else if (changed && stats && _options.log) console.log('[securenow] Firewall: re-synced %d blocked IPs', stats.total);
|
|
486
|
+
syncDone();
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
if (alChanged) {
|
|
490
|
+
legacyAllowlistSync((err, changed, stats) => {
|
|
491
|
+
if (err && _options.log) console.warn('[securenow] Firewall: allowlist sync failed:', formatRequestError(err));
|
|
492
|
+
else if (changed && stats && _options.log) console.log('[securenow] Firewall: re-synced %d allowed IPs', stats.total);
|
|
493
|
+
syncDone();
|
|
494
|
+
});
|
|
464
495
|
}
|
|
465
496
|
}
|
|
466
497
|
|
|
@@ -495,7 +526,7 @@ function pollOnce(callback) {
|
|
|
495
526
|
_consecutiveErrors++;
|
|
496
527
|
_stats.errors++;
|
|
497
528
|
maybeOpenCircuit();
|
|
498
|
-
if (_options.log) console.warn('[securenow] Firewall: poll failed:', err
|
|
529
|
+
if (_options.log) console.warn('[securenow] Firewall: poll failed:', formatRequestError(err));
|
|
499
530
|
return callback(err);
|
|
500
531
|
}
|
|
501
532
|
_consecutiveErrors = 0;
|
|
@@ -565,7 +596,7 @@ function startSyncLoop() {
|
|
|
565
596
|
if (retryTimer.unref) retryTimer.unref();
|
|
566
597
|
return;
|
|
567
598
|
}
|
|
568
|
-
if (_options.log) console.warn('[securenow] Firewall: initial sync failed:', err
|
|
599
|
+
if (_options.log) console.warn('[securenow] Firewall: initial sync failed:', formatRequestError(err));
|
|
569
600
|
if (_options.failMode === 'closed') {
|
|
570
601
|
_matcher = { isBlocked: () => true, stats: () => ({ exact: 0, cidr: 0, total: 0 }) };
|
|
571
602
|
}
|
package/package.json
CHANGED