aaden-cli 1.6.109-rc.0 → 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/AadenDeploy/Utils/enableNgrok.sh +21 -19
- package/AadenDeploy/Utils/fixCronJob.sh +5 -4
- package/AadenDeploy/aadensetup.sh +217 -163
- package/AadenDeploy/checkDeviceId.sh +104 -0
- package/AadenDeploy/setDeviceId.sh +130 -0
- package/README.md +2 -1
- package/dist/index.js +168 -13
- package/dist/patch/202608190700removeGitDoUpdateCron.sh +39 -0
- package/index.js +27 -6
- package/modules/tse-cert.js +89 -0
- package/modules/tse-info.js +44 -6
- package/package.json +2 -2
- package/patch/202608190700removeGitDoUpdateCron.sh +39 -0
- package/dist/patch/202304051415FixDatevMemory.sh +0 -19
- package/patch/202304051415FixDatevMemory.sh +0 -19
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# 设定本机设备号(= ERP 门店编号)。全新装机(aadensetup.sh)和镜像克隆来的机器改号都走这一个入口:
|
|
3
|
+
# aaden-cli set-device-id <号>
|
|
4
|
+
#
|
|
5
|
+
# 1. 先查云端:这个号是 ERP 里哪家店、是不是已经有收银机在用(checkDeviceId.sh),拦住撞号
|
|
6
|
+
# 2. 写配置:没有 config.ini 时让后端 AutoSetup.php 按默认配置生成;已有时只改 [Device] 段和 config.json,其余配置原样保留
|
|
7
|
+
# 3. 主机名 aadenXXXX,/etc/hosts 里只留一行(以前每次装机都往里追加一行)
|
|
8
|
+
# 4. ngrok 隧道 ikXXXX / SSH 端口 10000+号,重启隧道;第一次装时下载 ngrok 并装成服务
|
|
9
|
+
# 5. 马上上报一次,云端 ERP 门店列表的「机器」列随即显示就绪
|
|
10
|
+
#
|
|
11
|
+
# 退出码:0 成功;1 号不对或取消;2 写配置失败
|
|
12
|
+
export PATH="/usr/local/bin:$PATH"
|
|
13
|
+
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
14
|
+
CONFIG_DIR=/var/www/html/Resource/config
|
|
15
|
+
CONFIG_INI=$CONFIG_DIR/config.ini
|
|
16
|
+
CONFIG_JSON=$CONFIG_DIR/config.json
|
|
17
|
+
|
|
18
|
+
# 和 aaden-cli 其他地方一样用 -S 从 stdin 喂密码(aaden 不一定有免密 sudo)。
|
|
19
|
+
# 注意:不能再往 sudoS 的命令里用管道喂内容——stdin 已经被密码占了,内容一律先写临时文件再 sudoS cp。
|
|
20
|
+
sudoS() { echo "Innerken322." | sudo -S -p '' "$@"; }
|
|
21
|
+
# 先把 sudo 凭据缓存上:后面调的 aaden-cli 子命令(fix-ngrok 等)里还有裸 sudo
|
|
22
|
+
sudoS -v 2>/dev/null
|
|
23
|
+
|
|
24
|
+
NEW_ID="$1"
|
|
25
|
+
if [ -z "$NEW_ID" ]; then
|
|
26
|
+
read -r -p '设备号: ' NEW_ID
|
|
27
|
+
fi
|
|
28
|
+
NEW_ID=$(printf '%s' "$NEW_ID" | tr -d '[:space:]')
|
|
29
|
+
if ! [[ "$NEW_ID" =~ ^[0-9]+$ ]]; then
|
|
30
|
+
echo "设备号必须是数字:'$NEW_ID'"
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
NEW_ID=$((10#$NEW_ID)) # 去掉前导零,和云端、ERP 编号一致
|
|
34
|
+
NAME="IK$(printf '%04d' "$NEW_ID")"
|
|
35
|
+
HOST="aaden$(printf '%04d' "$NEW_ID")" # 和 ngrok 的 ik 号一样补到 4 位:aaden0001、aaden1958
|
|
36
|
+
|
|
37
|
+
ini_device_id() {
|
|
38
|
+
[ -f "$CONFIG_INI" ] || return
|
|
39
|
+
awk -F= '/^\[/ { s = $0 } s ~ /^\[Device\]/ && $1 ~ /^[ \t]*DeviceId[ \t]*$/ { gsub(/[ \t"\r]/, "", $2); print $2; exit }' "$CONFIG_INI"
|
|
40
|
+
}
|
|
41
|
+
OLD_ID=$(ini_device_id)
|
|
42
|
+
|
|
43
|
+
if [ "$OLD_ID" = "$NEW_ID" ]; then
|
|
44
|
+
echo "本机已经是设备号 $NEW_ID,只重新应用主机名、ngrok 并上报"
|
|
45
|
+
else
|
|
46
|
+
[ -n "$OLD_ID" ] && echo "本机原设备号:$OLD_ID → 改成 $NEW_ID"
|
|
47
|
+
bash "$DIR/checkDeviceId.sh" "$NEW_ID" || exit 1
|
|
48
|
+
|
|
49
|
+
# 已经启用 TSE 的机器改号:TSE 记录会挂到新号上。正常流程是先改号,再「清空数据、启用 TSE」
|
|
50
|
+
if [ -n "$OLD_ID" ] && grep -qiE '^[ \t]*TseEnabled[ \t]*=[ \t]*"?(true|1)"?[ \t]*$' "$CONFIG_INI"; then
|
|
51
|
+
echo "⚠ 本机已经启用 TSE(原设备号 $OLD_ID)。正常应该先改号,再清空数据、启用 TSE。"
|
|
52
|
+
answer=""
|
|
53
|
+
{ exec 3</dev/tty; } 2>/dev/null && read -r -p "确定要改号请输入 yes:" answer <&3 2>/dev/tty
|
|
54
|
+
[ "$answer" = "yes" ] || { echo "已取消。"; exit 1; }
|
|
55
|
+
fi
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------- 配置
|
|
59
|
+
if [ ! -f "$CONFIG_INI" ]; then
|
|
60
|
+
# 全新装机:后端按默认配置生成 config.ini / config.json(以 www-data 写,权限和以前一样)
|
|
61
|
+
curl -fsS -m 30 "http://localhost/PHP/AutoSetup.php?DeviceId=$NEW_ID" >/dev/null \
|
|
62
|
+
|| { echo "调用 AutoSetup.php 失败(Apache/PHP 还没起来?)"; exit 2; }
|
|
63
|
+
elif [ "$OLD_ID" != "$NEW_ID" ]; then
|
|
64
|
+
TMP=$(mktemp)
|
|
65
|
+
awk -v id="$NEW_ID" -v name="$NAME" '
|
|
66
|
+
function fill() {
|
|
67
|
+
if (!doneId) print "DeviceId = \"" id "\""
|
|
68
|
+
if (!doneName) print "DeviceName = \"" name "\""
|
|
69
|
+
}
|
|
70
|
+
/^\[/ {
|
|
71
|
+
if (inDev) fill()
|
|
72
|
+
inDev = ($0 ~ /^\[Device\][ \t\r]*$/)
|
|
73
|
+
if (inDev) seen = 1
|
|
74
|
+
print; next
|
|
75
|
+
}
|
|
76
|
+
inDev && /^[ \t]*DeviceId[ \t]*=/ { print "DeviceId = \"" id "\""; doneId = 1; next }
|
|
77
|
+
inDev && /^[ \t]*DeviceName[ \t]*=/ { print "DeviceName = \"" name "\""; doneName = 1; next }
|
|
78
|
+
{ print }
|
|
79
|
+
END {
|
|
80
|
+
if (inDev) fill()
|
|
81
|
+
if (!seen) { print "[Device]"; print "DeviceId = \"" id "\""; print "DeviceName = \"" name "\"" }
|
|
82
|
+
}' "$CONFIG_INI" >"$TMP"
|
|
83
|
+
# cp 覆盖内容、保留原文件的属主和权限
|
|
84
|
+
sudoS cp "$TMP" "$CONFIG_INI" || { rm -f "$TMP"; echo "写 $CONFIG_INI 失败"; exit 2; }
|
|
85
|
+
printf '{"DeviceId":"%s","DeviceName":"%s"}' "$NEW_ID" "$NAME" >"$TMP"
|
|
86
|
+
sudoS cp "$TMP" "$CONFIG_JSON"
|
|
87
|
+
rm -f "$TMP"
|
|
88
|
+
fi
|
|
89
|
+
if [ "$(ini_device_id)" != "$NEW_ID" ]; then
|
|
90
|
+
echo "写完配置后读到的设备号是「$(ini_device_id)」,不是 $NEW_ID"
|
|
91
|
+
exit 2
|
|
92
|
+
fi
|
|
93
|
+
echo "配置已写入:$CONFIG_INI → DeviceId = $NEW_ID"
|
|
94
|
+
|
|
95
|
+
# ---------------------------------------------------------------- 主机名
|
|
96
|
+
# 先写 /etc/hosts 再改主机名,否则改完的那一刻 sudo 会报 unable to resolve host
|
|
97
|
+
TMP=$(mktemp)
|
|
98
|
+
grep -vE '[[:space:]]aaden[0-9]+[[:space:]]*$' /etc/hosts >"$TMP"
|
|
99
|
+
echo "127.0.1.1 $HOST" >>"$TMP"
|
|
100
|
+
sudoS cp "$TMP" /etc/hosts
|
|
101
|
+
rm -f "$TMP"
|
|
102
|
+
# WSL 没有 systemd 时 hostnamectl 不可用,退回直接写 /etc/hostname
|
|
103
|
+
if ! sudoS hostnamectl set-hostname "$HOST" 2>/dev/null; then
|
|
104
|
+
TMP=$(mktemp)
|
|
105
|
+
echo "$HOST" >"$TMP"
|
|
106
|
+
sudoS cp "$TMP" /etc/hostname && sudoS hostname "$HOST"
|
|
107
|
+
rm -f "$TMP"
|
|
108
|
+
fi
|
|
109
|
+
echo "主机名:$HOST"
|
|
110
|
+
|
|
111
|
+
# ---------------------------------------------------------------- ngrok
|
|
112
|
+
aaden-cli update-ngrok-config "$NEW_ID" >/dev/null
|
|
113
|
+
if [ -f /etc/systemd/system/aaden-ngrok-v3.service ] && [ -x /etc/aaden/ngrok ]; then
|
|
114
|
+
sudoS systemctl restart aaden-ngrok-v3 2>/dev/null || sudoS service aaden-ngrok-v3 restart
|
|
115
|
+
else
|
|
116
|
+
aaden-cli fix-ngrok # 第一次:下载 ngrok、装 systemd 服务
|
|
117
|
+
fi
|
|
118
|
+
echo "ngrok:ik$(printf '%04d' "$NEW_ID").ngrok.aaden.io,SSH 端口 $((10000 + NEW_ID))"
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------- 上报
|
|
121
|
+
# do-log 连不上云端时退出码也是 0,所以回头问云端确认收到了没有
|
|
122
|
+
aaden-cli do-log >/dev/null 2>&1
|
|
123
|
+
MINUTES=$(curl -fsS -m 15 "${AADEN_CLOUD_URL:-https://cloud-v2.aaden.io}/deviceLog/install-check/$NEW_ID" 2>/dev/null \
|
|
124
|
+
| python3 -c 'import json, sys; m = json.load(sys.stdin).get("lastReportMinutesAgo"); print("" if m is None else m)' 2>/dev/null)
|
|
125
|
+
if [ -n "$MINUTES" ] && [ "$MINUTES" -le 5 ]; then
|
|
126
|
+
echo "云端已收到本机上报,ERP 门店列表的「机器」列显示就绪"
|
|
127
|
+
else
|
|
128
|
+
echo "⚠ 云端还没收到本机上报(网络不通?)。联网后每 5 分钟会自动上报一次"
|
|
129
|
+
fi
|
|
130
|
+
exit 0
|
package/README.md
CHANGED
|
@@ -28,7 +28,8 @@ Options:
|
|
|
28
28
|
|
|
29
29
|
Commands:
|
|
30
30
|
update [channel]
|
|
31
|
-
install [options] <deviceId> [channel]
|
|
31
|
+
install [options] <deviceId> [channel] 装机(= AadenDeploy/aadensetup.sh):撞号检查、装软件、部署后端、设备号、自检并上报
|
|
32
|
+
set-device-id [deviceId] 设定本机设备号:撞号检查、config.ini、主机名、ngrok、立即上报(镜像克隆的机器改号也用它)
|
|
32
33
|
setup-wifi
|
|
33
34
|
upgrade
|
|
34
35
|
where
|
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
|
|
387
|
+
function readIniSection(name) {
|
|
290
388
|
const section = {}
|
|
291
|
-
let
|
|
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
|
-
|
|
395
|
+
inSection = header[1].trim() === name
|
|
298
396
|
continue
|
|
299
397
|
}
|
|
300
|
-
if (!
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
18866
|
+
module.exports = {"i8":"1.6.110"};
|
|
18733
18867
|
|
|
18734
18868
|
/***/ })
|
|
18735
18869
|
|
|
@@ -18843,6 +18977,8 @@ program.command('rollback').action(rollback)
|
|
|
18843
18977
|
program
|
|
18844
18978
|
.command('install <deviceId>')
|
|
18845
18979
|
.action(install)
|
|
18980
|
+
// 设定本机设备号(= ERP 门店编号):撞号检查 + config.ini + 主机名 + ngrok + 立即上报。装机和镜像克隆来的机器改号都用它
|
|
18981
|
+
program.command('set-device-id [deviceId]').action(setDeviceId)
|
|
18846
18982
|
program.command('remove-all-data').action(removeAllData)
|
|
18847
18983
|
program.command('mount-tse').action(remountTSE)
|
|
18848
18984
|
program.command('upgrade').action(upgrade) // auto execute.
|
|
@@ -19110,12 +19246,13 @@ async function updateNgrokConfig(deviceId) {
|
|
|
19110
19246
|
console.log('Start to config Ngrok service')
|
|
19111
19247
|
console.log('开始配置' + deviceId + '设备的Ngrok服务')
|
|
19112
19248
|
|
|
19249
|
+
// 和其他地方一样用 -S 喂密码:aaden 没有免密 sudo、又没有终端时(set-device-id 被脚本调用)裸 sudo 会直接失败
|
|
19113
19250
|
const command = [
|
|
19114
19251
|
'cd /etc',
|
|
19115
|
-
'sudo mkdir -p aaden',
|
|
19252
|
+
'echo "Innerken322." | sudo -S -p \'\' mkdir -p aaden',
|
|
19116
19253
|
'cd aaden',
|
|
19117
|
-
'sudo touch ngrok-config.yml',
|
|
19118
|
-
'sudo chmod 777 ngrok-config.yml',
|
|
19254
|
+
'echo "Innerken322." | sudo -S -p \'\' touch ngrok-config.yml',
|
|
19255
|
+
'echo "Innerken322." | sudo -S -p \'\' chmod 777 ngrok-config.yml',
|
|
19119
19256
|
`echo "
|
|
19120
19257
|
server_addr: ngrok.aaden.io:4443
|
|
19121
19258
|
trust_host_root_certs: true
|
|
@@ -19527,11 +19664,29 @@ async function install(deviceId) {
|
|
|
19527
19664
|
'cd AadenDeploy',
|
|
19528
19665
|
`./aadensetup.sh ${deviceId} ${channel}` + (installApp ? ' b' : '')
|
|
19529
19666
|
]
|
|
19530
|
-
|
|
19667
|
+
// 「出厂安装」事件由 aadensetup.sh 自己在最后上报(带每一步的结果和自检),这里不再重复记
|
|
19668
|
+
const {code} = await cli.executeInShell(command, program.debug)
|
|
19669
|
+
process.exitCode = code
|
|
19531
19670
|
} else {
|
|
19532
19671
|
console.log('Device Id is invalid.')
|
|
19533
19672
|
}
|
|
19534
|
-
|
|
19673
|
+
}
|
|
19674
|
+
|
|
19675
|
+
async function setDeviceId(deviceId) {
|
|
19676
|
+
const oldId = getMyDeviceId()
|
|
19677
|
+
const command = [
|
|
19678
|
+
...getPrivilegeCMD('AadenDeploy/setDeviceId.sh'),
|
|
19679
|
+
'cd AadenDeploy',
|
|
19680
|
+
`./setDeviceId.sh ${deviceId && deviceId !== 'notset' ? deviceId : ''}`
|
|
19681
|
+
]
|
|
19682
|
+
const {code} = await cli.executeInShell(command, program.debug)
|
|
19683
|
+
// aadensetup.sh 按退出码判断这一步成没成
|
|
19684
|
+
process.exitCode = code
|
|
19685
|
+
if (code !== 0) {
|
|
19686
|
+
return
|
|
19687
|
+
}
|
|
19688
|
+
const newId = getMyDeviceId()
|
|
19689
|
+
await logAction(String(oldId) === String(newId) ? `重新应用设备号 ${newId}` : `设定设备号 ${oldId === -1 ? '(无)' : oldId} → ${newId}`)
|
|
19535
19690
|
}
|
|
19536
19691
|
|
|
19537
19692
|
function getMyDeviceId() {
|
|
@@ -19709,7 +19864,7 @@ function getPrivilegeCMD(path, pri = '+x', relativePath = true) {
|
|
|
19709
19864
|
if (relativePath) {
|
|
19710
19865
|
command.push(goIntoRootDir())
|
|
19711
19866
|
}
|
|
19712
|
-
command.push(`echo "Innerken322." | sudo -S chmod ${pri} ${path}`)
|
|
19867
|
+
command.push(`echo "Innerken322." | sudo -S -p "" chmod ${pri} ${path}`)
|
|
19713
19868
|
return command
|
|
19714
19869
|
}
|
|
19715
19870
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# 一次性修复:摘掉老 git 通道的 doUpdate cron。
|
|
3
|
+
#
|
|
4
|
+
# 病症:设备 crontab 里有
|
|
5
|
+
# 59 4 * * * curl -X POST 'http://localhost/PHP/UpdateSelf.php?op=doUpdate'
|
|
6
|
+
# @reboot ... curl -X POST '.../UpdateSelf.php?op=doUpdate'
|
|
7
|
+
# 它每天 04:59(和 @reboot)从**设备本地陈旧 gitCache** 部署后端;而 git fetch 因 deploy key
|
|
8
|
+
# 未分发到全网而失败(FETCH_HEAD 0 字节),缓存卡在旧版本;git 通道又没有"绝不降级"保护 →
|
|
9
|
+
# 于是把 light-update 刚更好的新版本降级回退(实测 1.9.63 → 1.9.62/1.9.61,每天来回打架)。
|
|
10
|
+
#
|
|
11
|
+
# 结论:light-update(S3/code.zip 通道)已是后端唯一活着的更新通道,且自带"只升不降"版本门。
|
|
12
|
+
# 摘掉 doUpdate cron,终结两条通道打架。crontab 不走 git/S3 部署 → 改了就稳。
|
|
13
|
+
#
|
|
14
|
+
# 幂等:没有该行就不动;grep -vF 精确匹配整串,不误伤 crontab.sh / light-update 那几行。
|
|
15
|
+
set -u
|
|
16
|
+
|
|
17
|
+
MARK='UpdateSelf.php?op=doUpdate'
|
|
18
|
+
|
|
19
|
+
strip_for() { # $1 = 目标用户(空=当前用户)
|
|
20
|
+
local u="$1" cur new
|
|
21
|
+
local args=()
|
|
22
|
+
[ -n "$u" ] && args=(-u "$u")
|
|
23
|
+
cur=$(crontab "${args[@]}" -l 2>/dev/null) || return 0
|
|
24
|
+
printf '%s\n' "$cur" | grep -qF "$MARK" || return 0 # 没有该 cron → 不动
|
|
25
|
+
new=$(printf '%s\n' "$cur" | grep -vF "$MARK")
|
|
26
|
+
printf '%s\n' "$new" | crontab "${args[@]}" - \
|
|
27
|
+
&& echo " 已从 ${u:-当前用户} 的 crontab 摘除 doUpdate cron"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# doUpdate cron 在 aaden 用户的 crontab。patch 正常由 root 的 auto-upgrade 触发 → 用 -u aaden;
|
|
31
|
+
# 兜底再扫一遍当前用户(万一某些设备以 aaden 直接跑 patch),谁有就摘谁。
|
|
32
|
+
if [ "$(id -u)" = "0" ]; then
|
|
33
|
+
strip_for aaden
|
|
34
|
+
strip_for ""
|
|
35
|
+
else
|
|
36
|
+
strip_for ""
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
echo " removeGitDoUpdateCron 完成(light-update 成为后端唯一更新通道)"
|
package/index.js
CHANGED
|
@@ -40,6 +40,8 @@ program.command('rollback').action(rollback)
|
|
|
40
40
|
program
|
|
41
41
|
.command('install <deviceId>')
|
|
42
42
|
.action(install)
|
|
43
|
+
// 设定本机设备号(= ERP 门店编号):撞号检查 + config.ini + 主机名 + ngrok + 立即上报。装机和镜像克隆来的机器改号都用它
|
|
44
|
+
program.command('set-device-id [deviceId]').action(setDeviceId)
|
|
43
45
|
program.command('remove-all-data').action(removeAllData)
|
|
44
46
|
program.command('mount-tse').action(remountTSE)
|
|
45
47
|
program.command('upgrade').action(upgrade) // auto execute.
|
|
@@ -307,12 +309,13 @@ async function updateNgrokConfig(deviceId) {
|
|
|
307
309
|
console.log('Start to config Ngrok service')
|
|
308
310
|
console.log('开始配置' + deviceId + '设备的Ngrok服务')
|
|
309
311
|
|
|
312
|
+
// 和其他地方一样用 -S 喂密码:aaden 没有免密 sudo、又没有终端时(set-device-id 被脚本调用)裸 sudo 会直接失败
|
|
310
313
|
const command = [
|
|
311
314
|
'cd /etc',
|
|
312
|
-
'sudo mkdir -p aaden',
|
|
315
|
+
'echo "Innerken322." | sudo -S -p \'\' mkdir -p aaden',
|
|
313
316
|
'cd aaden',
|
|
314
|
-
'sudo touch ngrok-config.yml',
|
|
315
|
-
'sudo chmod 777 ngrok-config.yml',
|
|
317
|
+
'echo "Innerken322." | sudo -S -p \'\' touch ngrok-config.yml',
|
|
318
|
+
'echo "Innerken322." | sudo -S -p \'\' chmod 777 ngrok-config.yml',
|
|
316
319
|
`echo "
|
|
317
320
|
server_addr: ngrok.aaden.io:4443
|
|
318
321
|
trust_host_root_certs: true
|
|
@@ -724,11 +727,29 @@ async function install(deviceId) {
|
|
|
724
727
|
'cd AadenDeploy',
|
|
725
728
|
`./aadensetup.sh ${deviceId} ${channel}` + (installApp ? ' b' : '')
|
|
726
729
|
]
|
|
727
|
-
|
|
730
|
+
// 「出厂安装」事件由 aadensetup.sh 自己在最后上报(带每一步的结果和自检),这里不再重复记
|
|
731
|
+
const {code} = await cli.executeInShell(command, program.debug)
|
|
732
|
+
process.exitCode = code
|
|
728
733
|
} else {
|
|
729
734
|
console.log('Device Id is invalid.')
|
|
730
735
|
}
|
|
731
|
-
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
async function setDeviceId(deviceId) {
|
|
739
|
+
const oldId = getMyDeviceId()
|
|
740
|
+
const command = [
|
|
741
|
+
...getPrivilegeCMD('AadenDeploy/setDeviceId.sh'),
|
|
742
|
+
'cd AadenDeploy',
|
|
743
|
+
`./setDeviceId.sh ${deviceId && deviceId !== 'notset' ? deviceId : ''}`
|
|
744
|
+
]
|
|
745
|
+
const {code} = await cli.executeInShell(command, program.debug)
|
|
746
|
+
// aadensetup.sh 按退出码判断这一步成没成
|
|
747
|
+
process.exitCode = code
|
|
748
|
+
if (code !== 0) {
|
|
749
|
+
return
|
|
750
|
+
}
|
|
751
|
+
const newId = getMyDeviceId()
|
|
752
|
+
await logAction(String(oldId) === String(newId) ? `重新应用设备号 ${newId}` : `设定设备号 ${oldId === -1 ? '(无)' : oldId} → ${newId}`)
|
|
732
753
|
}
|
|
733
754
|
|
|
734
755
|
function getMyDeviceId() {
|
|
@@ -906,7 +927,7 @@ function getPrivilegeCMD(path, pri = '+x', relativePath = true) {
|
|
|
906
927
|
if (relativePath) {
|
|
907
928
|
command.push(goIntoRootDir())
|
|
908
929
|
}
|
|
909
|
-
command.push(`echo "Innerken322." | sudo -S chmod ${pri} ${path}`)
|
|
930
|
+
command.push(`echo "Innerken322." | sudo -S -p "" chmod ${pri} ${path}`)
|
|
910
931
|
return command
|
|
911
932
|
}
|
|
912
933
|
|
|
@@ -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}
|