@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/lib/build/plugins/internal.js +0 -26
  3. package/lib/build/plugins/nodejs/index.js +8 -3
  4. package/lib/cds.js +8 -2
  5. package/lib/init/merge.js +21 -17
  6. package/lib/init/template/cloud-logging/index.js +3 -3
  7. package/lib/init/template/destination/index.js +3 -3
  8. package/lib/init/template/enterprise-messaging/index.js +4 -1
  9. package/lib/init/template/event-mesh/index.js +3 -0
  10. package/lib/init/template/multitenancy/index.js +2 -2
  11. package/lib/init/template/workzone-standard/index.js +1 -1
  12. package/node_modules/@cap-js/db-service/CHANGELOG.md +13 -0
  13. package/node_modules/@cap-js/db-service/lib/cqn4sql.js +9 -5
  14. package/node_modules/@cap-js/db-service/package.json +1 -1
  15. package/node_modules/@cap-js/sqlite/CHANGELOG.md +9 -0
  16. package/node_modules/@cap-js/sqlite/package.json +2 -2
  17. package/node_modules/@sap/cds/CHANGELOG.md +19 -0
  18. package/node_modules/@sap/cds/common.cds +3 -3
  19. package/node_modules/@sap/cds/lib/compile/minify.js +3 -1
  20. package/node_modules/@sap/cds/lib/env/cds-env.js +1 -1
  21. package/node_modules/@sap/cds/lib/env/cds-requires.js +1 -0
  22. package/node_modules/@sap/cds/libx/_runtime/common/generic/crud.js +7 -1
  23. package/node_modules/@sap/cds/libx/_runtime/common/utils/cqn.js +1 -1
  24. package/node_modules/@sap/cds/libx/_runtime/fiori/lean-draft.js +1 -1
  25. package/node_modules/@sap/cds/libx/_runtime/messaging/common-utils/normalizeIncomingMessage.js +1 -1
  26. package/node_modules/@sap/cds/libx/_runtime/messaging/kafka.js +1 -1
  27. package/node_modules/@sap/cds/libx/http/location.js +10 -11
  28. package/node_modules/@sap/cds/libx/odata/middleware/read.js +1 -1
  29. package/node_modules/@sap/cds/libx/queue/SchedulingService.js +4 -0
  30. package/node_modules/@sap/cds/libx/queue/index.js +44 -10
  31. package/node_modules/@sap/cds/libx/queue/processing.js +47 -16
  32. package/node_modules/@sap/cds/libx/queue/utils.js +3 -0
  33. package/node_modules/@sap/cds/package.json +1 -1
  34. package/node_modules/@sap/cds/tasks/enterprise-messaging-deploy.js +13 -5
  35. package/node_modules/proxy-addr/HISTORY.md +60 -54
  36. package/node_modules/proxy-addr/README.md +1 -1
  37. package/node_modules/proxy-addr/index.js +32 -0
  38. package/node_modules/proxy-addr/package.json +8 -4
  39. package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +19 -18
  40. package/node_modules/yaml/browser/dist/nodes/Alias.js +25 -23
  41. package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +19 -18
  42. package/node_modules/yaml/dist/nodes/Alias.js +25 -23
  43. package/node_modules/yaml/package.json +1 -1
  44. 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.SELECT.forUpdate.ignoreLocked = true
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
- // The latter is for backwards compatibility where `service` was not stored and `target` was the service name
219
- const srvName = _msg.service || _task.target
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
- // only delete singleton task if not modified in the meantime
268
- if (task.msg.queue?.task) _delete.where({ timestamp: { '<': _timestampPlusOneMS(task.msg.timestamp) } })
269
- await _delete
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.timestamp
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
- if (task.msg.queue?.every) {
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 _addParamFalse = w => {
31
- if (w.xpr) return w.xpr.map(xpr => _addParamFalse(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 ${appid ? `appid = '${appid}' AND ` : ''}target = '${queueName}'
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 => _addParamFalse(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 => _addParamFalse(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
- // The latter is for backwards compatibility where `service` was not stored and `target` was the service name
261
- const srvName = _msg.service || _task.target
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
- await DELETE.from(OUTBOX).where({ ID: task.ID })
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 (only if not already a callback event)
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
- // Next recurrence for .every() tasks
356
- if (task.msg.queue?.every) {
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.timestamp
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
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "10.1.0",
3
+ "version": "10.1.1",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [
@@ -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 oa2 = {
23
- client: xsuaaSrv.credentials.clientid,
24
- secret: xsuaaSrv.credentials.clientsecret,
25
- endpoint: xsuaaSrv.credentials.url
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.7 / 2021-05-31
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 / 2020-02-24
8
- ==================
13
+ 2.0.6
14
+ =====
9
15
 
10
16
  * deps: ipaddr.js@1.9.1
11
17
 
12
- 2.0.5 / 2019-04-16
13
- ==================
18
+ 2.0.5
19
+ =====
14
20
 
15
21
  * deps: ipaddr.js@1.9.0
16
22
 
17
- 2.0.4 / 2018-07-26
18
- ==================
23
+ 2.0.4
24
+ =====
19
25
 
20
26
  * deps: ipaddr.js@1.8.0
21
27
 
22
- 2.0.3 / 2018-02-19
23
- ==================
28
+ 2.0.3
29
+ =====
24
30
 
25
31
  * deps: ipaddr.js@1.6.0
26
32
 
27
- 2.0.2 / 2017-09-24
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 / 2017-09-10
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 / 2017-08-08
43
- ==================
48
+ 2.0.0
49
+ =====
44
50
 
45
51
  * Drop support for Node.js below 0.10
46
52
 
47
- 1.1.5 / 2017-07-25
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 / 2017-03-24
54
- ==================
59
+ 1.1.4
60
+ =====
55
61
 
56
62
  * deps: ipaddr.js@1.3.0
57
63
 
58
- 1.1.3 / 2017-01-14
59
- ==================
64
+ 1.1.3
65
+ =====
60
66
 
61
67
  * deps: ipaddr.js@1.2.0
62
68
 
63
- 1.1.2 / 2016-05-29
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 / 2016-05-03
70
- ==================
75
+ 1.1.1
76
+ =====
71
77
 
72
78
  * Fix regression matching mixed versions against multiple subnets
73
79
 
74
- 1.1.0 / 2016-05-01
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 / 2015-12-09
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 / 2015-12-01
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 / 2015-05-10
97
- ==================
102
+ 1.0.8
103
+ =====
98
104
 
99
105
  * deps: ipaddr.js@1.0.1
100
106
 
101
- 1.0.7 / 2015-03-16
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 / 2015-02-01
108
- ==================
113
+ 1.0.6
114
+ =====
109
115
 
110
116
  * deps: ipaddr.js@0.1.8
111
117
 
112
- 1.0.5 / 2015-01-08
113
- ==================
118
+ 1.0.5
119
+ =====
114
120
 
115
121
  * deps: ipaddr.js@0.1.6
116
122
 
117
- 1.0.4 / 2014-11-23
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 / 2014-09-21
124
- ==================
129
+ 1.0.3
130
+ =====
125
131
 
126
132
  * Use `forwarded` npm module
127
133
 
128
- 1.0.2 / 2014-09-18
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 / 2014-06-03
136
- ==================
141
+ 1.0.1
142
+ =====
137
143
 
138
144
  * Fix links in npm package
139
145
 
140
- 1.0.0 / 2014-05-08
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 / 2014-05-04
154
- ==================
159
+ 0.0.1
160
+ =====
155
161
 
156
162
  * Fix bad npm publish
157
163
 
158
- 0.0.0 / 2014-05-04
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` arugment may also be a single IP address string or an
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.7",
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.0.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.23.4",
24
- "eslint-plugin-markdown": "2.2.0",
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",