aaden-cli 1.6.109 → 1.6.110

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/dist/index.js CHANGED
@@ -250,6 +250,102 @@ function collectHostInfo() {
250
250
  module.exports = {collectHostInfo}
251
251
 
252
252
 
253
+ /***/ }),
254
+
255
+ /***/ 8226:
256
+ /***/ ((module) => {
257
+
258
+ // TSE 证书到期日:第一代 Swissbit TSE(BSI-DSZ-CC-1121)的证书只有 5.5 年,
259
+ // 到期当天起 TSE 拒绝对时,之后每笔交易都报 WORM_ERROR_NO_TIME_SET,整店静默停签。
260
+ // 门店上的 Node 是 14,没有 crypto.X509Certificate,这里只解析需要的几个字段:
261
+ // 有效期(notBefore / notAfter)和 subject 里的 dnQualifier(区分第一代 / 第二代)。
262
+
263
+ const OID_DN_QUALIFIER = '2.5.4.46'
264
+
265
+ // 读一个 DER TLV,返回 tag、内容起止位置
266
+ function readTlv(buf, pos) {
267
+ const tag = buf[pos]
268
+ let len = buf[pos + 1]
269
+ let start = pos + 2
270
+ if (len & 0x80) {
271
+ const n = len & 0x7f
272
+ len = 0
273
+ for (let i = 0; i < n; i++) len = len * 256 + buf[pos + 2 + i]
274
+ start += n
275
+ }
276
+ return {tag, start, end: start + len}
277
+ }
278
+
279
+ function children(buf, tlv) {
280
+ const list = []
281
+ let pos = tlv.start
282
+ while (pos < tlv.end) {
283
+ const child = readTlv(buf, pos)
284
+ list.push(child)
285
+ pos = child.end
286
+ }
287
+ return list
288
+ }
289
+
290
+ function decodeOid(bytes) {
291
+ const parts = [Math.floor(bytes[0] / 40), bytes[0] % 40]
292
+ let value = 0
293
+ for (let i = 1; i < bytes.length; i++) {
294
+ value = value * 128 + (bytes[i] & 0x7f)
295
+ if (!(bytes[i] & 0x80)) {
296
+ parts.push(value)
297
+ value = 0
298
+ }
299
+ }
300
+ return parts.join('.')
301
+ }
302
+
303
+ // UTCTime(0x17,两位年份)或 GeneralizedTime(0x18)
304
+ function decodeTime(buf, tlv) {
305
+ const s = buf.slice(tlv.start, tlv.end).toString('ascii')
306
+ const full = tlv.tag === 0x17 ? (parseInt(s.slice(0, 2), 10) < 50 ? '20' : '19') + s : s
307
+ const m = full.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/)
308
+ return m ? `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}Z` : null
309
+ }
310
+
311
+ function parseCertificateDer(der) {
312
+ const cert = readTlv(der, 0)
313
+ const tbs = children(der, cert)[0]
314
+ const fields = children(der, tbs)
315
+ // 有显式 version([0])时整体后移一位:serialNumber、signature、issuer、validity、subject
316
+ const offset = fields[0].tag === 0xa0 ? 1 : 0
317
+ const [notBefore, notAfter] = children(der, fields[offset + 3])
318
+ let profile = null
319
+ for (const rdn of children(der, fields[offset + 4])) {
320
+ for (const attr of children(der, rdn)) {
321
+ const [oid, value] = children(der, attr)
322
+ if (decodeOid(der.slice(oid.start, oid.end)) === OID_DN_QUALIFIER) {
323
+ profile = der.slice(value.start, value.end).toString('utf-8')
324
+ }
325
+ }
326
+ }
327
+ return {
328
+ tseCertNotBefore: decodeTime(der, notBefore),
329
+ tseCertNotAfter: decodeTime(der, notAfter),
330
+ tseCertProfile: profile,
331
+ }
332
+ }
333
+
334
+ /**
335
+ * TseHost 的 /certificate 和门店库 RealTseInfo.certificate 都是「证书链 PEM 整体再 base64」,
336
+ * 第一张是 TSE 自己的证书,后面是 CA。也兼容直接给 PEM 或 DER 的情况。
337
+ */
338
+ function parseTseCertificate(encoded) {
339
+ const raw = String(encoded).trim()
340
+ const pem = raw.includes('-----BEGIN') ? raw : Buffer.from(raw, 'base64').toString('utf-8')
341
+ const block = pem.match(/-----BEGIN CERTIFICATE-----([\s\S]*?)-----END CERTIFICATE-----/)
342
+ const der = block ? Buffer.from(block[1].replace(/\s+/g, ''), 'base64') : Buffer.from(raw, 'base64')
343
+ return parseCertificateDer(der)
344
+ }
345
+
346
+ module.exports = {parseTseCertificate}
347
+
348
+
253
349
  /***/ }),
