@sap/cds-dk 10.1.1 → 10.1.2
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/CHANGELOG.md +12 -0
- package/lib/build/plugins/internal.js +0 -26
- package/lib/build/plugins/nodejs/index.js +8 -3
- package/lib/cds.js +8 -2
- package/lib/init/merge.js +21 -17
- package/lib/init/template/cloud-logging/index.js +3 -3
- package/lib/init/template/destination/index.js +3 -3
- package/lib/init/template/enterprise-messaging/index.js +4 -1
- package/lib/init/template/event-mesh/index.js +3 -0
- package/lib/init/template/multitenancy/index.js +2 -2
- package/lib/init/template/workzone-standard/index.js +1 -1
- package/node_modules/@cap-js/db-service/CHANGELOG.md +13 -0
- package/node_modules/@cap-js/db-service/lib/cqn4sql.js +9 -5
- package/node_modules/@cap-js/db-service/package.json +1 -1
- package/node_modules/@cap-js/sqlite/CHANGELOG.md +9 -0
- package/node_modules/@cap-js/sqlite/package.json +2 -2
- package/node_modules/@sap/cds/CHANGELOG.md +19 -0
- package/node_modules/@sap/cds/common.cds +3 -3
- package/node_modules/@sap/cds/lib/compile/minify.js +3 -1
- package/node_modules/@sap/cds/lib/env/cds-env.js +1 -1
- package/node_modules/@sap/cds/lib/env/cds-requires.js +1 -0
- package/node_modules/@sap/cds/libx/_runtime/common/generic/crud.js +7 -1
- package/node_modules/@sap/cds/libx/_runtime/common/utils/cqn.js +1 -1
- package/node_modules/@sap/cds/libx/_runtime/fiori/lean-draft.js +1 -1
- package/node_modules/@sap/cds/libx/_runtime/messaging/common-utils/normalizeIncomingMessage.js +1 -1
- package/node_modules/@sap/cds/libx/_runtime/messaging/kafka.js +1 -1
- package/node_modules/@sap/cds/libx/http/location.js +10 -11
- package/node_modules/@sap/cds/libx/odata/middleware/read.js +1 -1
- package/node_modules/@sap/cds/libx/queue/SchedulingService.js +4 -0
- package/node_modules/@sap/cds/libx/queue/index.js +44 -10
- package/node_modules/@sap/cds/libx/queue/processing.js +47 -16
- package/node_modules/@sap/cds/libx/queue/utils.js +3 -0
- package/node_modules/@sap/cds/package.json +1 -1
- package/node_modules/@sap/cds/tasks/enterprise-messaging-deploy.js +13 -5
- package/node_modules/proxy-addr/HISTORY.md +60 -54
- package/node_modules/proxy-addr/README.md +1 -1
- package/node_modules/proxy-addr/index.js +32 -0
- package/node_modules/proxy-addr/package.json +8 -4
- package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +19 -18
- package/node_modules/yaml/browser/dist/nodes/Alias.js +25 -23
- package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +19 -18
- package/node_modules/yaml/dist/nodes/Alias.js +25 -23
- package/node_modules/yaml/package.json +1 -1
- package/package.json +1 -1
|
@@ -132,7 +132,7 @@ const _processTasks = (target, tenant, _opts = {}) => {
|
|
|
132
132
|
|
|
133
133
|
if (opts.maxAttempts) tasksQuery.where({ attempts: { '<': opts.maxAttempts } })
|
|
134
134
|
if (opts.parallel) {
|
|
135
|
-
tasksQuery.
|
|
135
|
+
tasksQuery.forUpdate({ ignoreLocked: true })
|
|
136
136
|
const shifted = new Date(Math.max(0, currTime - _timeout)).toISOString()
|
|
137
137
|
tasksQuery.where({ status: null, or: { lastAttemptTimestamp: { '<': shifted } } })
|
|
138
138
|
// Note: If there are messages which are not yet timed out, but will hang eventually,
|
|
@@ -215,8 +215,14 @@ const _processTasks = (target, tenant, _opts = {}) => {
|
|
|
215
215
|
const user = new cds.User.Privileged(userId)
|
|
216
216
|
context.user = user
|
|
217
217
|
|
|
218
|
-
//
|
|
219
|
-
|
|
218
|
+
// older messages have no `service`; the name is encoded in `target` as
|
|
219
|
+
// `<targetPrefix><serviceName>` (see utils.targetName)
|
|
220
|
+
let srvName = _msg.service
|
|
221
|
+
if (!srvName) {
|
|
222
|
+
srvName = _task.target
|
|
223
|
+
if (opts.targetPrefix && srvName.startsWith(opts.targetPrefix))
|
|
224
|
+
srvName = srvName.slice(opts.targetPrefix.length)
|
|
225
|
+
}
|
|
220
226
|
delete _msg.service
|
|
221
227
|
|
|
222
228
|
const fromSend = _msg._fromSend
|
|
@@ -227,6 +233,7 @@ const _processTasks = (target, tenant, _opts = {}) => {
|
|
|
227
233
|
|
|
228
234
|
const task = {
|
|
229
235
|
ID: _task.ID,
|
|
236
|
+
timestamp: _task.timestamp,
|
|
230
237
|
msg,
|
|
231
238
|
context,
|
|
232
239
|
srvName,
|
|
@@ -246,6 +253,19 @@ const _processTasks = (target, tenant, _opts = {}) => {
|
|
|
246
253
|
let minAttemptFailed
|
|
247
254
|
try {
|
|
248
255
|
const _handleWithErr = async task => {
|
|
256
|
+
if (!cds.services[task.srvName]) {
|
|
257
|
+
// service not served/registered: unrecoverable, so dead-letter it (attempts -> maxAttempts)
|
|
258
|
+
// instead of crashing the app or blocking the queue
|
|
259
|
+
LOG.error(`No service '${task.srvName}' found for a queued message; treating as unrecoverable.`)
|
|
260
|
+
if (!minAttemptFailed) minAttemptFailed = task.attempts
|
|
261
|
+
else minAttemptFailed = Math.min(minAttemptFailed, task.attempts)
|
|
262
|
+
task[$error] = new Error(`No service '${task.srvName}' found`)
|
|
263
|
+
if (opts.maxAttempts) {
|
|
264
|
+
task.updateData = { attempts: opts.maxAttempts }
|
|
265
|
+
toBeUpdated.push(task)
|
|
266
|
+
} else toBeDeleted.push(task)
|
|
267
|
+
return
|
|
268
|
+
}
|
|
249
269
|
const service = cds.unqueued(await cds.connect.to(task.srvName))
|
|
250
270
|
try {
|
|
251
271
|
if (task.msg.query) {
|
|
@@ -264,9 +284,21 @@ const _processTasks = (target, tenant, _opts = {}) => {
|
|
|
264
284
|
// we can only delete in the current tx if legacyLocking (i.e., long-lasting db locks) is false
|
|
265
285
|
if (!opts.legacyLocking) {
|
|
266
286
|
const _delete = DELETE.from(tasksEntity).where({ ID: task.ID })
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
287
|
+
// guard on the selected row's timestamp (not `task.msg.timestamp`: `timestamp` is in
|
|
288
|
+
// `_ignoredContext`, absent from the msg, and reads back as `now`)
|
|
289
|
+
if (task.msg.queue?.task)
|
|
290
|
+
_delete.where({ timestamp: { '<': _timestampPlusOneMS(new Date(task.timestamp)) } })
|
|
291
|
+
const _deleted = await _delete
|
|
292
|
+
const _cnt = typeof _deleted === 'number' ? _deleted : (_deleted?.affected ?? 0)
|
|
293
|
+
// recreate the next occurrence in the same tx; skip if the guarded delete removed nothing
|
|
294
|
+
// (the singleton was unscheduled/rescheduled concurrently)
|
|
295
|
+
if (task.msg.queue?.every && (!task.msg.queue?.task || _cnt > 0)) {
|
|
296
|
+
const _m = { ...task.msg }
|
|
297
|
+
_m._fromSend = task.msg instanceof cds.Request
|
|
298
|
+
await INSERT.into(tasksEntity).entries(create_task(target, _m, task.context, task.srvName, opts, true))
|
|
299
|
+
// re-trigger the drain to schedule the new occurrence
|
|
300
|
+
_setWaitingTime(0)
|
|
301
|
+
}
|
|
270
302
|
}
|
|
271
303
|
return result
|
|
272
304
|
})
|
|
@@ -353,7 +385,7 @@ const _processTasks = (target, tenant, _opts = {}) => {
|
|
|
353
385
|
if (each.updateData.lastError && typeof each.updateData.lastError !== 'string') {
|
|
354
386
|
each.updateData.lastError = inspect(each.updateData.lastError)
|
|
355
387
|
}
|
|
356
|
-
each.updateData.lastAttemptTimestamp = cds.context
|
|
388
|
+
each.updateData.lastAttemptTimestamp = cds.context?.timestamp ?? new Date()
|
|
357
389
|
queries.push(UPDATE(tasksEntity).where({ ID: each.ID }).set(each.updateData))
|
|
358
390
|
}
|
|
359
391
|
|
|
@@ -409,11 +441,11 @@ const _processTasks = (target, tenant, _opts = {}) => {
|
|
|
409
441
|
}
|
|
410
442
|
}
|
|
411
443
|
|
|
412
|
-
// handle `every`
|
|
413
|
-
|
|
444
|
+
// handle `every` — !legacyLocking recreates in the success tx above; the legacyLocking path
|
|
445
|
+
// (deferred delete) recreates here, using a read-back to detect a concurrent unschedule/reschedule
|
|
446
|
+
if (task.msg.queue?.every && opts.legacyLocking) {
|
|
414
447
|
const _m = { ...task.msg }
|
|
415
448
|
if (task.msg.queue.task) {
|
|
416
|
-
// Read from database as the singleton task might have been modified/deleted in the meantime
|
|
417
449
|
const dbTask = await SELECT.one(tasksEntity).where({ ID: task.ID })
|
|
418
450
|
if (!dbTask) continue //> was unscheduled -> no next occurrence
|
|
419
451
|
Object.assign(_m, JSON.parse(dbTask.msg))
|
|
@@ -425,6 +457,8 @@ const _processTasks = (target, tenant, _opts = {}) => {
|
|
|
425
457
|
}
|
|
426
458
|
// invoke failed handlers (only if max attempts is reached)
|
|
427
459
|
for (const task of toBeUpdated) {
|
|
460
|
+
// skip dead-lettered messages whose service is not served/registered (see above)
|
|
461
|
+
if (!cds.services[task.srvName]) continue
|
|
428
462
|
if (
|
|
429
463
|
!task.msg.event.endsWith('/#succeeded') &&
|
|
430
464
|
!task.msg.event.endsWith('/#failed') &&
|
|
@@ -25,10 +25,15 @@ const $service = Symbol('service')
|
|
|
25
25
|
const MAX_WAITING_TIME = 1480000
|
|
26
26
|
const CAPATTEMPTS = Math.ceil(Math.log(MAX_WAITING_TIME / 1000 + 1) / Math.log(1.5))
|
|
27
27
|
|
|
28
|
+
const _timestampPlusOneMS = ts => new Date(ts.getTime() + 1).toISOString()
|
|
29
|
+
|
|
28
30
|
let _processableTasksQuery = {}
|
|
29
31
|
|
|
30
|
-
const
|
|
31
|
-
if (w.xpr) return w.xpr.map(xpr =>
|
|
32
|
+
const _inlineLiterals = w => {
|
|
33
|
+
if (w.xpr) return w.xpr.map(xpr => _inlineLiterals(xpr))
|
|
34
|
+
// also recurse into function args (e.g. `startswith(target, '<appid>')`) so a literal arg isn't
|
|
35
|
+
// bound as a `?` and shift the positional binding of the query's real params
|
|
36
|
+
if (w.args) return (Array.isArray(w.args) ? w.args : Object.values(w.args)).map(a => _inlineLiterals(a))
|
|
32
37
|
if (w.val === undefined) return w
|
|
33
38
|
w.param = false
|
|
34
39
|
return w
|
|
@@ -50,9 +55,14 @@ const _getProcessableTasksQuery = ({ maxAttempts, chunkSize }, queueName) => {
|
|
|
50
55
|
|
|
51
56
|
const appid = cds.env.appid
|
|
52
57
|
|
|
58
|
+
// also match pre-`appid` rows: `appid IS NULL` with `target` starting with `appid` (the former app prefix)
|
|
59
|
+
const targetCondition = appid
|
|
60
|
+
? `(appid = '${appid}' AND target = '${queueName}') OR (appid IS NULL AND startswith(target, '${appid}') = true)`
|
|
61
|
+
: `target = '${queueName}'`
|
|
62
|
+
|
|
53
63
|
const sql = `
|
|
54
64
|
SELECT ID FROM ${entity.name}
|
|
55
|
-
WHERE ${
|
|
65
|
+
WHERE (${targetCondition})
|
|
56
66
|
AND attempts < ${maxAttempts}
|
|
57
67
|
AND (status is NULL OR lastAttemptTimestamp < ?)
|
|
58
68
|
AND (${attemptConditions.join(' OR ')})
|
|
@@ -61,10 +71,9 @@ const _getProcessableTasksQuery = ({ maxAttempts, chunkSize }, queueName) => {
|
|
|
61
71
|
|
|
62
72
|
_processableTasksQuery[queueName] = cds.ql(sql)
|
|
63
73
|
_processableTasksQuery[queueName].SELECT.limit.rows.param = false
|
|
64
|
-
_processableTasksQuery[queueName].SELECT.where.map(w =>
|
|
74
|
+
_processableTasksQuery[queueName].SELECT.where.map(w => _inlineLiterals(w))
|
|
65
75
|
_processableTasksQuery[queueName].SELECT.columns = Object.keys(entity.elements).map(k => ({ ref: [k] }))
|
|
66
|
-
_processableTasksQuery[queueName].forUpdate()
|
|
67
|
-
_processableTasksQuery[queueName].SELECT.forUpdate.ignoreLocked = true
|
|
76
|
+
_processableTasksQuery[queueName].forUpdate({ ignoreLocked: true })
|
|
68
77
|
|
|
69
78
|
return _processableTasksQuery[queueName]
|
|
70
79
|
}
|
|
@@ -101,10 +110,9 @@ const _getFlushTasksQuery = ({ maxAttempts, chunkSize }, queueName) => {
|
|
|
101
110
|
ORDER BY timestamp ASC, ID ASC
|
|
102
111
|
LIMIT ${chunkSize}`)
|
|
103
112
|
_flushTasksQuery[queueName].SELECT.limit.rows.param = false
|
|
104
|
-
_flushTasksQuery[queueName].SELECT.where.map(w =>
|
|
113
|
+
_flushTasksQuery[queueName].SELECT.where.map(w => _inlineLiterals(w))
|
|
105
114
|
_flushTasksQuery[queueName].SELECT.columns = Object.keys(entity.elements).map(k => ({ ref: [k] }))
|
|
106
|
-
_flushTasksQuery[queueName].forUpdate()
|
|
107
|
-
_flushTasksQuery[queueName].SELECT.forUpdate.ignoreLocked = true
|
|
115
|
+
_flushTasksQuery[queueName].forUpdate({ ignoreLocked: true })
|
|
108
116
|
}
|
|
109
117
|
return _flushTasksQuery[queueName]
|
|
110
118
|
}
|
|
@@ -257,8 +265,13 @@ const _processTasks = (queueName, tenant, _opts = {}) => {
|
|
|
257
265
|
const user = new cds.User.Privileged(userId)
|
|
258
266
|
context.user = user
|
|
259
267
|
|
|
260
|
-
//
|
|
261
|
-
|
|
268
|
+
// older messages have no `service`; the name is encoded in `target` as `<appid><serviceName>`
|
|
269
|
+
let srvName = _msg.service
|
|
270
|
+
if (!srvName) {
|
|
271
|
+
srvName = _task.target
|
|
272
|
+
const appid = cds.env.appid
|
|
273
|
+
if (appid && srvName.startsWith(appid)) srvName = srvName.slice(appid.length)
|
|
274
|
+
}
|
|
262
275
|
delete _msg.service
|
|
263
276
|
|
|
264
277
|
const fromSend = _msg._fromSend
|
|
@@ -301,6 +314,15 @@ const _processTasks = (queueName, tenant, _opts = {}) => {
|
|
|
301
314
|
|
|
302
315
|
try {
|
|
303
316
|
const _handleWithErr = async task => {
|
|
317
|
+
if (!cds.services[task.srvName]) {
|
|
318
|
+
// service not served/registered: unrecoverable, so dead-letter it (attempts -> maxAttempts)
|
|
319
|
+
// instead of crashing the app or blocking the queue
|
|
320
|
+
LOG.error(`No service '${task.srvName}' found for a queued message; treating as unrecoverable.`)
|
|
321
|
+
task[$error] = new Error(`No service '${task.srvName}' found`)
|
|
322
|
+
task.updateData = { attempts: opts.maxAttempts }
|
|
323
|
+
toBeUpdated.push(task)
|
|
324
|
+
return
|
|
325
|
+
}
|
|
304
326
|
const service = cds.unqueued(await cds.connect.to(task.srvName))
|
|
305
327
|
try {
|
|
306
328
|
if (task.msg.query) {
|
|
@@ -330,12 +352,18 @@ const _processTasks = (queueName, tenant, _opts = {}) => {
|
|
|
330
352
|
.where({ msg: { like: `%"tenant":"${task.msg.data.tenant}"%` } })
|
|
331
353
|
.where({ msg: { like: `%"target":"${task.msg.data.target}"%` } })
|
|
332
354
|
} else {
|
|
333
|
-
|
|
355
|
+
const _delete = DELETE.from(OUTBOX).where({ ID: task.ID })
|
|
356
|
+
// guard on the selected row's timestamp so a concurrent unschedule/reschedule is
|
|
357
|
+
// serialized against the recurrence recreated below
|
|
358
|
+
if (task.msg.queue?.task)
|
|
359
|
+
_delete.where({ timestamp: { '<': _timestampPlusOneMS(new Date(task._task.timestamp)) } })
|
|
360
|
+
const _deleted = await _delete
|
|
361
|
+
const _cnt = typeof _deleted === 'number' ? _deleted : (_deleted?.affected ?? 0)
|
|
334
362
|
|
|
335
363
|
// For atomicity: create follow-up tasks in same transaction as delete
|
|
336
364
|
const follow_ups = []
|
|
337
365
|
|
|
338
|
-
// #succeeded callback
|
|
366
|
+
// #succeeded callback — always owed once the in-flight execution completed
|
|
339
367
|
if (!task.msg.event.endsWith('/#succeeded') && !task.msg.event.endsWith('/#failed')) {
|
|
340
368
|
const succeededMsg = _newMsgForCallbackFrom(task.msg)
|
|
341
369
|
succeededMsg.event = succeededMsg.event + '/#succeeded'
|
|
@@ -352,8 +380,9 @@ const _processTasks = (queueName, tenant, _opts = {}) => {
|
|
|
352
380
|
}
|
|
353
381
|
}
|
|
354
382
|
|
|
355
|
-
//
|
|
356
|
-
|
|
383
|
+
// next occurrence — only if the guarded delete removed the row (else it was unscheduled/
|
|
384
|
+
// rescheduled concurrently); non-singletons always recreate
|
|
385
|
+
if (task.msg.queue?.every && (!task.msg.queue?.task || _cnt > 0)) {
|
|
357
386
|
const _m = { ...task.msg }
|
|
358
387
|
_m._fromSend = task.msg instanceof cds.Request
|
|
359
388
|
const next_iteration_task = create_task(queueName, _m, task.context, task.srvName, opts, true)
|
|
@@ -423,7 +452,7 @@ const _processTasks = (queueName, tenant, _opts = {}) => {
|
|
|
423
452
|
if (each.updateData.lastError && typeof each.updateData.lastError !== 'string') {
|
|
424
453
|
each.updateData.lastError = inspect(each.updateData.lastError)
|
|
425
454
|
}
|
|
426
|
-
each.updateData.lastAttemptTimestamp = cds.context.
|
|
455
|
+
each.updateData.lastAttemptTimestamp = cds.context?.timestamp ?? new Date().toISOString()
|
|
427
456
|
follow_up_tasks.push(Object.assign({}, each._task, each.updateData))
|
|
428
457
|
afters.add(waitingTime(each.updateData.attempts))
|
|
429
458
|
}
|
|
@@ -441,6 +470,8 @@ const _processTasks = (queueName, tenant, _opts = {}) => {
|
|
|
441
470
|
|
|
442
471
|
// Create #failed callbacks (only if max attempts is reached)
|
|
443
472
|
for (const task of toBeUpdated) {
|
|
473
|
+
// skip dead-lettered messages whose service is not served/registered (see above)
|
|
474
|
+
if (!cds.services[task.srvName]) continue
|
|
444
475
|
if (
|
|
445
476
|
!is_t0 &&
|
|
446
477
|
!task.msg.event.endsWith('/#succeeded') &&
|
|
@@ -51,6 +51,9 @@ const create_task = (queueName, msg, context, serviceName, queueOpts, reoccurren
|
|
|
51
51
|
appid: cds.env.appid,
|
|
52
52
|
target: cds.env.appid ? queueName : targetName(queueName, queueOpts),
|
|
53
53
|
timestamp,
|
|
54
|
+
// reset status so re-submitting a task that is currently being processed (UPSERT over its
|
|
55
|
+
// 'processing' row) yields a processable row again instead of a stuck one
|
|
56
|
+
status: null,
|
|
54
57
|
msg: JSON.stringify(_msg)
|
|
55
58
|
}
|
|
56
59
|
|
|
@@ -19,11 +19,19 @@ const xsuaaSrv = Object.keys(vcap_services)
|
|
|
19
19
|
if (!xsuaaSrv)
|
|
20
20
|
throw new Error('You need to provide credentials of service `XSUAA` in environment variable `VCAP_SERVICES`')
|
|
21
21
|
const tokenStore = {}
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
const xsuaaCreds = xsuaaSrv.credentials
|
|
23
|
+
const oa2 =
|
|
24
|
+
xsuaaCreds['credential-type'] === 'x509'
|
|
25
|
+
? {
|
|
26
|
+
client: xsuaaCreds.clientid,
|
|
27
|
+
endpoint: xsuaaCreds.certurl,
|
|
28
|
+
mTLS: { cert: xsuaaCreds.certificate, key: xsuaaCreds.key }
|
|
29
|
+
}
|
|
30
|
+
: {
|
|
31
|
+
client: xsuaaCreds.clientid,
|
|
32
|
+
secret: xsuaaCreds.clientsecret,
|
|
33
|
+
endpoint: xsuaaCreds.url
|
|
34
|
+
}
|
|
27
35
|
|
|
28
36
|
const main = async () => {
|
|
29
37
|
// use authorizedRequest to fire against deploy endpoint
|
|
@@ -1,144 +1,150 @@
|
|
|
1
|
-
2.0.
|
|
2
|
-
|
|
1
|
+
2.0.8
|
|
2
|
+
=====
|
|
3
|
+
|
|
4
|
+
* Fix [CVE-2026-90711](https://www.cve.org/CVERecord?id=CVE-2026-90711) ([GHSA-jqcg-44mw-7w3h](https://github.com/jshttp/proxy-addr/security/advisories/GHSA-jqcg-44mw-7w3h))
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
2.0.7
|
|
8
|
+
=====
|
|
3
9
|
|
|
4
10
|
* deps: forwarded@0.2.0
|
|
5
11
|
- Use `req.socket` over deprecated `req.connection`
|
|
6
12
|
|
|
7
|
-
2.0.6
|
|
8
|
-
|
|
13
|
+
2.0.6
|
|
14
|
+
=====
|
|
9
15
|
|
|
10
16
|
* deps: ipaddr.js@1.9.1
|
|
11
17
|
|
|
12
|
-
2.0.5
|
|
13
|
-
|
|
18
|
+
2.0.5
|
|
19
|
+
=====
|
|
14
20
|
|
|
15
21
|
* deps: ipaddr.js@1.9.0
|
|
16
22
|
|
|
17
|
-
2.0.4
|
|
18
|
-
|
|
23
|
+
2.0.4
|
|
24
|
+
=====
|
|
19
25
|
|
|
20
26
|
* deps: ipaddr.js@1.8.0
|
|
21
27
|
|
|
22
|
-
2.0.3
|
|
23
|
-
|
|
28
|
+
2.0.3
|
|
29
|
+
=====
|
|
24
30
|
|
|
25
31
|
* deps: ipaddr.js@1.6.0
|
|
26
32
|
|
|
27
|
-
2.0.2
|
|
28
|
-
|
|
33
|
+
2.0.2
|
|
34
|
+
=====
|
|
29
35
|
|
|
30
36
|
* deps: forwarded@~0.1.2
|
|
31
37
|
- perf: improve header parsing
|
|
32
38
|
- perf: reduce overhead when no `X-Forwarded-For` header
|
|
33
39
|
|
|
34
|
-
2.0.1
|
|
35
|
-
|
|
40
|
+
2.0.1
|
|
41
|
+
=====
|
|
36
42
|
|
|
37
43
|
* deps: forwarded@~0.1.1
|
|
38
44
|
- Fix trimming leading / trailing OWS
|
|
39
45
|
- perf: hoist regular expression
|
|
40
46
|
* deps: ipaddr.js@1.5.2
|
|
41
47
|
|
|
42
|
-
2.0.0
|
|
43
|
-
|
|
48
|
+
2.0.0
|
|
49
|
+
=====
|
|
44
50
|
|
|
45
51
|
* Drop support for Node.js below 0.10
|
|
46
52
|
|
|
47
|
-
1.1.5
|
|
48
|
-
|
|
53
|
+
1.1.5
|
|
54
|
+
=====
|
|
49
55
|
|
|
50
56
|
* Fix array argument being altered
|
|
51
57
|
* deps: ipaddr.js@1.4.0
|
|
52
58
|
|
|
53
|
-
1.1.4
|
|
54
|
-
|
|
59
|
+
1.1.4
|
|
60
|
+
=====
|
|
55
61
|
|
|
56
62
|
* deps: ipaddr.js@1.3.0
|
|
57
63
|
|
|
58
|
-
1.1.3
|
|
59
|
-
|
|
64
|
+
1.1.3
|
|
65
|
+
=====
|
|
60
66
|
|
|
61
67
|
* deps: ipaddr.js@1.2.0
|
|
62
68
|
|
|
63
|
-
1.1.2
|
|
64
|
-
|
|
69
|
+
1.1.2
|
|
70
|
+
=====
|
|
65
71
|
|
|
66
72
|
* deps: ipaddr.js@1.1.1
|
|
67
73
|
- Fix IPv6-mapped IPv4 validation edge cases
|
|
68
74
|
|
|
69
|
-
1.1.1
|
|
70
|
-
|
|
75
|
+
1.1.1
|
|
76
|
+
=====
|
|
71
77
|
|
|
72
78
|
* Fix regression matching mixed versions against multiple subnets
|
|
73
79
|
|
|
74
|
-
1.1.0
|
|
75
|
-
|
|
80
|
+
1.1.0
|
|
81
|
+
=====
|
|
76
82
|
|
|
77
83
|
* Fix accepting various invalid netmasks
|
|
78
84
|
- IPv4 netmasks must be contingous
|
|
79
85
|
- IPv6 addresses cannot be used as a netmask
|
|
80
86
|
* deps: ipaddr.js@1.1.0
|
|
81
87
|
|
|
82
|
-
1.0.10
|
|
83
|
-
|
|
88
|
+
1.0.10
|
|
89
|
+
======
|
|
84
90
|
|
|
85
91
|
* deps: ipaddr.js@1.0.5
|
|
86
92
|
- Fix regression in `isValid` with non-string arguments
|
|
87
93
|
|
|
88
|
-
1.0.9
|
|
89
|
-
|
|
94
|
+
1.0.9
|
|
95
|
+
=====
|
|
90
96
|
|
|
91
97
|
* deps: ipaddr.js@1.0.4
|
|
92
98
|
- Fix accepting some invalid IPv6 addresses
|
|
93
99
|
- Reject CIDRs with negative or overlong masks
|
|
94
100
|
* perf: enable strict mode
|
|
95
101
|
|
|
96
|
-
1.0.8
|
|
97
|
-
|
|
102
|
+
1.0.8
|
|
103
|
+
=====
|
|
98
104
|
|
|
99
105
|
* deps: ipaddr.js@1.0.1
|
|
100
106
|
|
|
101
|
-
1.0.7
|
|
102
|
-
|
|
107
|
+
1.0.7
|
|
108
|
+
=====
|
|
103
109
|
|
|
104
110
|
* deps: ipaddr.js@0.1.9
|
|
105
111
|
- Fix OOM on certain inputs to `isValid`
|
|
106
112
|
|
|
107
|
-
1.0.6
|
|
108
|
-
|
|
113
|
+
1.0.6
|
|
114
|
+
=====
|
|
109
115
|
|
|
110
116
|
* deps: ipaddr.js@0.1.8
|
|
111
117
|
|
|
112
|
-
1.0.5
|
|
113
|
-
|
|
118
|
+
1.0.5
|
|
119
|
+
=====
|
|
114
120
|
|
|
115
121
|
* deps: ipaddr.js@0.1.6
|
|
116
122
|
|
|
117
|
-
1.0.4
|
|
118
|
-
|
|
123
|
+
1.0.4
|
|
124
|
+
=====
|
|
119
125
|
|
|
120
126
|
* deps: ipaddr.js@0.1.5
|
|
121
127
|
- Fix edge cases with `isValid`
|
|
122
128
|
|
|
123
|
-
1.0.3
|
|
124
|
-
|
|
129
|
+
1.0.3
|
|
130
|
+
=====
|
|
125
131
|
|
|
126
132
|
* Use `forwarded` npm module
|
|
127
133
|
|
|
128
|
-
1.0.2
|
|
129
|
-
|
|
134
|
+
1.0.2
|
|
135
|
+
=====
|
|
130
136
|
|
|
131
137
|
* Fix a global leak when multiple subnets are trusted
|
|
132
138
|
* Support Node.js 0.6
|
|
133
139
|
* deps: ipaddr.js@0.1.3
|
|
134
140
|
|
|
135
|
-
1.0.1
|
|
136
|
-
|
|
141
|
+
1.0.1
|
|
142
|
+
=====
|
|
137
143
|
|
|
138
144
|
* Fix links in npm package
|
|
139
145
|
|
|
140
|
-
1.0.0
|
|
141
|
-
|
|
146
|
+
1.0.0
|
|
147
|
+
=====
|
|
142
148
|
|
|
143
149
|
* Add `trust` argument to determine proxy trust on
|
|
144
150
|
* Accepts custom function
|
|
@@ -150,12 +156,12 @@
|
|
|
150
156
|
* Add `proxyaddr.compile` to pre-compile `trust` function
|
|
151
157
|
to make subsequent calls faster
|
|
152
158
|
|
|
153
|
-
0.0.1
|
|
154
|
-
|
|
159
|
+
0.0.1
|
|
160
|
+
=====
|
|
155
161
|
|
|
156
162
|
* Fix bad npm publish
|
|
157
163
|
|
|
158
|
-
0.0.0
|
|
159
|
-
|
|
164
|
+
0.0.0
|
|
165
|
+
=====
|
|
160
166
|
|
|
161
167
|
* Initial release
|
|
@@ -37,7 +37,7 @@ proxyaddr(req, function (addr) { return addr === '127.0.0.1' })
|
|
|
37
37
|
proxyaddr(req, function (addr, i) { return i < 1 })
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
The `trust`
|
|
40
|
+
The `trust` argument may also be a single IP address string or an
|
|
41
41
|
array of trusted addresses, as plain IP addresses, CIDR-formatted
|
|
42
42
|
strings, or IP/netmask strings.
|
|
43
43
|
|
|
@@ -256,6 +256,13 @@ function trustMulti (subnets) {
|
|
|
256
256
|
if (!isip(addr)) return false
|
|
257
257
|
|
|
258
258
|
var ip = parseip(addr)
|
|
259
|
+
|
|
260
|
+
if (ip.kind() === 'ipv6' && ip.isIPv4MappedAddress()) {
|
|
261
|
+
// Canonicalize IPv4-mapped candidates to IPv4 so a mapped address
|
|
262
|
+
// cannot bypass the cross-family guard via the same-family path
|
|
263
|
+
ip = ip.toIPv4Address()
|
|
264
|
+
}
|
|
265
|
+
|
|
259
266
|
var ipconv
|
|
260
267
|
var kind = ip.kind()
|
|
261
268
|
|
|
@@ -272,6 +279,12 @@ function trustMulti (subnets) {
|
|
|
272
279
|
continue
|
|
273
280
|
}
|
|
274
281
|
|
|
282
|
+
if (subnetkind !== 'ipv4' && !(subnetrange >= 96 && subnetip.isIPv4MappedAddress())) {
|
|
283
|
+
// IPv6 subnet only spans IPv4 when it is a mapped subnet whose
|
|
284
|
+
// prefix covers the ::ffff: marker; otherwise it must not match IPv4
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
|
|
275
288
|
if (!ipconv) {
|
|
276
289
|
// Convert IP to match subnet IP kind
|
|
277
290
|
ipconv = subnetkind === 'ipv4'
|
|
@@ -280,6 +293,9 @@ function trustMulti (subnets) {
|
|
|
280
293
|
}
|
|
281
294
|
|
|
282
295
|
trusted = ipconv
|
|
296
|
+
} else if (kind === 'ipv6' && subnetip.isIPv4MappedAddress()) {
|
|
297
|
+
// A native IPv6 candidate cannot match an IPv4-mapped subnet
|
|
298
|
+
continue
|
|
283
299
|
}
|
|
284
300
|
|
|
285
301
|
if (trusted.match(subnetip, subnetrange)) {
|
|
@@ -308,6 +324,13 @@ function trustSingle (subnet) {
|
|
|
308
324
|
if (!isip(addr)) return false
|
|
309
325
|
|
|
310
326
|
var ip = parseip(addr)
|
|
327
|
+
|
|
328
|
+
if (ip.kind() === 'ipv6' && ip.isIPv4MappedAddress()) {
|
|
329
|
+
// Canonicalize IPv4-mapped candidates to IPv4 so a mapped address
|
|
330
|
+
// cannot bypass the cross-family guard via the same-family path
|
|
331
|
+
ip = ip.toIPv4Address()
|
|
332
|
+
}
|
|
333
|
+
|
|
311
334
|
var kind = ip.kind()
|
|
312
335
|
|
|
313
336
|
if (kind !== subnetkind) {
|
|
@@ -316,10 +339,19 @@ function trustSingle (subnet) {
|
|
|
316
339
|
return false
|
|
317
340
|
}
|
|
318
341
|
|
|
342
|
+
if (!subnetisipv4 && !(subnetrange >= 96 && subnetip.isIPv4MappedAddress())) {
|
|
343
|
+
// IPv6 subnet only spans IPv4 when it is a mapped subnet whose
|
|
344
|
+
// prefix covers the ::ffff: marker; otherwise it must not match IPv4
|
|
345
|
+
return false
|
|
346
|
+
}
|
|
347
|
+
|
|
319
348
|
// Convert IP to match subnet IP kind
|
|
320
349
|
ip = subnetisipv4
|
|
321
350
|
? ip.toIPv4Address()
|
|
322
351
|
: ip.toIPv4MappedAddress()
|
|
352
|
+
} else if (kind === 'ipv6' && subnetip.isIPv4MappedAddress()) {
|
|
353
|
+
// A native IPv6 candidate cannot match an IPv4-mapped subnet
|
|
354
|
+
return false
|
|
323
355
|
}
|
|
324
356
|
|
|
325
357
|
return ip.match(subnetip, subnetrange)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proxy-addr",
|
|
3
3
|
"description": "Determine address of proxied request",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.8",
|
|
5
5
|
"author": "Douglas Christopher Wilson <doug@somethingdoug.com>",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
"x-forwarded-for"
|
|
11
11
|
],
|
|
12
12
|
"repository": "jshttp/proxy-addr",
|
|
13
|
+
"funding": {
|
|
14
|
+
"type": "opencollective",
|
|
15
|
+
"url": "https://opencollective.com/express"
|
|
16
|
+
},
|
|
13
17
|
"dependencies": {
|
|
14
18
|
"forwarded": "0.2.0",
|
|
15
19
|
"ipaddr.js": "1.9.1"
|
|
@@ -17,11 +21,11 @@
|
|
|
17
21
|
"devDependencies": {
|
|
18
22
|
"benchmark": "2.1.4",
|
|
19
23
|
"beautify-benchmark": "0.2.4",
|
|
20
|
-
"deep-equal": "1.
|
|
24
|
+
"deep-equal": "1.1.2",
|
|
21
25
|
"eslint": "7.26.0",
|
|
22
26
|
"eslint-config-standard": "14.1.1",
|
|
23
|
-
"eslint-plugin-import": "2.
|
|
24
|
-
"eslint-plugin-markdown": "2.2.
|
|
27
|
+
"eslint-plugin-import": "2.32.0",
|
|
28
|
+
"eslint-plugin-markdown": "2.2.1",
|
|
25
29
|
"eslint-plugin-node": "11.1.0",
|
|
26
30
|
"eslint-plugin-promise": "4.3.1",
|
|
27
31
|
"eslint-plugin-standard": "4.1.0",
|