254
350
 
255
351
  /***/ 6677:
@@ -259,6 +355,8 @@ const fs = __nccwpck_require__(7147)
259
355
  const os = __nccwpck_require__(2037)
260
356
  const http = __nccwpck_require__(3685)
261
357
  const https = __nccwpck_require__(5687)
358
+ const {execFileSync} = __nccwpck_require__(2081)
359
+ const {parseTseCertificate} = __nccwpck_require__(8226)
262
360
 
263
361
  // do-log 顺带上报的 TSE 适配服务(TseHost)信息,用来在云端看车队的 TSE 版本和服务状态。
264
362
  // WSL 门店的 TseHost 跑在 Windows 侧,网站目录里的 jar 不一定是实际在跑的那个,
@@ -286,18 +384,18 @@ async function safe(fn, fallback = null) {
286
384
  }
287
385
  }
288
386
 
289
- function readTseSection() {
387
+ function readIniSection(name) {
290
388
  const section = {}
291
- let inTse = false
389
+ let inSection = false
292
390
  for (const raw of fs.readFileSync(CONFIG_INI, 'utf-8').split(/\r?\n/)) {
293
391
  const line = raw.trim()
294
392
  if (!line || line.startsWith(';') || line.startsWith('#')) continue
295
393
  const header = line.match(/^\[(.+)]$/)
296
394
  if (header) {
297
- inTse = header[1].trim() === 'TSE'
395
+ inSection = header[1].trim() === name
298
396
  continue
299
397
  }
300
- if (!inTse) continue
398
+ if (!inSection) continue
301
399
  const eq = line.indexOf('=')
302
400
  if (eq < 0) continue
303
401
  section[line.slice(0, eq).trim()] = line.slice(eq + 1).trim().replace(/^"(.*)"$/, '$1')
@@ -358,8 +456,42 @@ function servingJarPath() {
358
456
  return match ? match[1] : LINUX_DEFAULT_JAR
359
457
  }
360
458
 
459
+ // 门店库里最新的那块 TSE(换过 TSE 会有多行);只读数据库,TSE 坏了也读得到
460
+ function readCertFromDb() {
461
+ const db = readIniSection('DB')
462
+ const out = execFileSync('mysql', [
463
+ '-h', db.host || 'localhost', '-u', db.user || 'aaden', '-N', '-B', '--raw', db.name || 'aaden',
464
+ '-e', 'select serialNumber, certificate from RealTseInfo order by id desc limit 1',
465
+ ], {
466
+ encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'],
467
+ // 密码走环境变量,不出现在进程列表里
468
+ env: Object.assign({}, process.env, {MYSQL_PWD: db.password || ''}),
469
+ }).trim()
470
+ if (!out) return null
471
+ const [serial, certificate] = out.split('\t')
472
+ return {serial, certificate: certificate && certificate !== 'NULL' ? certificate : null}
473
+ }
474
+
475
+ // TSE 自己的序列号、证书有效期和型号(dnQualifier:BSI-DSZ-CC-1121 是第一代,-V2 是第二代)
476
+ async function collectCert(tseHost, serviceUp) {
477
+ const fromDb = await safe(readCertFromDb)
478
+ let certificate = fromDb && fromDb.certificate
479
+ // 库里还没存证书(新装、或从没取到过)时再问服务;服务连不上就算了
480
+ if (!certificate && serviceUp) {
481
+ const res = await httpGet(`${tseHost}/certificate`)
482
+ if (res.status === 200) certificate = await safe(() => JSON.parse(res.body).certificate)
483
+ }
484
+ const parsed = certificate ? await safe(() => parseTseCertificate(certificate), {}) : {}
485
+ return {
486
+ tseSerial: (fromDb && fromDb.serial) || null,
487
+ tseCertNotBefore: parsed.tseCertNotBefore || null,
488
+ tseCertNotAfter: parsed.tseCertNotAfter || null,
489
+ tseCertProfile: parsed.tseCertProfile || null,
490
+ }
491
+ }
492
+
361
493
  async function probeTse() {
362
- const section = await safe(readTseSection, {})
494
+ const section = await safe(() => readIniSection('TSE'), {})
363
495
  const tseEnabled = parseEnabled(section.TseEnabled)
364
496
  const tseHost = (section.TseHost || DEFAULT_TSE_HOST).replace(/\/+$/, '')
365
497
  // 不同版本的 jar 大小不同,没有 /adapterVersion 的老版本只能靠它区分
@@ -390,7 +522,9 @@ async function probeTse() {
390
522
  }
391
523
  }
392
524
 
393
- return {tseEnabled, tseHost, tseStatus, tseAdapterVersion, tseJarSize}
525
+ const cert = await collectCert(tseHost, ping.status !== 0)
526
+
527
+ return {tseEnabled, tseHost, tseStatus, tseAdapterVersion, tseJarSize, ...cert}
394
528
  }
395
529
 
396
530
  // 整体出错就什么都不报,云端保留上次的值
@@ -18729,7 +18863,7 @@ module.exports = JSON.parse('{"name":"axios","version":"0.19.2","description":"P
18729
18863
  /***/ ((module) => {
18730
18864
 
18731
18865
  "use strict";
18732
- module.exports = {"i8":"1.6.109"};
18866
+ module.exports = {"i8":"1.6.110"};
18733
18867
 
18734
18868
  /***/ })
18735
18869
 
@@ -0,0 +1,89 @@
1
+ // TSE 证书到期日:第一代 Swissbit TSE(BSI-DSZ-CC-1121)的证书只有 5.5 年,
2
+ // 到期当天起 TSE 拒绝对时,之后每笔交易都报 WORM_ERROR_NO_TIME_SET,整店静默停签。
3
+ // 门店上的 Node 是 14,没有 crypto.X509Certificate,这里只解析需要的几个字段:
4
+ // 有效期(notBefore / notAfter)和 subject 里的 dnQualifier(区分第一代 / 第二代)。
5
+
6
+ const OID_DN_QUALIFIER = '2.5.4.46'
7
+
8
+ // 读一个 DER TLV,返回 tag、内容起止位置
9
+ function readTlv(buf, pos) {
10
+ const tag = buf[pos]
11
+ let len = buf[pos + 1]
12
+ let start = pos + 2
13
+ if (len & 0x80) {
14
+ const n = len & 0x7f
15
+ len = 0
16
+ for (let i = 0; i < n; i++) len = len * 256 + buf[pos + 2 + i]
17
+ start += n
18
+ }
19
+ return {tag, start, end: start + len}
20
+ }
21
+
22
+ function children(buf, tlv) {
23
+ const list = []
24
+ let pos = tlv.start
25
+ while (pos < tlv.end) {
26
+ const child = readTlv(buf, pos)
27
+ list.push(child)
28
+ pos = child.end
29
+ }
30
+ return list
31
+ }
32
+
33
+ function decodeOid(bytes) {
34
+ const parts = [Math.floor(bytes[0] / 40), bytes[0] % 40]
35
+ let value = 0
36
+ for (let i = 1; i < bytes.length; i++) {
37
+ value = value * 128 + (bytes[i] & 0x7f)
38
+ if (!(bytes[i] & 0x80)) {
39
+ parts.push(value)
40
+ value = 0
41
+ }
42
+ }
43
+ return parts.join('.')
44
+ }
45
+
46
+ // UTCTime(0x17,两位年份)或 GeneralizedTime(0x18)
47
+ function decodeTime(buf, tlv) {
48
+ const s = buf.slice(tlv.start, tlv.end).toString('ascii')
49
+ const full = tlv.tag === 0x17 ? (parseInt(s.slice(0, 2), 10) < 50 ? '20' : '19') + s : s
50
+ const m = full.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/)
51
+ return m ? `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}Z` : null
52
+ }
53
+
54
+ function parseCertificateDer(der) {
55
+ const cert = readTlv(der, 0)
56
+ const tbs = children(der, cert)[0]
57
+ const fields = children(der, tbs)
58
+ // 有显式 version([0])时整体后移一位:serialNumber、signature、issuer、validity、subject
59
+ const offset = fields[0].tag === 0xa0 ? 1 : 0
60
+ const [notBefore, notAfter] = children(der, fields[offset + 3])
61
+ let profile = null
62
+ for (const rdn of children(der, fields[offset + 4])) {
63
+ for (const attr of children(der, rdn)) {
64
+ const [oid, value] = children(der, attr)
65
+ if (decodeOid(der.slice(oid.start, oid.end)) === OID_DN_QUALIFIER) {
66
+ profile = der.slice(value.start, value.end).toString('utf-8')
67
+ }
68
+ }
69
+ }
70
+ return {
71
+ tseCertNotBefore: decodeTime(der, notBefore),
72
+ tseCertNotAfter: decodeTime(der, notAfter),
73
+ tseCertProfile: profile,
74
+ }
75
+ }
76
+
77
+ /**
78
+ * TseHost 的 /certificate 和门店库 RealTseInfo.certificate 都是「证书链 PEM 整体再 base64」,
79
+ * 第一张是 TSE 自己的证书,后面是 CA。也兼容直接给 PEM 或 DER 的情况。
80
+ */
81
+ function parseTseCertificate(encoded) {
82
+ const raw = String(encoded).trim()
83
+ const pem = raw.includes('-----BEGIN') ? raw : Buffer.from(raw, 'base64').toString('utf-8')
84
+ const block = pem.match(/-----BEGIN CERTIFICATE-----([\s\S]*?)-----END CERTIFICATE-----/)
85
+ const der = block ? Buffer.from(block[1].replace(/\s+/g, ''), 'base64') : Buffer.from(raw, 'base64')
86
+ return parseCertificateDer(der)
87
+ }
88
+
89
+ module.exports = {parseTseCertificate}
@@ -2,6 +2,8 @@ const fs = require('fs')
2
2
  const os = require('os')
3
3
  const http = require('http')
4
4
  const https = require('https')
5
+ const {execFileSync} = require('child_process')
6
+ const {parseTseCertificate} = require('./tse-cert')
5
7
 
6
8
  // do-log 顺带上报的 TSE 适配服务(TseHost)信息,用来在云端看车队的 TSE 版本和服务状态。
7
9
  // WSL 门店的 TseHost 跑在 Windows 侧,网站目录里的 jar 不一定是实际在跑的那个,
@@ -29,18 +31,18 @@ async function safe(fn, fallback = null) {
29
31
  }
30
32
  }
31
33
 
32
- function readTseSection() {
34
+ function readIniSection(name) {
33
35
  const section = {}
34
- let inTse = false
36
+ let inSection = false
35
37
  for (const raw of fs.readFileSync(CONFIG_INI, 'utf-8').split(/\r?\n/)) {
36
38
  const line = raw.trim()
37
39
  if (!line || line.startsWith(';') || line.startsWith('#')) continue
38
40
  const header = line.match(/^\[(.+)]$/)
39
41
  if (header) {
40
- inTse = header[1].trim() === 'TSE'
42
+ inSection = header[1].trim() === name
41
43
  continue
42
44
  }
43
- if (!inTse) continue
45
+ if (!inSection) continue
44
46
  const eq = line.indexOf('=')
45
47
  if (eq < 0) continue
46
48
  section[line.slice(0, eq).trim()] = line.slice(eq + 1).trim().replace(/^"(.*)"$/, '$1')
@@ -101,8 +103,42 @@ function servingJarPath() {
101
103
  return match ? match[1] : LINUX_DEFAULT_JAR
102
104
  }
103
105
 
106
+ // 门店库里最新的那块 TSE(换过 TSE 会有多行);只读数据库,TSE 坏了也读得到
107
+ function readCertFromDb() {
108
+ const db = readIniSection('DB')
109
+ const out = execFileSync('mysql', [
110
+ '-h', db.host || 'localhost', '-u', db.user || 'aaden', '-N', '-B', '--raw', db.name || 'aaden',
111
+ '-e', 'select serialNumber, certificate from RealTseInfo order by id desc limit 1',
112
+ ], {
113
+ encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'],
114
+ // 密码走环境变量,不出现在进程列表里
115
+ env: Object.assign({}, process.env, {MYSQL_PWD: db.password || ''}),
116
+ }).trim()
117
+ if (!out) return null
118
+ const [serial, certificate] = out.split('\t')
119
+ return {serial, certificate: certificate && certificate !== 'NULL' ? certificate : null}
120
+ }
121
+
122
+ // TSE 自己的序列号、证书有效期和型号(dnQualifier:BSI-DSZ-CC-1121 是第一代,-V2 是第二代)
123
+ async function collectCert(tseHost, serviceUp) {
124
+ const fromDb = await safe(readCertFromDb)
125
+ let certificate = fromDb && fromDb.certificate
126
+ // 库里还没存证书(新装、或从没取到过)时再问服务;服务连不上就算了
127
+ if (!certificate && serviceUp) {
128
+ const res = await httpGet(`${tseHost}/certificate`)
129
+ if (res.status === 200) certificate = await safe(() => JSON.parse(res.body).certificate)
130
+ }
131
+ const parsed = certificate ? await safe(() => parseTseCertificate(certificate), {}) : {}
132
+ return {
133
+ tseSerial: (fromDb && fromDb.serial) || null,
134
+ tseCertNotBefore: parsed.tseCertNotBefore || null,
135
+ tseCertNotAfter: parsed.tseCertNotAfter || null,
136
+ tseCertProfile: parsed.tseCertProfile || null,
137
+ }
138
+ }
139
+
104
140
  async function probeTse() {
105
- const section = await safe(readTseSection, {})
141
+ const section = await safe(() => readIniSection('TSE'), {})
106
142
  const tseEnabled = parseEnabled(section.TseEnabled)
107
143
  const tseHost = (section.TseHost || DEFAULT_TSE_HOST).replace(/\/+$/, '')
108
144
  // 不同版本的 jar 大小不同,没有 /adapterVersion 的老版本只能靠它区分
@@ -133,7 +169,9 @@ async function probeTse() {
133
169
  }
134
170
  }
135
171
 
136
- return {tseEnabled, tseHost, tseStatus, tseAdapterVersion, tseJarSize}
172
+ const cert = await collectCert(tseHost, ping.status !== 0)
173
+
174
+ return {tseEnabled, tseHost, tseStatus, tseAdapterVersion, tseJarSize, ...cert}
137
175
  }
138
176
 
139
177
  // 整体出错就什么都不报,云端保留上次的值
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aaden-cli",
3
- "version": "1.6.109",
3
+ "version": "1.6.110",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {