Compare commits

...
52 Commits
Author SHA1 Message Date
cheney 4207d6e467 更新 cli 命令
TDevOPsCICD / build-image (push) Waiting to run
2026-06-17 10:31:26 +08:00
cheney a7f596aa0a 调试通过 2026-06-17 10:25:27 +08:00
cheney 364efbc1c1 修改 meta.json 规则, 涉及version 、group 字段. 2026-06-17 10:03:51 +08:00
cheney fc81584878 上传成功,准备修改路径 2026-06-17 09:30:22 +08:00
cheney 0132be7586 添加 docker runsh 命令 2026-06-17 09:13:30 +08:00
cheney e05ce19eac 整理远程模块
TDevOPsCICD / build-image (push) Waiting to run
2026-06-16 16:40:34 +08:00
cheney b46cddc7cf 完善 cicd 命令
TDevOPsCICD / build-image (push) Waiting to run
2026-06-16 09:59:46 +08:00
cheney 98de345c5f 打包过程不顺利
TDevOPsCICD / build-image (push) Has been cancelled
2026-04-04 20:57:28 +08:00
cheney b46258d014 集成到 cli 2026-04-04 20:40:52 +08:00
cheney 05e8de9686 完善验签逻辑 2026-04-04 20:34:09 +08:00
cheney b73d9b0aac 测试通过
TDevOPsCICD / build-image (push) Waiting to run
2026-04-04 18:24:03 +08:00
cheney 03169b5165 验证失败 2026-04-04 18:06:09 +08:00
cheney 959f058def sm2 验签不通过 2026-04-04 17:54:13 +08:00
cheney f9d9203349 sm2 验签不通过 2026-04-04 17:45:53 +08:00
cheney 6e95e8a1b1 sm2 验签通过 2026-04-04 17:29:00 +08:00
cheney 9d826fb985 rsa p7 验签通过 2026-04-04 08:32:06 +08:00
cheney 5a033318c0 rsa p7 info 通过 2026-04-04 08:24:27 +08:00
cheney e97d7d7fd0 添加快速发布
TDevOPsCICD / build-image (push) Has been cancelled
2026-01-27 17:10:20 +08:00
cheney 24961dc734 优化显示
TDevOPsCICD / build-image (push) Waiting to run
2026-01-27 16:59:57 +08:00
cheney 6ddcff5275 build info
TDevOPsCICD / build-image (push) Waiting to run
2026-01-27 16:12:31 +08:00
cheney d5e12945e1 修正 platform 命令 2026-01-27 15:59:24 +08:00
cheney 714572994a linux 打包方式回到最老的方式
TDevOPsCICD / build-image (push) Waiting to run
2026-01-27 14:06:16 +08:00
cheney a226bc65dd net scan
TDevOPsCICD / build-image (push) Waiting to run
2026-01-27 10:13:12 +08:00
cheney 5a5bb80927 :7 testci
TDevOPsCICD / build-image (push) Failing after 4m49s
2025-10-22 16:01:12 +08:00
cheney e6c264f226 :6 testci
TDevOPsCICD / build-image (push) Failing after 6m42s
2025-10-22 15:46:15 +08:00
cheney 94709a25e6 :5 testci
TDevOPsCICD / build-image (push) Failing after 1m21s
2025-10-22 15:41:15 +08:00
cheney 38cf2d53d8 :4 testci
TDevOPsCICD / build-image (push) Successful in 1m28s
2025-10-22 15:05:02 +08:00
cheney 9cef28b135 :3 ci
TDevOPsCICD / build-image (push) Failing after 6m41s
2025-10-22 14:51:56 +08:00
cheney 0e15380aa3 :2 testci
TDevOPsCICD / build-image (push) Failing after 52s
2025-10-22 14:42:00 +08:00
cheney 9c62274108 :1 test ci
TDevOPsCICD / build-image (push) Failing after 1m22s
2025-10-22 14:39:24 +08:00
Cheney 0ca130f8e8 测试 CICD
TDevOPsCICD / build-image (push) Successful in 56s
2025-04-23 15:55:54 +08:00
Cheney 25f48f8ede 测试 CICD
TDevOPsCICD / build-image (push) Waiting to run
2025-04-23 15:54:37 +08:00
Cheney a849c280f7 测试 CICD
TDevOPsCICD / build-image (push) Failing after 30s
2025-04-23 15:39:57 +08:00
Cheney b99b0b8299 测试 CICD
TDevOPsCICD / build-image (push) Failing after 30s
2025-04-23 15:10:18 +08:00
Cheney 60c1e61f73 测试 CICD
TDevOPsCICD / lint-and-test (18.x) (push) Has been cancelled
TDevOPsCICD / lint-and-test (20.x) (push) Has been cancelled
TDevOPsCICD / build-image (push) Has been cancelled
2025-04-16 15:34:09 +08:00
Cheney c2119665fd remote 命令 2025-03-13 16:02:37 +08:00
Cheney 658a7031b6 remote 命令 2025-03-13 15:48:22 +08:00
Cheney 7eccf1bf15 remote 命令 2025-03-13 15:37:01 +08:00
Cheney ecff0496cb remote 命令 2025-03-13 15:31:16 +08:00
Cheney d05bb4cfe6 remote 命令 2025-03-13 15:27:23 +08:00
Cheney 37af0bd7a6 修改配置路径 2025-03-13 15:16:30 +08:00
Cheney 5ff621c05b publish 功能 2025-03-13 11:27:11 +08:00
Cheney 5f5e73e1c9 更新 platform 打印格式 2025-03-12 11:23:44 +08:00
Cheney 77e610b38b 添加脚本和 meta 2025-03-12 10:27:24 +08:00
Cheney 97a76195c2 add os release 2025-03-11 17:29:48 +08:00
Cheney 8149846f16 git problem 2025-03-11 16:50:44 +08:00
Cheney 5713e9e663 update 2025-03-11 14:59:31 +08:00
Cheney 4b6508a606 run 命令试用 2024-09-03 15:40:58 +08:00
Cheney 4275120cba 添加 freem 命令 2024-09-02 17:37:31 +08:00
Cheney 2acd92086c 恢复一个误修改 2024-09-02 17:36:34 +08:00
Cheney 5fd50792a0 添加 kit 子目录 2024-03-07 15:50:13 +08:00
Cheney e0c2ab8c1e init 2024-03-07 15:48:17 +08:00
83 changed files with 8999 additions and 132 deletions
+34
View File
@@ -0,0 +1,34 @@
name: TDevOPsCICD
on: [push]
jobs:
build-image:
runs-on: workbench
steps:
- uses: https://gitea.com/actions/checkout@v4
- name: Build Binary
env:
OBJECT_TOKEN: ${{ secrets.OBJECT_TOKEN }} # 需要在Gitea项目设置中配置的密钥
GITEA_BASE_URL: http://git.honor3.com # 你的Gitea实例地址
PACKAGE_OWNER: Fullstack # 仓库所有者用户名
REPO_NAME: kit.program # 仓库名
PACKAGE_NAME: kit
PACKAGE_VERSION: ${{ github.ref_name }} # 使用分支名或标签作为版本
run: |
echo ----------------------------------------
echo ${{ github.ref_name }} ${{ github.sha }}
bun -v
ping -c 3 git.honor3.com
cd kit
bun i
bun run windows
curl -X POST \
-H "Authorization: token $OBJECT_TOKEN" \
-F "data=@./dist/windows/kit.exe" \
"$GITEA_BASE_URL/api/packages/$PACKAGE_OWNER/generic/$REPO_NAME/$PACKAGE_VERSION/kit.exe"
bun run linux
curl -X POST \
-H "Authorization: token $OBJECT_TOKEN" \
-F "data=@./dist/linux/kit" \
"$GITEA_BASE_URL/api/packages/$PACKAGE_OWNER/generic/$REPO_NAME/$PACKAGE_VERSION/kit"
+3 -1
View File
@@ -1,2 +1,4 @@
node_modules
dist
dist
build.json
kit/.kit/modules/*
+4 -1
View File
@@ -2,7 +2,10 @@
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/website/.docusaurus" />
<excludeFolder url="file://$MODULE_DIR$/website/build" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
+68
View File
@@ -0,0 +1,68 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="AliAccessStaticViaInstance" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliArrayNamingShouldHaveBracket" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliControlFlowStatementWithoutBraces" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliDeprecation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliEqualsAvoidNull" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliLongLiteralsEndingWithLowercaseL" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliMissingOverrideAnnotation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliWrapperTypeEquality" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAbstractClassShouldStartWithAbstractNaming" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAbstractMethodOrInterfaceMethodMustUseJavadoc" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidApacheBeanUtilsCopy" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidCallStaticSimpleDateFormat" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidCommentBehindStatement" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidComplexCondition" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidConcurrentCompetitionRandom" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidDoubleOrFloatEqualCompare" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidManuallyCreateThread" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidMissUseOfMathRandom" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidNegationOperator" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidNewDateGetTime" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidPatternCompileInMethod" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidReturnInFinally" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidStartWithDollarAndUnderLineNaming" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidUseTimer" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaBigDecimalAvoidDoubleConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaBooleanPropertyShouldNotStartWithIs" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaClassCastExceptionWithSubListToArrayList" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaClassCastExceptionWithToArray" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaClassMustHaveAuthor" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaClassNamingShouldBeCamel" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaCollectionInitShouldAssignCapacity" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaCommentsMustBeJavadocFormat" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaConcurrentExceptionWithModifyOriginSubList" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaConstantFieldShouldBeUpperCase" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaCountDownShouldInFinally" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaDontModifyInForeachCircle" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaEnumConstantsMustHaveComment" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaExceptionClassShouldEndWithException" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaIbatisMethodQueryForList" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaLockShouldWithTryFinally" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaLowerCamelCaseVariableNaming" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaMethodReturnWrapperType" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaMethodTooLong" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaPackageNaming" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaPojoMustOverrideToString" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaPojoMustUsePrimitiveField" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaPojoNoDefaultValue" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaRemoveCommentedCode" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaServiceOrDaoClassShouldEndWithImpl" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaSneakyThrowsWithoutExceptionType" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaStringConcat" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaSwitchExpression" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaSwitchStatement" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaTestClassShouldEndWithTestNaming" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaThreadLocalShouldRemove" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaThreadPoolCreation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaThreadShouldSetName" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaTransactionMustHaveRollback" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaUndefineMagicConstant" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaUnsupportedExceptionWithModifyAsList" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaUseQuietReferenceNotation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaUseRightCaseForDateFormat" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="MapOrSetKeyShouldOverrideHashCodeEquals" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>
+9
View File
@@ -0,0 +1,9 @@
# Readme
+109 -109
View File
@@ -1,110 +1,110 @@
const CmdLine = require("./CmdLine")
const CliCmdLine = require("./CliCmdLine")
const Config = require("./config")
const gConfig = new Config();
module.exports = class CliCmdParser {
constructor(name) {
// 软件名称
this.name = name
this.cmdLines = {}
this.funcs = {}
}
/**
* 声明一条命令
* @param cmdLine 命令模板
* @param info 命令说明
* @param func 响应函数
*/
addCmdLine(cmdLine, info, func) {
let cl = CmdLine.parse(cmdLine, info);
if( this.cmdLines[ cl.cmd ] ) {
throw "重复注册的命令 " + cl.cmd
}
this.cmdLines[ cl.cmd ] = cl;
if ( func ) {
this.funcs[ cl.cmd ] = func;
}
return cl;
}
/**
* 打印命令帮助
*/
help(){
let bs = ""
if( this.name ) {
bs += this.name + gConfig.crlf
}
for ( let cmd in this.cmdLines ) {
bs = bs.concat("\t")
.concat( this.cmdLines[ cmd ].line )
.concat("\t")
.concat( this.cmdLines[ cmd ].info )
.concat( gConfig.crlf );
}
return bs;
}
canEval( cliCmdLine ){
return !! this.funcs[ cliCmdLine.cmdline.cmd ];
}
async eval( cliCmdLine ){
let func = this.funcs[ cliCmdLine.cmdline.cmd ];
if ( null == func ) {
throw "没找到命令相应函数";
}
return await func( cliCmdLine );
}
parse(argv){
if ( ! argv || argv.length < 1 ) {
return null;
}
if ( argv[0].startsWith("-") ) {
throw "入参命令格式错误 : 没有入参 cmd"
}
if ( ! this.cmdLines[ argv[0] ] ) {
return null;
}
// 标记: 去除 - 之后的值
const flags = new Set([])
// 命名参数: 去除 -- 之后的值为 key,之后值为 value
const params = {}
// 位置参数:值
const position = []
for (let i = 1; i < argv.length; i++ ) {
if ( argv[i].startsWith("--") ) {
if ( i + 1 == argv.length || argv[ i + 1 ].startsWith("-") ) {
throw "flag 格式错误" + argv[i];
}
params[ argv[i].substring(2) ] = argv[++i];
} else if ( argv[i].startsWith("-") ) {
flags.add( argv[i].substring(1) );
} else {
position.push( argv[i] );
}
}
let line = this.cmdLines[ argv[0] ];
// 整理
// line 是藐视
// flags, params, position 是值
return new CliCmdLine( this, line, flags, params, position);
}
const CmdLine = require("./CmdLine")
const CliCmdLine = require("./CliCmdLine")
const Config = require("./Config")
const gConfig = new Config();
module.exports = class CliCmdParser {
constructor(name) {
// 软件名称
this.name = name
this.cmdLines = {}
this.funcs = {}
}
/**
* 声明一条命令
* @param cmdLine 命令模板
* @param info 命令说明
* @param func 响应函数
*/
addCmdLine(cmdLine, info, func) {
let cl = CmdLine.parse(cmdLine, info);
if( this.cmdLines[ cl.cmd ] ) {
throw "重复注册的命令 " + cl.cmd
}
this.cmdLines[ cl.cmd ] = cl;
if ( func ) {
this.funcs[ cl.cmd ] = func;
}
return cl;
}
/**
* 打印命令帮助
*/
help(){
let bs = ""
if( this.name ) {
bs += this.name + gConfig.crlf
}
for ( let cmd in this.cmdLines ) {
bs = bs.concat("\t")
.concat( this.cmdLines[ cmd ].line )
.concat("\t")
.concat( this.cmdLines[ cmd ].info )
.concat( gConfig.crlf );
}
return bs;
}
canEval( cliCmdLine ){
return !! this.funcs[ cliCmdLine.cmdline.cmd ];
}
async eval( cliCmdLine ){
let func = this.funcs[ cliCmdLine.cmdline.cmd ];
if ( null == func ) {
throw "没找到命令相应函数";
}
return await func( cliCmdLine );
}
parse(argv){
if ( ! argv || argv.length < 1 ) {
return null;
}
if ( argv[0].startsWith("-") ) {
throw "入参命令格式错误 : 没有入参 cmd"
}
if ( ! this.cmdLines[ argv[0] ] ) {
return null;
}
// 标记: 去除 - 之后的值
const flags = new Set([])
// 命名参数: 去除 -- 之后的值为 key,之后值为 value
const params = {}
// 位置参数:值
const position = []
for (let i = 1; i < argv.length; i++ ) {
if ( argv[i].startsWith("--") ) {
if ( i + 1 == argv.length || argv[ i + 1 ].startsWith("-") ) {
throw "flag 格式错误" + argv[i];
}
params[ argv[i].substring(2) ] = argv[++i];
} else if ( argv[i].startsWith("-") ) {
flags.add( argv[i].substring(1) );
} else {
position.push( argv[i] );
}
}
let line = this.cmdLines[ argv[0] ];
// 整理
// line 是藐视
// flags, params, position 是值
return new CliCmdLine( this, line, flags, params, position);
}
}
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env bash
bun /app/kit.program/kit/src/index.js $@
+47
View File
@@ -0,0 +1,47 @@
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "remote_test",
"runtimeExecutable": "bun",
"runtimeArgs": ["test", "./test/remote_test.js", "-t", "exposes documented public methods", "--inspect-brk"],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/**/*.js"]
},
{
"type": "bun",
"internalConsoleOptions": "neverOpen",
"request": "launch",
"name": "Debug File",
"program": "${file}",
"cwd": "${workspaceFolder}",
"stopOnEntry": false,
"watchMode": false
},
{
"type": "bun",
"internalConsoleOptions": "neverOpen",
"request": "launch",
"name": "Run File",
"program": "${file}",
"cwd": "${workspaceFolder}",
"noDebug": true,
"watchMode": false
},
{
"type": "bun",
"internalConsoleOptions": "neverOpen",
"request": "attach",
"name": "Attach Bun",
"url": "ws://localhost:6499/e4xu5fgl1a",
"stopOnEntry": false
}
]
}
+661
View File
@@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>.
+22
View File
@@ -0,0 +1,22 @@
// build-time.js
const fs = require('fs');
const path = require('path');
// 生成当前时间的 yyyyMMdd 格式
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const buildTime = `${year}${month}${day}`;
// 创建 JSON 对象
const buildData = {
buildTime: buildTime,
timestamp: now.toISOString()
};
// 写入文件
const filePath = path.join(__dirname, 'build.json');
fs.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
console.log(`Build time ${buildTime} written to build.json`);
+366
View File
@@ -0,0 +1,366 @@
- 基于 webdav 协议访问。包括**远程仓库存储方案**(基于 WebDAV 协议)与**本地客户端存储方案**。
- 支持存储多种操作系统(Linux、Windows)、多种架构(X86、aarch64)
- 支持记录官网地址,方便定期从官网拉取新版本存储。
---
## Part A:远程仓库存储方案(基于 WebDAV)
### 1. 概述
本方案定义了一种基于 WebDAV 协议访问的远程仓库存储格式,用于存储开源项目的目标代码(二进制分发文件)。仓库支持:
- 多种操作系统(Linux、Windows、macOS、FreeBSD 等)
- 多种硬件架构(x86_64、aarch64、armv7、i386 等)
- 平台无关的二进制文件(如 Java JAR、Python 轮包、纯 JS 文件等)
- 单文件可执行程序与多文件压缩包(zip、tar.gz 等)
- 版本管理(语义化版本)与元数据记录
仓库通过 WebDAV 的目录和文件操作进行访问,支持枚举、下载、上传、删除等操作。
### 2. 仓库根目录结构
```
/repo/ # WebDAV 服务暴露的根路径
└─ {group}/ # 分组目录,默认 /modules;空值等同 /
└─ {project-id}/ # 项目目录(唯一标识),实际路径为 group/id
├─ metadata.json # 项目级元数据文件
└─ {version}/ # 版本目录(如 1.0、1.1、2.0)
├─ version.json # 版本级元数据文件(推荐)
├─ any/any/ # 平台无关文件目录(保留字)
│ └─ ... # 单文件或压缩包
├─ {os}/ # 操作系统(如 linux, windows, darwin)
│ └─ {arch}/ # 架构(如 x86_64, aarch64)
│ └─ ... # 具体二进制文件或压缩包
└─ ...
```
#### 2.1 命名规则
- `project-id`:来自项目 `meta.json` 中的唯一 `id`,小写字母、数字、连字符(-),长度 ≤ 64。
- `group`:来自项目 `meta.json`,默认 `/modules`;空字符串等同 `/`;支持 `/docker`、`/tmpls`、`/docker/net` 等子路径。
- `version`:来自项目 `meta.json` 的 `version`;若为空,发布时根据远程 `metadata.json.version` 尾数 +1,远程不存在时初始为 `1.0`。
- `os` 与 `arch` 使用标准化名称(见下表),`any` 为保留字,表示任意平台。
| 操作系统 | 标准名称 | 架构 | 标准名称 |
| ------- | --------- | ------- | --------- |
| Linux | `linux` | x86_64 | `x86_64` |
| Windows | `windows` | aarch64 | `aarch64` |
| macOS | `darwin` | armv7 | `armv7` |
| FreeBSD | `freebsd` | i386 | `i386` |
| 任意平台 | `any` | 任意架构 | `any` |
#### 2.2 目录说明
- **`any/any/`**:存放与操作系统和架构均无关的文件(如 JAR、纯 Python 脚本、WASM 模块)。客户端在任何平台均可直接使用此目录下的文件。
- **`{os}/{arch}/`**:存放特定平台、特定架构的二进制文件或压缩包。
- 同一版本下可同时包含 `any/any/` 和具体平台目录。
### 3. 元数据文件规范
#### 3.1 项目级元数据:`metadata.json`
位于每个项目根目录,UTF-8 编码,JSON 格式。
**字段说明**:
| 字段名 | 类型 | 必选 | 说明 |
| `project_id` | string | 是 | 项目唯一标识,与目录名相同 |
| `group` | string | 否 | 仓库分组路径,默认 `/modules`,空值等同 `/` |
| `official_url` | string | 是 | 项目官网地址(用于定期拉取新版本) |
| `description` | string | 否 | 项目简述 |
| `version` | string | 是 | 当前远程最新版本号;不再使用 `versions` 数组 |
| `last_fetch_check` | string (ISO 8601) | 否 | 上次检查官网更新的时间戳 |
| `fetch_rules` | array of objects | 否 | 自动拉取时的匹配规则(见 3.3) |
| `extra` | object | 否 | 扩展字段 |
**示例**:
```json
{
"project_id": "myapp",
"group": "/modules",
"official_url": "",
"description": "An example tool",
"version": "1.2",
"last_fetch_check": "2026-06-15T12:00:00Z",
"fetch_rules": [...],
"extra": { "license": "Apache-2.0" }
}
```
#### 3.2 版本级元数据:`version.json`
位于每个版本目录下,包含该版本的详细信息和文件清单。
**字段说明**:
| 字段名 | 类型 | 必选 | 说明 |
| ----------------------- | ------------------ | ---- | ---------------------------------------------- |
| `version` | string | 是 | 版本号,与目录名一致 |
| `release_date` | string (ISO 8601) | 否 | 官方发布日期 |
| `official_download_url` | string | 否 | 官网的直接下载页或 API 地址 |
| `artifacts` | array of objects | 是 | 该版本包含的所有文件条目(见 3.2.1) |
| `checksums` | object | 否 | 文件路径到哈希值的映射(可选,与 artifacts 可共存) |
| `signatures` | object | 否 | 文件路径到数字签名的映射 |
##### 3.2.1 `artifacts` 数组中每个对象的字段
| 字段名 | 类型 | 必选 | 说明 |
| ----------------- | --------- | ---- | ------------------------------------------------------------ |
| `path` | string | 是 | 相对于版本目录的文件路径,如 `linux/x86_64/myapp` 或 `any/any/app.zip` |
| `type` | string | 是 | 文件类型:`executable`(单文件可执行)、`archive`(压缩包)、`library`(库文件)、`data`(资源数据) |
| `archive_format` | string | 否 | 当 `type="archive"` 时,取值为 `zip`、`tar.gz`、`tar.xz` 等 |
| `entry_point` | string | 否 | 当 `type="archive"` 时,指定压缩包内的可执行文件相对路径(如 `bin/app`) |
| `unpack` | boolean | 否 | 是否建议客户端自动解压,默认 `false`。若为 `true`,客户端解压后应运行 `entry_point` |
| `checksum` | string | 否 | 该文件的哈希值(格式 `算法:值`,如 `sha256:abc...`) |
**示例**:
- **单文件可执行(Linux)**
```json
{
"version": "1.0.0",
"artifacts": [
{
"path": "linux/x86_64/myapp",
"type": "executable"
}
],
"checksums": {
"linux/x86_64/myapp": "sha256:e3b0c442..."
}
}
```
- **多文件压缩包(Windows)**
```json
{
"version": "2.0.0",
"artifacts": [
{
"path": "windows/x86_64/myapp.zip",
"type": "archive",
"archive_format": "zip",
"entry_point": "myapp/bin/myapp.exe",
"unpack": true
}
]
}
```
- **平台无关 JAR 文件**
```json
{
"version": "3.0.0",
"artifacts": [
{
"path": "any/any/myapp.jar",
"type": "executable"
}
]
}
```
#### 3.3 自动拉取规则(`fetch_rules`)
在 `metadata.json` 中可定义 `fetch_rules` 数组,用于指导自动化工具从官网下载并分类存储。
每个规则对象包含:
| 字段名 | 类型 | 说明 |
| ----------------- | --------- | ------------------------------------------------------------ |
| `url_pattern` | string | 正则表达式,匹配下载 URL |
| `os` | string | 目标操作系统(`linux`、`windows`、`darwin`、`any` 等) |
| `arch` | string | 目标架构(`x86_64`、`aarch64`、`any` 等) |
| `type` | string | 文件类型(`executable` 或 `archive`) |
| `archive_format` | string | 可选,当 `type="archive"` 时指定 |
| `entry_point` | string | 可选,压缩包内的入口点 |
| `unpack` | boolean | 可选,是否自动解压 |
**示例**:
```json
"fetch_rules": [
{
"url_pattern": ".*-linux-amd64\\.tar\\.gz$",
"os": "linux",
"arch": "x86_64",
"type": "archive",
"archive_format": "tar.gz",
"entry_point": "myapp/bin/myapp",
"unpack": true
},
{
"url_pattern": ".*\\.jar$",
"os": "any",
"arch": "any",
"type": "executable"
}
]
```
### 4. 客户端访问方式
基于 WebDAV 协议,客户端可通过以下 HTTP 方法操作:
- `PROPFIND`:列举项目、版本、os/arch 目录。
- `GET`:下载二进制文件或元数据文件。
- `PUT` / `MKCOL`:上传新版本或新文件(用于手动或自动填充)。
- `DELETE`:删除过期版本。
推荐 WebDAV 服务器配置:启用目录列表、支持 CORS、支持 HTTPS。
### 5. 自动化拉取新版本流程
外部工具(如 cron 作业)按以下步骤更新仓库:
1. 遍历仓库根目录下每个项目的 `metadata.json`。
2. 访问 `official_url`,获取所有可用版本列表及最新版本。
3. 对比已存储版本,找出缺失的新版本。
4. 对于每个新版本,根据 `fetch_rules`(或内置逻辑)匹配下载 URL,下载对应的二进制/压缩包。
5. 按 `os` / `arch` / `any` 规则存放到 `{group}/{project-id}/{version}/{os}/{arch}/` 下。
6. 计算文件哈希,生成或更新该版本的 `version.json`。
7. 更新项目级 `metadata.json`(`version`、`group`、`last_fetch_check`),不再写入 `versions`。
---
## Part B:本地客户端存储方案(用于版本管理与回退)
### 1. 概述
客户端从远程 WebDAV 仓库下载项目的目标代码后,需要在本地持久化存储,并支持:
- 多版本共存
- 当前激活版本的快速切换(符号链接)
- 版本回退(无需重新下载)
- 压缩包自动解压与入口点管理
- 元数据记录(安装历史、校验信息)
- 可配置的旧版本清理策略
### 2. 本地根目录结构
客户端约定一个根目录(如 `~/.cache/artifact-repo/` 或 `~/.local/share/artifact-repo/`),其下按项目组织:
```
~/.local/share/artifact-repo/
└─ {project-id}/ # 项目目录(与远程 project-id 一致)
├─ .meta/ # 元数据目录(隐藏)
│ ├─ install.json # 本地安装记录
│ └─ history/ # 可选,操作历史 JSON 文件
├─ versions/ # 所有已下载的版本
│ ├─ {semver}/ # 版本目录(如 1.2.0)
│ │ ├─ .version.json # 从远程复制的 version.json,附加本地信息
│ │ └─ {os}/{arch}/ # 与远程结构一致的实际文件
│ │ └─ ...
│ └─ {semver}/...
└─ current -> versions/{semver} # 符号链接,指向当前激活的版本
```
> **注意**:符号链接可放在项目根目录下,也可放在 `.meta/current`。推荐项目根目录下的 `current`,便于外部脚本直接引用(如 `$REPO_HOME/myapp/current/bin/myapp`)。
### 3. 元数据文件 `install.json`
位于 `.meta/install.json`,记录项目的本地安装状态。
**字段说明**:
| 字段名 | 类型 | 说明 |
| -------------------- | ------------------ | ------------------------------------------------------------ |
| `project_id` | string | 项目标识 |
| `installed_versions` | array of strings | 已下载到本地的所有版本号 |
| `current_version` | string | 当前激活的版本号(应与 `current` 符号链接一致) |
| `last_updated` | string (ISO 8601) | 最后一次变更时间(安装、切换、删除) |
| `history` | array of objects | 操作历史记录,每个元素包含 `timestamp`, `action`, `version`, `from`(可选) |
| `options` | object | 客户端配置,如 `auto_cleanup`, `max_versions` 等 |
**示例**:
```json
{
"project_id": "myapp",
"installed_versions": ["1.0.0", "1.2.0", "1.2.1"],
"current_version": "1.2.1",
"last_updated": "2026-06-15T14:30:00Z",
"history": [
{
"timestamp": "2026-06-10T09:00:00Z",
"action": "install",
"version": "1.0.0"
},
{
"timestamp": "2026-06-15T14:30:00Z",
"action": "switch",
"version": "1.2.1",
"from": "1.2.0"
}
],
"options": {
"auto_cleanup": true,
"max_versions": 5
}
}
```
### 4. 版本目录中的 `.version.json`
从远程仓库的 `version.json` 复制而来,并增加本地特有字段:
```json
{
"version": "1.2.1",
"installed_at": "2026-06-15T14:25:00Z",
"local_checksums": {
"linux/x86_64/myapp": "sha256:..."
},
... // 原始远程字段(artifacts, release_date 等)
}
```
### 5. 核心操作流程
#### 5.1 安装新版本(更新)
1. 从远程 WebDAV 仓库下载指定版本的所有相关文件到 `versions/{new_version}/`,保持远程目录结构(包括 `any/any/` 和具体平台目录)。
2. 同时下载 `version.json` 并保存为 `.version.json`,添加 `installed_at` 时间戳。
3. 更新 `.meta/install.json`:
- 将 `new_version` 加入 `installed_versions`(若未存在)。
- 设置 `current_version` 为 `new_version`(若需要立即激活)。
- 添加历史记录(`action: "install"` 或 `"switch"`)。
4. 更新项目根目录下的 `current` 符号链接指向 `versions/{new_version}`。
5. 如果 `artifacts` 中有 `type="archive"` 且 `unpack=true` 的条目,则解压到版本目录下的特定子目录(如 `extracted/`),并在 `.version.json` 中记录解压后的入口点路径。
6. 根据 `options` 中的清理策略删除旧版本(保留最近 N 个版本或保留指定天数)。
#### 5.2 回退到已安装的旧版本
1. 确认目标旧版本存在于 `installed_versions` 中。
2. 修改 `current` 符号链接指向 `versions/{old_version}`。
3. 更新 `.meta/install.json`:
- 将 `current_version` 改为 `old_version`。
- 添加历史记录(`action: "rollback"` 或 `"switch"`,可记录 `from` 原版本)。
4. 无需重新下载任何文件。
#### 5.3 卸载特定版本
1. 如果卸载的是当前激活版本,先切换到另一个已安装版本(或提示用户)。
2. 删除 `versions/{version}` 整个目录。
3. 从 `install.json` 的 `installed_versions` 数组中移除该版本。
4. 添加历史记录(`action: "uninstall"`)。
#### 5.4 清理旧版本(自动或手动)
清理策略示例:
- 保留最近 `max_versions` 个版本(不计当前版本)。
- 或删除 `keep_recent_days` 天之前安装且不是当前版本的版本。
- 每次安装新版本后自动触发清理。
### 6. 处理压缩包(archive)的特别说明
当远程 `artifact` 的 `unpack=true` 时:
- 客户端下载压缩包到 `versions/{version}/{path}`。
- 立即解压到同目录下的 `_extracted/` 子目录(例如 `versions/1.2.1/linux/x86_64/_extracted/`)。
- 根据 `entry_point` 在 `.version.json` 中记录解压后的可执行文件绝对路径(或相对于版本目录的路径)。
- 符号链接 `current` 可以直接指向解压后的入口点文件,而不是整个版本目录。实现方式:
```
current/bin/myapp -> ../versions/1.2.1/_extracted/bin/myapp
```
这样切换版本时只需更新符号链接,无需重新解压。
### 7. 多平台兼容性
客户端在下载时,应根据当前运行的操作系统和架构,仅从远程仓库获取匹配的文件:
- 优先尝试 `{os}/{arch}/`
- 若不存在,回退到 `any/any/`
- 下载后保持远程的相对路径结构,但客户端实际运行时只使用与当前平台相关的文件。
在本地存储中,所有版本目录仍保留完整的 `{os}/{arch}/` 和 `any/any/` 结构,便于在不同平台间迁移或共享缓存。
+3
View File
@@ -0,0 +1,3 @@
- 每个函数都要有中文注释, 包括功能说明、参数说明、返回值说明等,如果有注意事项,也要在注释中特别说明。
- 每次需求修改都要同时更新文档、代码、测试代码。
- 每条测试用例都要有一个唯一编号。例如 1-1、2-20 等。
+42
View File
@@ -0,0 +1,42 @@
```bash
# 验证远程仓库可达
check
# 发行某个模块 eg. myapp
publish myapp
# 列出已安装版本 eg. myapp
list myapp
# 输出:
# 1.0.0
# * 1.2.0 (current)
# 1.2.1
# 安装某个模块 eg. myapp
install myapp
# 切换到版本 1.2.0
use myapp@1.2.0
# 回滚到上一个版本
rollback myapp
# 删除模块
remove myapp
```
## 发布规则
项目 `meta.json` 支持 `group` 字段:
- 默认值为 `/modules`。
- 空字符串等同于 `/`。
- 可配置为 `/docker`、`/tmpls` 等一级路径,也可配置为 `/docker/net` 等子路径。
- 远程仓库整体上传路径为 `group/id`,其中 `id` 是项目唯一标识。
项目 `meta.json` 只使用 `version` 字段,不再使用 `versions`:
- `version` 有值时,按该版本上传;上传前检查远程 `group/id/version` 是否存在,已存在则报错。
- `version` 为空时,先读取远程 `group/id/metadata.json` 中的 `version`,将尾数 +1 后上传。
- 远程没有版本信息时,初始版本号为 `1.0`。
+5
View File
@@ -0,0 +1,5 @@
{
"linux" : "./dist/linux/kit",
"windows" : "./dist/windows/kit.exe",
"dir" : "C:\\Users\\occul\\SynologyDrive\\bin"
}
+669
View File
@@ -0,0 +1,669 @@
{
"name": "kit",
"version": "0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kit",
"version": "0.1",
"dependencies": {
"auto-launch": "^5.0.6",
"cross-port-killer": "^1.4.0",
"extract-zip": "^2.0.1",
"get-port": "^7.0.0",
"json5": "^2.2.3",
"jsrsasign": "^11.1.1",
"log4js": "^6.9.1",
"node-forge": "^1.3.1",
"physical-cpu-count": "^2.0.0",
"sm-crypto": "^0.3.13",
"systeminformation": "^5.30.6",
"webdav": "^5.3.1"
},
"bin": {
"kit": "src/index.js"
}
},
"node_modules/@buttercup/fetch": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@buttercup/fetch/-/fetch-0.1.2.tgz",
"integrity": "sha512-mDBtsysQ0Gnrp4FamlRJGpu7HUHwbyLC4uUav1I7QAqThFAa/4d1cdZCxrV5gKvh6zO1fu95bILNJi4Y2hALhQ==",
"optionalDependencies": {
"node-fetch": "^3.3.0"
}
},
"node_modules/@types/node": {
"version": "20.10.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.8.tgz",
"integrity": "sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA==",
"optional": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
"optional": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/applescript": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/applescript/-/applescript-1.0.0.tgz",
"integrity": "sha512-yvtNHdWvtbYEiIazXAdp/NY+BBb65/DAseqlNiJQjOx9DynuzOYDbVLBJvuc0ve0VL9x6B3OHF6eH52y9hCBtQ=="
},
"node_modules/auto-launch": {
"version": "5.0.6",
"resolved": "https://registry.npmmirror.com/auto-launch/-/auto-launch-5.0.6.tgz",
"integrity": "sha512-OgxiAm4q9EBf9EeXdPBiVNENaWE3jUZofwrhAkWjHDYGezu1k3FRZHU8V2FBxGuSJOHzKmTJEd0G7L7/0xDGFA==",
"dependencies": {
"applescript": "^1.0.0",
"mkdirp": "^0.5.1",
"path-is-absolute": "^1.0.0",
"untildify": "^3.0.2",
"winreg": "1.2.4"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/base-64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz",
"integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="
},
"node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"engines": {
"node": "*"
}
},
"node_modules/byte-length": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/byte-length/-/byte-length-1.0.2.tgz",
"integrity": "sha512-ovBpjmsgd/teRmgcPh23d4gJvxDoXtAzEL9xTfMU8Yc2kqCDb7L9jAG0XHl1nzuGl+h3ebCIF1i62UFyA9V/2Q=="
},
"node_modules/charenc": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==",
"engines": {
"node": "*"
}
},
"node_modules/cross-port-killer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/cross-port-killer/-/cross-port-killer-1.4.0.tgz",
"integrity": "sha512-ujqfftKsSeorFMVI6JP25xMBixHEaDWVK+NarRZAGnJjR5AhebRQU+g+k/Lj8OHwM6f+wrrs8u5kkCdI7RLtxQ==",
"bin": {
"kill-port": "source/cli.js"
}
},
"node_modules/crypt": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
"integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==",
"engines": {
"node": "*"
}
},
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"optional": true,
"engines": {
"node": ">= 12"
}
},
"node_modules/date-format": {
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz",
"integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==",
"engines": {
"node": ">=4.0"
}
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
"dependencies": {
"debug": "^4.1.1",
"get-stream": "^5.1.0",
"yauzl": "^2.10.0"
},
"bin": {
"extract-zip": "cli.js"
},
"engines": {
"node": ">= 10.17.0"
},
"optionalDependencies": {
"@types/yauzl": "^2.9.1"
}
},
"node_modules/fast-xml-parser": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.3.tgz",
"integrity": "sha512-coV/D1MhrShMvU6D0I+VAK3umz6hUaxxhL0yp/9RjfiYUfAv14rDhGQL+PLForhMdr0wq3PiV07WtkkNjJjNHg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
},
{
"type": "paypal",
"url": "https://paypal.me/naturalintelligence"
}
],
"dependencies": {
"strnum": "^1.0.5"
},
"bin": {
"fxparser": "src/cli/cli.js"
}
},
"node_modules/fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
"dependencies": {
"pend": "~1.2.0"
}
},
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"optional": true,
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/flatted": {
"version": "3.2.9",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz",
"integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ=="
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"optional": true,
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
},
"engines": {
"node": ">=6 <7 || >=8"
}
},
"node_modules/get-port": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-7.0.0.tgz",
"integrity": "sha512-mDHFgApoQd+azgMdwylJrv2DX47ywGq1i5VFJE7fZ0dttNq3iQMfsU4IvEgBHojA3KqEudyu7Vq+oN8kNaNkWw==",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"dependencies": {
"pump": "^3.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"bin": {
"he": "bin/he"
}
},
"node_modules/hot-patcher": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-2.0.1.tgz",
"integrity": "sha512-ECg1JFG0YzehicQaogenlcs2qg6WsXQsxtnbr1i696u5tLUjtJdQAh0u2g0Q5YV45f263Ta1GnUJsc8WIfJf4Q=="
},
"node_modules/is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
},
"node_modules/jsbn": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
},
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"bin": {
"json5": "lib/cli.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/jsrsasign": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-11.1.1.tgz",
"integrity": "sha512-6w95OOXH8DNeGxakqLndBEqqwQ6A70zGaky1oxfg8WVLWOnghTfJsc5Tknx+Z88MHSb1bGLcqQHImOF8Lk22XA==",
"license": "MIT",
"funding": {
"url": "https://github.com/kjur/jsrsasign#donations"
}
},
"node_modules/layerr": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/layerr/-/layerr-2.0.1.tgz",
"integrity": "sha512-z0730CwG/JO24evdORnyDkwG1Q7b7mF2Tp1qRQ0YvrMMARbt1DFG694SOv439Gm7hYKolyZyaB49YIrYIfZBdg=="
},
"node_modules/log4js": {
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz",
"integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==",
"dependencies": {
"date-format": "^4.0.14",
"debug": "^4.3.4",
"flatted": "^3.2.7",
"rfdc": "^1.3.0",
"streamroller": "^3.1.5"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/md5": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
"integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
"dependencies": {
"charenc": "0.0.2",
"crypt": "0.0.2",
"is-buffer": "~1.1.6"
}
},
"node_modules/minimatch": {
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz",
"integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/nested-property": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/nested-property/-/nested-property-4.0.0.tgz",
"integrity": "sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA=="
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"optional": true,
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"optional": true,
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/node-forge": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz",
"integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==",
"license": "(BSD-3-Clause OR GPL-2.0)",
"engines": {
"node": ">= 6.13.0"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-posix": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz",
"integrity": "sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA=="
},
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="
},
"node_modules/physical-cpu-count": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/physical-cpu-count/-/physical-cpu-count-2.0.0.tgz",
"integrity": "sha512-rxJOljMuWtYlvREBmd6TZYanfcPhNUKtGDZBjBBS8WG1dpN2iwPsRJZgQqN/OtJuiQckdRFOfzogqJClTrsi7g=="
},
"node_modules/pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
},
"node_modules/rfdc": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz",
"integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA=="
},
"node_modules/sm-crypto": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/sm-crypto/-/sm-crypto-0.3.13.tgz",
"integrity": "sha512-ztNF+pZq6viCPMA1A6KKu3bgpkmYti5avykRHbcFIdSipFdkVmfUw2CnpM2kBJyppIalqvczLNM3wR8OQ0pT5w==",
"dependencies": {
"jsbn": "^1.1.0"
}
},
"node_modules/streamroller": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz",
"integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==",
"dependencies": {
"date-format": "^4.0.14",
"debug": "^4.3.4",
"fs-extra": "^8.1.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/strnum": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
"integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA=="
},
"node_modules/systeminformation": {
"version": "5.30.6",
"resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.30.6.tgz",
"integrity": "sha512-LEIyK1aEv5P3BhAPW3swdlIyCihxwEq/Gki+kcONieU4PIeRCSLDuGkk0Va/56PSBgjVgEksOM88dmY6YqOyfQ==",
"license": "MIT",
"os": [
"darwin",
"linux",
"win32",
"freebsd",
"openbsd",
"netbsd",
"sunos",
"android"
],
"bin": {
"systeminformation": "lib/cli.js"
},
"engines": {
"node": ">=8.0.0"
},
"funding": {
"type": "Buy me a coffee",
"url": "https://www.buymeacoffee.com/systeminfo"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"optional": true
},
"node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/untildify": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/untildify/-/untildify-3.0.3.tgz",
"integrity": "sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA==",
"engines": {
"node": ">=4"
}
},
"node_modules/url-join": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz",
"integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"node_modules/web-streams-polyfill": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.2.tgz",
"integrity": "sha512-3pRGuxRF5gpuZc0W+EpwQRmCD7gRqcDOMt688KmdlDAgAyaB1XlN0zq2njfDNm44XVdIouE7pZ6GzbdyH47uIQ==",
"optional": true,
"engines": {
"node": ">= 8"
}
},
"node_modules/webdav": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/webdav/-/webdav-5.3.1.tgz",
"integrity": "sha512-wzZdTHtMuSIXqHGBznc8FM2L94Mc/17Tbn9ppoMybRO0bjWOSIeScdVXWX5qqHsg00EjfiOcwMqGFx6ghIhccQ==",
"dependencies": {
"@buttercup/fetch": "^0.1.1",
"base-64": "^1.0.0",
"byte-length": "^1.0.2",
"fast-xml-parser": "^4.2.4",
"he": "^1.2.0",
"hot-patcher": "^2.0.0",
"layerr": "^2.0.1",
"md5": "^2.3.0",
"minimatch": "^7.4.6",
"nested-property": "^4.0.0",
"path-posix": "^1.0.0",
"url-join": "^5.0.0",
"url-parse": "^1.5.10"
},
"engines": {
"node": ">=14"
}
},
"node_modules/winreg": {
"version": "1.2.4",
"resolved": "https://registry.npmmirror.com/winreg/-/winreg-1.2.4.tgz",
"integrity": "sha512-IHpzORub7kYlb8A43Iig3reOvlcBJGX9gZ0WycHhghHtA65X0LYnMRuJs+aH1abVnMJztQkvQNlltnbPi5aGIA=="
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
"dependencies": {
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
}
}
}
+28
View File
@@ -0,0 +1,28 @@
{
"name": "kit",
"version": "0.1",
"bin": "src/index.js",
"scripts": {
"test": "bun src/index.js",
"linux": "bun build.js && bun build ./src/index.js --compile --bytecode --minify --target=bun-linux-x64-baseline --outfile=./dist/linux/kit",
"windows": "bun build.js && bun build ./src/index.js --compile --bytecode --minify --target=bun-windows-x64-baseline --outfile=./dist/windows/kit.exe",
"pkg-alpine": "bun build.js && pkg --compress GZip . -t node14-alpine-arm64 -o ./dist/alpine/kit",
"pkg-windows": "bun build.js && pkg --compress GZip . -t node14-windows-x64 -o ./dist/windows/kit.exe",
"pkg-arm64": "bun build.js && pkg --compress GZip . -t node14-linux-arm64 -o ./dist/linux/kit",
"pkg-linux": "bun build.js && pkg --compress GZip . -t node14-linux-x64 -o ./dist/linux/kit"
},
"dependencies": {
"auto-launch": "^5.0.6",
"cross-port-killer": "^1.4.0",
"extract-zip": "^2.0.1",
"get-port": "^7.0.0",
"json5": "^2.2.3",
"jsrsasign": "^11.1.1",
"log4js": "^6.9.1",
"node-forge": "^1.3.1",
"physical-cpu-count": "^2.0.0",
"sm-crypto": "^0.3.13",
"systeminformation": "^5.30.6",
"webdav": "^5.3.1"
}
}
+1
View File
@@ -0,0 +1 @@
node push.js windows D:\\SynologyDrive\\bin
+58
View File
@@ -0,0 +1,58 @@
// push.js
const fs = require('fs');
const path = require('path');
// 读取命令行参数
const args = process.argv.slice(2);
const platform = args[0]; // 'windows' 或 'linux'
const target = args[1]; // 目标配置键,如 'dir'
if (!platform || !target) {
console.error('用法: node push.js <平台> <目标配置键>');
console.error('示例: node push.js windows dir');
process.exit(1);
}
try {
// 读取配置文件
const configPath = path.join(__dirname, 'local.json');
const configData = JSON.parse(fs.readFileSync(configPath, 'utf8'));
// 验证参数
if (!configData[platform]) {
console.error(`错误: 配置文件中找不到平台 "${platform}"`);
process.exit(1);
}
if (!configData[target]) {
console.error(`错误: 配置文件中找不到目标 "${target}"`);
process.exit(1);
}
// 获取源文件和目标目录
const sourceFile = path.resolve(__dirname, configData[platform]);
const targetDir = configData[target];
// 获取源文件名
const sourceFileName = path.basename(sourceFile);
// 创建目标路径
const targetPath = path.join(targetDir, sourceFileName);
// 确保目标目录存在
if (!fs.existsSync(targetDir)) {
console.log(`创建目录: ${targetDir}`);
fs.mkdirSync(targetDir, { recursive: true });
}
// 复制文件
fs.copyFileSync(sourceFile, targetPath);
console.log(`成功复制文件:`);
console.log(` 从: ${sourceFile}`);
console.log(` 到: ${targetPath}`);
} catch (error) {
console.error(`错误: ${error.message}`);
process.exit(1);
}
+98
View File
@@ -0,0 +1,98 @@
/**
* 系统配置
*/
const os = require('os');
const process = require('process');
const path = require("path");
const fs = require('fs');
module.exports = class SystemConfig {
constructor() {
}
static isWindows(){
return os.platform() === 'win32'
}
static isLinux(){
return os.platform() === 'linux'
}
static getShellEncode(){
if ( this.isWindows() ) {
return "GBK"
} else {
return "UTF-8"
}
}
static getLocalKitPath(){
let path = "./"
if ( this.isWindows() ) {
if( require && require.main && require.main.filename ){
path = require('path').dirname(require.main.filename)
} else {
path = process.cwd()
}
} else {
path = require('path').dirname(process.execPath);
}
this.affirmDir(path)
$logger.debug(`LocalKitPath=${path}`)
return path;
}
static getLocalStorePath(){
const path = require("path")
let dir = path.resolve(this.getLocalKitPath(), ".kit")
this.affirmDir(dir)
return dir;
}
static getUserPath() {
// let userDirectory = '~';
// if ( this.isWindows() ) {
// userDirectory = process.env.USERPROFILE;
// }
return os.homedir();
}
static getUserConfigPath() {
const path = require("path")
const dir = path.resolve( this.getUserPath() , ".kit");
this.affirmDir(dir)
return dir;
}
static getUserConfigFilePath() {
const path = require("path")
return path.resolve( this.getUserPath() , ".kit", "config.json");
}
static getLogPath() {
const path = require("path")
let logs = path.resolve( this.getUserPath() , ".kit", "logs");
this.affirmDir(logs)
return logs
}
/**
* 确保目录存在
* @param path
*/
static affirmDir(path){
// 检查文件夹是否存在
const exists = fs.existsSync(path);
if (! exists) {
// 文件夹不存在
try {
fs.mkdirSync(path, { recursive: true });
} catch (err) {
// 处理错误
$logger.error(`affirmDir ${path} error ${ err }`);
}
}
}
}
+52
View File
@@ -0,0 +1,52 @@
/**
* 读写用户配置
* 实时维护状态
*/
const fs = require("fs")
const JSON5 = require('json5')
module.exports = class UserConfig {
constructor(path) {
this.path = path
this.init()
}
init(){
let txt = "{}";
try {
txt = fs.readFileSync(this.path, 'utf8')
} catch ( err ) {
if (err.code === 'ENOENT') {
// File does not exist.
txt = '{}';
} else {
// Some other error occurred. Throw it.
throw err;
}
}
this.configs = JSON5.parse( txt )
this.setDefault("man", "https://flowus.cn/fullstack/share/acd5b26f-b3d1-4d41-a58e-e4f23daf9db8")
}
get(key) {
return this.configs[key]
}
set(key, value) {
this.configs[key] = value
this.write()
}
setDefault(key, value){
if ( ! this.configs[key] ) {
this.configs[key] = value
}
}
write(){
fs.writeFileSync(this.path, JSON5.stringify(this.configs, undefined, 4))
}
}
+14
View File
@@ -0,0 +1,14 @@
/**
* config 模块处理配置相关事务
*/
const SystemConfig = require("./SystemConfig")
const UserConfig = require("./UserConfig")
module.exports = {
SystemConfig, UserConfig
}
+33
View File
@@ -0,0 +1,33 @@
/**
* 对象管理
* 懒加载对象
*/
const config = require("../config");
module.exports = class Context {
constructor() {
this.beans = {}
this.standalones = {}
}
standalone(name, factory){
if ( this.standalones[name] ){
throw `重定义 ${name}`
}
if ( ! factory ) {
throw `定义 ${name} 时未提供必须参数 factory`
}
this.standalones[name] = factory
}
async get(name){
if ( this.beans[name] ) {
return this.beans[name]
}
if ( this.standalones[name] ){
this.beans[name] = await this.standalones[name]()
return this.beans[name]
} else {
return undefined
}
}
}
+463
View File
@@ -0,0 +1,463 @@
const sm4 = require('sm-crypto').sm4
const sm3 = require('sm-crypto').sm3
const sm2 = require('sm-crypto').sm2
const crypto = require('crypto')
const forge = require('node-forge')
const rs = require('jsrsasign')
module.exports.sm4en3 = function (key, data){
let sm3key = sm3(key)
sm3key = sm3key.substring(0, 32)
let encryptData = sm4.encrypt(data, sm3key)
return encryptData
}
module.exports.sm4de3 = function (key, data){
let sm3key = sm3(key)
sm3key = sm3key.substring(0, 32)
let decryptData = sm4.decrypt(data, sm3key)
return decryptData
}
/**
* 从 PKCS7 签名中获取信息(包含证书、公钥和签名值)
* 支持 RSA 和 SM2 算法
* @param {Buffer|string} p7sign - PKCS7 签名数据
* @returns {Object} 包含证书、公钥和签名值的对象
*/
module.exports.getInfoFromPKCS7 = function (p7sign) {
// 处理输入数据
let base64Data
if (Buffer.isBuffer(p7sign)) {
base64Data = p7sign.toString('base64')
} else if (typeof p7sign === 'string') {
// 判断是否是 PEM 格式
if (p7sign.includes('-----BEGIN PKCS7-----')) {
// 提取 base64 部分
base64Data = p7sign
.replace(/-----BEGIN PKCS7-----\n?/, '')
.replace(/-----END PKCS7-----\n?/, '')
.replace(/\n/g, '')
} else {
// 如果不是 PEM 格式,假设是 base64
base64Data = p7sign
}
} else {
throw new Error('Invalid input type for p7sign')
}
// 尝试使用 node-forge 解析(支持 RSA)
try {
const pemData = `-----BEGIN PKCS7-----\n${base64Data}\n-----END PKCS7-----`
const p7 = forge.pkcs7.messageFromPem(pemData)
return extractInfoFromForgeP7(p7)
} catch (forgeError) {
// 如果 node-forge 解析失败,尝试使用 jsrsasign(支持 SM2)
try {
return extractInfoFromJsrsasignP7(base64Data)
} catch (jsrsasignError) {
throw new Error(`Failed to parse PKCS7: ${forgeError.message}, ${jsrsasignError.message}`)
}
}
}
/**
* 从 node-forge 解析的 PKCS7 中提取信息(RSA)
*/
function extractInfoFromForgeP7(p7) {
// 辅助函数:将属性数组转换为 DN 字符串
function attrsToDN(attrs) {
if (!attrs || attrs.length === 0) return ''
return attrs.map(attr => `${attr.name}=${attr.value}`).join(', ')
}
// 提取证书
let cert = null
if (p7.certificates && p7.certificates.length > 0) {
const certObj = p7.certificates[0]
cert = {
subject: certObj.subject.attributes.map(attr => ({
name: attr.name,
value: attr.value
})),
subjectDN: attrsToDN(certObj.subject.attributes),
issuer: certObj.issuer.attributes.map(attr => ({
name: attr.name,
value: attr.value
})),
issuerDN: attrsToDN(certObj.issuer.attributes),
serialNumber: certObj.serialNumber,
notBefore: certObj.validity.notBefore,
notAfter: certObj.validity.notAfter,
pem: forge.pki.certificateToPem(certObj)
}
}
// 提取公钥 - 从证书中提取公钥对象并转换为 PEM 和 Hex
let publicKey = null
let publicKeyHex = null
if (p7.certificates && p7.certificates.length > 0) {
const certObj = p7.certificates[0]
// 提取公钥对象并转换为 PEM 格式
if (certObj.publicKey) {
publicKey = forge.pki.publicKeyToPem(certObj.publicKey)
// 将公钥转换为 hex 格式
const publicKeyAsn1 = forge.pki.publicKeyToAsn1(certObj.publicKey)
const publicKeyDer = forge.asn1.toDer(publicKeyAsn1)
publicKeyHex = forge.util.bytesToHex(publicKeyDer.getBytes())
}
}
// 提取签名值 - 从 rawCapture 中获取(已经是原始签名值)
let signature = null
let signatureHex = null
if (p7.rawCapture && p7.rawCapture.signature) {
// rawCapture.signature 已经是原始签名值字符串
signature = forge.util.encode64(p7.rawCapture.signature)
// 同时提供 hex 格式
signatureHex = forge.util.bytesToHex(p7.rawCapture.signature)
}
return {
cert,
publicKey,
publicKeyHex,
signature,
signatureHex
}
}
/**
* 从 jsrsasign 解析的 PKCS7 中提取信息(支持 SM2)
*/
function extractInfoFromJsrsasignP7(base64Data) {
const parser = new rs.KJUR.asn1.cms.CMSParser()
const p7 = parser.getCMSSignedData(rs.b64tohex(base64Data))
// 提取证书
let cert = null
let publicKey = null
let publicKeyHex = null
// p7.certs 是一个对象,包含 array 属性
const certs = p7.certs && p7.certs.array ? p7.certs.array : []
if (certs.length > 0) {
const certPem = certs[0]
// 解析证书获取详细信息
const x509 = new rs.X509()
x509.readCertPEM(certPem)
// 获取主题和颁发者
const subjectStr = x509.getSubjectString()
const issuerStr = x509.getIssuerString()
// 将字符串转换为数组格式
function parseDNString(dnStr) {
if (!dnStr || dnStr === '/') return []
const parts = dnStr.substring(1).split('/')
return parts.map(part => {
const [name, value] = part.split('=')
return { name, value }
})
}
const subjectAttrs = parseDNString(subjectStr)
const issuerAttrs = parseDNString(issuerStr)
// 转换为 DN 字符串
function attrsToDN(attrs) {
if (!attrs || attrs.length === 0) return ''
return attrs.map(attr => `${attr.name}=${attr.value}`).join(', ')
}
cert = {
subject: subjectAttrs,
subjectDN: attrsToDN(subjectAttrs),
issuer: issuerAttrs,
issuerDN: attrsToDN(issuerAttrs),
serialNumber: x509.getSerialNumberHex(),
notBefore: x509.getNotBefore(),
notAfter: x509.getNotAfter(),
pem: certPem
}
// 提取公钥 - 使用 ASN.1 解析提取原始公钥字节
try {
// 将 PEM 转换为 hex
const certHex = rs.pemtohex(certPem)
// 获取 tbsCertificate
const tbsCert = rs.ASN1HEX.getTLVbyList(certHex, 0, [0])
// 获取 subjectPublicKeyInfo (在 tbsCertificate 中的索引是 6)
const spki = rs.ASN1HEX.getTLVbyList(tbsCert, 0, [6])
// 提取公钥值 (BIT STRING)
const publicKeyBitString = rs.ASN1HEX.getTLVbyList(spki, 0, [1])
// BIT STRING 的内容:跳过 tag 03 (2 hex), 跳过 length (2 hex), 跳过 unused bits (2 hex)
// 所以从第 6 个字符开始
const publicKeyHexContent = publicKeyBitString.substring(6)
// 提取算法 OID 来检测是否是 SM2
const algorithmSeq = rs.ASN1HEX.getTLVbyList(spki, 0, [0])
const oidTlv = rs.ASN1HEX.getTLVbyList(algorithmSeq, 0, [0])
const oidHex = oidTlv.substring(4)
// SM2 OID: 1.2.156.10197.1.301 -> hex 2A811CCF550101301
const isSM2 = oidHex.includes('2A811CCF55') || oidHex.includes('2a811ccf55')
// 创建完整的 subjectPublicKeyInfo PEM
publicKey = rs.KJUR.asn1.ASN1Util.getPEMStringFromHex(spki, 'PUBLIC KEY')
// publicKeyHex 就是 subjectPublicKeyInfo 的完整 hex
publicKeyHex = spki
} catch (e) {
// SM2 公钥可能无法通过标准方式获取
console.error('提取 SM2 公钥错误:', e.message)
publicKey = null
publicKeyHex = null
}
}
// 提取签名值
let signature = null
let signatureHex = null
if (p7.sinfos && p7.sinfos.length > 0) {
const signer = p7.sinfos[0]
if (signer.sighex) {
signatureHex = signer.sighex
signature = rs.hextob64(signer.sighex)
}
}
return {
cert,
publicKey,
publicKeyHex,
signature,
signatureHex
}
}
/**
* 验签 PKCS7 签名(自动判断 RSA 或 SM2 算法)
* @param {Buffer|string} p7sign - PKCS7 签名数据
* @param {Buffer|string} [originalData] - 原始数据,当签名是 detech 时必须,当签名是 attach 时不传
* @returns {boolean} 验签结果
*/
module.exports.verifyPKCS7 = function (p7sign, originalData) {
// 处理输入数据
let base64Data
if (Buffer.isBuffer(p7sign)) {
base64Data = p7sign.toString('base64')
} else if (typeof p7sign === 'string') {
// 判断是否是 PEM 格式
if (p7sign.includes('-----BEGIN PKCS7-----')) {
// 提取 base64 部分
base64Data = p7sign
.replace(/-----BEGIN PKCS7-----\n?/, '')
.replace(/-----END PKCS7-----\n?/, '')
.replace(/\n/g, '')
} else {
// 如果不是 PEM 格式,假设是 base64
base64Data = p7sign
}
} else {
throw new Error('Invalid input type for p7sign')
}
// 准备验证所需的数据
let verifyData
if (originalData) {
// 当提供了原始数据时,使用原始数据进行验证(detch 模式)
if (Buffer.isBuffer(originalData)) {
verifyData = originalData
} else if (typeof originalData === 'string') {
verifyData = Buffer.from(originalData)
} else {
throw new Error('Invalid input type for originalData')
}
}
// 尝试使用 node-forge 解析和验证(支持 RSA)
try {
const pemData = `-----BEGIN PKCS7-----\n${base64Data}\n-----END PKCS7-----`
const p7 = forge.pkcs7.messageFromPem(pemData)
// 如果没有提供原始数据,从 PKCS7 中提取(attach 模式)
if (!verifyData) {
if (p7.content) {
verifyData = Buffer.from(p7.content.getBytes(), 'binary')
} else {
throw new Error('No content found in PKCS7 signature for attach mode')
}
}
// 对于 RSA 签名,使用公钥手动验证
if (p7.certificates && p7.certificates.length > 0) {
const publicKey = p7.certificates[0].publicKey
if (publicKey && p7.rawCapture && p7.rawCapture.signature) {
// 计算数据的摘要
const md = forge.md.sha256.create()
md.update(verifyData.toString(), 'utf8')
const digest = md.digest().getBytes()
// 使用公钥验证签名
return publicKey.verify(digest, p7.rawCapture.signature)
}
}
// 如果 RSA 验证失败或不适用,尝试使用 node-forge 的 verify(如果支持)
return p7.verify()
} catch (forgeError) {
// 如果 node-forge 解析失败,尝试使用 jsrsasign 解析和验证(支持 SM2)
try {
return verifyPKCS7WithJsrsasign(base64Data, verifyData)
} catch (jsrsasignError) {
// 静默处理错误,直接返回 false
return false
}
}
}
/**
* 使用 jsrsasign 验证 PKCS7 签名(支持 SM2)
*/
function verifyPKCS7WithJsrsasign(base64Data, verifyData) {
const parser = new rs.KJUR.asn1.cms.CMSParser()
const p7 = parser.getCMSSignedData(rs.b64tohex(base64Data))
// 如果没有提供原始数据,从 PKCS7 中提取(attach 模式)
if (!verifyData) {
if (p7.econtent && p7.econtent.hex) {
// econtent.hex 是原始数据的 hex 编码
verifyData = Buffer.from(p7.econtent.hex, 'hex')
} else {
throw new Error('No content found in PKCS7 signature for attach mode')
}
}
// 获取签名信息
if (!p7.sinfos || p7.sinfos.length === 0) {
throw new Error('No signer info found in PKCS7')
}
const signer = p7.sinfos[0]
// 获取证书和公钥
if (!p7.certs || !p7.certs.array || p7.certs.array.length === 0) {
throw new Error('No certificate found in PKCS7')
}
const certPem = p7.certs.array[0]
const x509 = new rs.X509()
x509.readCertPEM(certPem)
// 判断签名算法
const sigAlg = signer.sigalg
// SM2 签名算法 OID: 1.2.156.10197.1.301.1 或 1.2.156.10197.1.501
if (sigAlg === '1.2.156.10197.1.301.1' || sigAlg === '1.2.156.10197.1.501' || sigAlg.includes('sm2')) {
// SM2 验证
// 提取公钥 hex
const certHex = rs.pemtohex(certPem)
const tbsCert = rs.ASN1HEX.getTLVbyList(certHex, 0, [0])
const spki = rs.ASN1HEX.getTLVbyList(tbsCert, 0, [6])
const publicKeyBitString = rs.ASN1HEX.getTLVbyList(spki, 0, [1])
let publicKeyHex = publicKeyBitString.substring(6) // 跳过 tag, length, unused bits
// 确保公钥有 04 前缀(sm-crypto 0.3.13 需要)
if (!publicKeyHex.startsWith('04')) {
publicKeyHex = '04' + publicKeyHex
}
// 计算 SM3 哈希
const hash = sm3(verifyData.toString())
// 签名格式:用户提供的是 ASN.1 DER 格式,需要使用 der: true 选项
let signatureHex = signer.sighex
// 使用 SM2 验证签名 (注意:使用 doVerifySignature,不是 doVerify)
// 尝试不同的选项组合,包括 userId
const userId = '1234567812345678' // 默认 userId
try {
// 尝试 1: 使用原始数据 + DER + SM3 + userId
const result1 = sm2.doVerifySignature(verifyData.toString(), signatureHex, publicKeyHex, { der: true, hash: true, userId })
if (result1) return true
// 尝试 2: 使用原始数据 + DER + SM3
const result2 = sm2.doVerifySignature(verifyData.toString(), signatureHex, publicKeyHex, { der: true, hash: true })
if (result2) return true
// 尝试 3: 使用原始数据 + DER
const result3 = sm2.doVerifySignature(verifyData.toString(), signatureHex, publicKeyHex, { der: true })
if (result3) return true
// 尝试 4: 使用哈希 + DER + SM3 + userId
const result4 = sm2.doVerifySignature(hash, signatureHex, publicKeyHex, { der: true, hash: true, userId })
if (result4) return true
// 尝试 5: 使用哈希 + DER + SM3
const result5 = sm2.doVerifySignature(hash, signatureHex, publicKeyHex, { der: true, hash: true })
if (result5) return true
// 尝试 6: 使用哈希 + DER
const result6 = sm2.doVerifySignature(hash, signatureHex, publicKeyHex, { der: true })
if (result6) return true
// 尝试 7: 不使用 der 选项(直接使用签名)
const result7 = sm2.doVerifySignature(verifyData.toString(), signatureHex, publicKeyHex)
if (result7) return true
// 尝试 8: 不使用 der 选项 + SM3 + userId
const result8 = sm2.doVerifySignature(verifyData.toString(), signatureHex, publicKeyHex, { hash: true, userId })
if (result8) return true
// 尝试 9: 转换签名格式为原始 r|s 格式
if (signatureHex.startsWith('30')) {
try {
// 解析 ASN.1 DER 格式
// 格式: 30 len 02 len r 02 len s
const rStart = 8 // 30440220
const r = signatureHex.substring(rStart, rStart + 64) // 32字节 = 64 hex 字符
const sStart = rStart + 64 + 4 // 0220
const s = signatureHex.substring(sStart, sStart + 64) // 32字节 = 64 hex 字符
const rawSignature = r + s
// 尝试使用原始 r|s 格式
const result9 = sm2.doVerifySignature(verifyData.toString(), rawSignature, publicKeyHex)
if (result9) return true
// 尝试使用原始 r|s 格式 + SM3 + userId
const result10 = sm2.doVerifySignature(verifyData.toString(), rawSignature, publicKeyHex, { hash: true, userId })
if (result10) return true
} catch (e) {
// 忽略错误,继续尝试
}
}
} catch (e) {
// 忽略错误,继续尝试其他选项
}
return false
} else {
// RSA 验证(通过 jsrsasign)
try {
const pubKey = x509.getPublicKey()
const sig = new rs.KJUR.crypto.Signature({ alg: 'SHA256withRSA' })
sig.init(pubKey)
sig.updateString(verifyData.toString())
return sig.verify(signer.sighex)
} catch (e) {
throw new Error('RSA verification failed: ' + e.message)
}
}
}
+261
View File
@@ -0,0 +1,261 @@
#!/usr/bin/env node
const { execFileSync } = require("child_process");
const fs = require("fs");
const path = require("path");
function exitError(message, code = 1) {
console.error("ERROR: " + message);
process.exit(code);
}
function run(command, args) {
return execFileSync(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
}
function ensureDocker() {
try {
run("docker", ["version", "--format", "{{.Server.Version}}"]);
} catch (e) {
exitError("docker command is not executable, or Docker daemon is unavailable");
}
}
function inspectContainer(container) {
try {
const text = run("docker", ["inspect", container]);
const data = JSON.parse(text);
if (!Array.isArray(data) || data.length === 0) {
exitError("container not found: " + container);
}
return data[0];
} catch (e) {
if (e instanceof SyntaxError) {
exitError("docker inspect output is not valid JSON");
}
exitError("docker inspect failed: " + container);
}
}
function quoteSh(value) {
if (value === undefined || value === null) {
return "''";
}
const text = String(value);
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(text)) {
return text;
}
return "'" + text.replace(/'/g, "'\\''") + "'";
}
function quoteMountSpec(value) {
const text = String(value);
if (text.startsWith("$(pwd)")) {
const rest = text.slice("$(pwd)".length).replace(/(["\\`$])/g, "\\$1");
return '"$(pwd)' + rest + '"';
}
return quoteSh(text);
}
function normalizeContainerName(name) {
return (name || "container").replace(/^\/+/, "") || "container";
}
function isLikelyProjectPath(source) {
if (!source) return false;
const normalized = source.replace(/\\/g, "/").replace(/\/+$/, "");
if (normalized === "/") return false;
if (/^[A-Za-z]:$/i.test(normalized)) return false;
const specialPrefixes = [
"/bin", "/boot", "/dev", "/etc", "/lib", "/lib64", "/proc", "/run", "/sbin", "/sys", "/tmp", "/usr", "/var",
"/opt/docker", "/var/lib/docker", "/var/run/docker.sock",
"C:/Program Files", "C:/Program Files (x86)", "C:/ProgramData", "C:/Windows"
];
return !specialPrefixes.some(prefix => normalized === prefix || normalized.startsWith(prefix + "/"));
}
function relativeToPwdIfProject(source) {
if (!isLikelyProjectPath(source)) {
return source;
}
const normalized = source.replace(/\\/g, "/").replace(/\/+$/, "");
const base = path.posix.basename(normalized);
if (!base || base === "." || base === "/") {
return source;
}
return "$(pwd)/" + base;
}
function collectEnv(config) {
const env = Array.isArray(config.Env) ? config.Env : [];
return env
.filter(item => typeof item === "string" && item.length > 0)
.map(item => " -e " + quoteSh(item) + " \\");
}
function collectPorts(networkSettings, hostConfig) {
const lines = [];
const seen = new Set();
const ports = Object.assign({}, networkSettings && networkSettings.Ports ? networkSettings.Ports : {});
const bindings = hostConfig && hostConfig.PortBindings ? hostConfig.PortBindings : {};
for (const containerPort of Object.keys(bindings)) {
if (!ports[containerPort]) ports[containerPort] = bindings[containerPort];
}
for (const containerPort of Object.keys(ports).sort()) {
const entries = ports[containerPort];
if (!Array.isArray(entries)) continue;
for (const entry of entries) {
if (!entry || !entry.HostPort) continue;
const hostIp = entry.HostIp && entry.HostIp !== "0.0.0.0" && entry.HostIp !== "::" ? entry.HostIp + ":" : "";
const spec = hostIp + entry.HostPort + ":" + containerPort;
if (seen.has(spec)) continue;
seen.add(spec);
lines.push(" -p " + quoteSh(spec) + " \\");
}
}
return lines;
}
function collectMounts(info) {
const mounts = Array.isArray(info.Mounts) ? info.Mounts : [];
const lines = [];
for (const mount of mounts) {
if (!mount || !mount.Destination) continue;
if (mount.Type === "volume" && mount.Name) {
let spec = mount.Name + ":" + mount.Destination;
if (mount.RW === false) spec += ":ro";
lines.push(" -v " + quoteMountSpec(spec) + " \\");
continue;
}
if (mount.Source) {
let spec = relativeToPwdIfProject(mount.Source) + ":" + mount.Destination;
if (mount.RW === false) spec += ":ro";
lines.push(" -v " + quoteMountSpec(spec) + " \\");
}
}
return lines;
}
function collectNetworks(info) {
const hostConfig = info.HostConfig || {};
const networkMode = hostConfig.NetworkMode;
const lines = [];
if (networkMode && networkMode !== "default" && networkMode !== "bridge") {
lines.push(" --network " + quoteSh(networkMode) + " \\");
return lines;
}
const networkSettings = info.NetworkSettings || {};
const networks = networkSettings.Networks ? Object.keys(networkSettings.Networks) : [];
if (networks.length === 1 && networks[0] !== "bridge") {
lines.push(" --network " + quoteSh(networks[0]) + " \\");
}
return lines;
}
function collectSimpleOptions(info) {
const config = info.Config || {};
const hostConfig = info.HostConfig || {};
const lines = [];
if (hostConfig.RestartPolicy && hostConfig.RestartPolicy.Name && hostConfig.RestartPolicy.Name !== "no") {
let restart = hostConfig.RestartPolicy.Name;
if (restart === "on-failure" && hostConfig.RestartPolicy.MaximumRetryCount) {
restart += ":" + hostConfig.RestartPolicy.MaximumRetryCount;
}
lines.push(" --restart " + quoteSh(restart) + " \\");
}
if (config.WorkingDir) lines.push(" -w " + quoteSh(config.WorkingDir) + " \\");
if (config.User) lines.push(" -u " + quoteSh(config.User) + " \\");
if (hostConfig.Privileged) lines.push(" --privileged \\");
if (config.Tty) lines.push(" -t \\");
if (config.OpenStdin) lines.push(" -i \\");
return lines;
}
function commandTail(config) {
let entrypoint = [];
if (Array.isArray(config.Entrypoint)) {
entrypoint = config.Entrypoint.filter(item => item !== null && item !== undefined).map(String);
} else if (typeof config.Entrypoint === "string" && config.Entrypoint) {
entrypoint = [config.Entrypoint];
}
const cmd = Array.isArray(config.Cmd) ? config.Cmd : (typeof config.Cmd === "string" && config.Cmd ? [config.Cmd] : []);
return {
entrypoint: entrypoint.length > 0 ? entrypoint[0] : null,
args: entrypoint.slice(1).concat(cmd)
};
}
function buildScript(info) {
const config = info.Config || {};
const hostConfig = info.HostConfig || {};
const name = normalizeContainerName(info.Name);
const image = config.Image || info.Image || "";
const tail = commandTail(config);
const lines = [
"#!/usr/bin/env bash",
"set -euo pipefail",
"",
"name=" + quoteSh(name),
"img=" + quoteSh(image),
"",
"docker run -d \\",
" --name \"$name\" \\",
];
lines.push(...collectSimpleOptions(info));
lines.push(...collectEnv(config));
lines.push(...collectPorts(info.NetworkSettings || {}, hostConfig));
lines.push(...collectMounts(info));
lines.push(...collectNetworks(info));
if (tail.entrypoint) {
lines.push(" --entrypoint " + quoteSh(tail.entrypoint) + " \\");
}
const cmd = tail.args.map(quoteSh).join(" ");
if (cmd) {
lines.push(" \"$img\" " + cmd);
} else {
lines.push(" \"$img\"");
}
lines.push("");
return lines.join("\n");
}
function outputFile(container, explicitOutput) {
if (explicitOutput) return explicitOutput;
const safe = container.replace(/[^A-Za-z0-9_.-]+/g, "_").replace(/^_+|_+$/g, "") || "container";
return path.resolve(process.cwd(), "docker-run-" + safe + ".sh");
}
function main() {
const args = process.argv.slice(2);
if (args.length < 1 || args[0] === "-h" || args[0] === "--help") {
console.log("Usage: node restore-run.js <container-id-or-name> [output-file]");
process.exit(args.length < 1 ? 1 : 0);
}
ensureDocker();
const container = args[0];
const info = inspectContainer(container);
const script = buildScript(info);
const file = outputFile(container, args[1]);
fs.writeFileSync(file, script, { mode: 0o755 });
console.log(file);
}
if (require.main === module) {
main();
}
module.exports = {
buildScript,
main
};
+443
View File
@@ -0,0 +1,443 @@
/**
* kit cli 注册和调用执行
*/
require("./init")
const { CliCmdParser } = require("../../argv")
const parser = new CliCmdParser("kit V" + $version + " build " + $build);
const net = require("./net")
const shell = require("./shell")
const {sm4en3, sm4de3} = require("./crypto");
const convutil = require("./util/convutil")
const path = require("path");
const fs = require("fs");
const {sm3} = require("sm-crypto");
const zlib = require("zlib");
const mem = require("./mem")
const { getAllDeviceInfo } = require("./platform")
const kitcommand = require("./kitcommand")
kitcommand.init()
kitcommand.regTo( parser )
// parser.addCmdLine("man", "打开在线帮助;", async function (){
// const uConfig = await $context.get("uConfig")
// shell.execSync("start " + uConfig.get("man"))
// });
// parser.addCmdLine("manu", "人工维护配置;", function (){
// shell.execSync("start " + $sConfig.getUserConfigPath())
// });
//
// parser.addCmdLine("run [--file] [func]", "运行项目脚本;", function (cli) {
// $logger.info( "kit 当前执行目录 " + process.cwd());
// const fs = require("fs");
//
// let step = cli.getParamValue("func");
// if ( ! step ) {
// step = "main"
// }
//
// let file = cli.getParamValue("file");
// if ( ! file ) {
// file = "kit"
// }
//
// const stat = fs.existsSync(`./${file}.js`)
// if ( ! stat ) {
// if ( ! cli.getParamValue("file") ) {
// fs.writeFileSync("./kit.js",
// `
// async function main(){
// console.log("hello kit!")
// }
// `);
// } else {
// $logger.info( `文件 ${file} 不存在` );
// return
// }
// }
//
//
// const projectScript = fs.readFileSync("./kit.js", "utf-8");
// const vm = require('node:vm');
// const script = new vm.Script(projectScript + ";" + step + "()");
// const context = {
// animal: 'cat',
// count: 2,
// $logger,
// shell
// };
//
// script.runInNewContext(context, {
// timeout : 10 * 60 * 1000,
// breakOnSigint : true,
// });
// $logger.info( "暂未实现" );
// });
//
// parser.addCmdLine("config <key> [value]", "查询/设置配置;", async function (cli){
// let key = cli.getParamValue("key");
// let value = cli.getParamValue("value");
// const uConfig = await $context.get("uConfig")
// if (null != key && null == value) {
// let v = uConfig.get(key);
// $logger.info("{} : {}", key, v);
// } else if (null != key && null != value) {
// uConfig.set(key, value);
// $logger.info("set {} : {}", key, value);
// } else {
// $logger.error("参数异常");
// }
// });
//
// parser.addCmdLine("remote-check", "验证远程仓库;", async function (){
// let remote = await $context.get("remote")
// if (! ( await remote.check()) ) {
// $logger.error("远程仓库不可用")
// return
// } else {
// $logger.info("远程仓库可用")
// }
// });
//
// parser.addCmdLine("publish", "发布到远程仓库;", async function (){
// let remote = await $context.get("remote")
// if (! ( await remote.check()) ) {
// $logger.error("远程仓库不可用")
// return
// } else {
// $logger.info("远程仓库可用")
// }
// remote.publish()
// });
// parser.addCmdLine("install [module]", "安装/更新模块;", async function (cli){
// let remote = await $context.get("remote")
// if (! ( await remote.check()) ) {
// $logger.error("远程仓库不可用")
// return
// } else {
// $logger.info("远程仓库可用")
// }
// remote.install(cli.getParamValue("module"))
// });
parser.addCmdLine("platform [-json]", "平台信息", function (cli) {
// 调用主函数并打印结果
getAllDeviceInfo().then(info => {
console.log('Device Information:');
let table = []
for ( let k in info.os ){
table.push({ '项' : 'OS ' + k, '值' : info.os[k]})
}
for ( let k in info.cpu ){
table.push({ '项' : 'CPU ' + k, '值' : info.cpu[k]})
}
for ( let k in info.memory ){
if ( k.endsWith("Size") ) {
continue;
}
table.push({ '项' : 'Mem ' + k, '值' : info.memory[k]})
}
if( cli.hasFlag("json") ) {
console.log( JSON.stringify(info, null, 4));
} else {
console.table(table);
const disks = info.disk;
const diskTable = []
disks.forEach(disk => {
diskTable.push({
'盘符' : disk.mount
,'总空间' : `${(disk.size / 1024 / 1024 / 1024).toFixed(2)} GB`
,'已用空间' : `${(disk.used / 1024 / 1024 / 1024).toFixed(2)} GB`
,'可用空间' : `${(disk.available / 1024 / 1024 / 1024).toFixed(2)} GB`
,'使用率' : `${disk.use}%`
})
});
console.table(diskTable);
}
}).catch(error => {
console.error('Error getting device information:', error);
});
});
parser.addCmdLine("freem [level]", "释放内存", function (cli) {
let level = cli.getParamValue("level")
mem.free( level )
});
parser.addCmdLine("docker runsh <container> [--file]", "反向获取 docker 容器启动脚本", function (cli) {
let container = cli.getParamValue("container")
let file = cli.getParamValue("file")
import("./docker/index.js").then(psl => psl.main(container, file))
});
parser.addCmdLine("killport <port>", "释放端口", function (cli) {
let port = cli.getParamValue("port")
net.killport( parseInt(port, 10) );
});
parser.addCmdLine("getport [--port]", "获取一个可用端口;或查询指定端口状态;", async function (cli) {
const getPort = await import('get-port').then(psl => psl.default);
let port = cli.getParamValue("port")
if( ! port ) {
let ret = await getPort()
$logger.info(`端口 ${ret} 未被使用`)
} else {
await net.getport( port )
}
});
parser.addCmdLine("checkip", "获取自己的公网 ip;", async function (cli) {
let ret = await net.checkip()
if( "ok" === ret.ret ) {
$logger.info( "ip : " + ret.data.ip )
$logger.info( "location : " + ret.data.location.join(" ") )
} else {
$logger.error("获取失败")
}
});
parser.addCmdLine("scan <ip-and-mask> [ports]", "扫描局域网;", async function (cli) {
let ipAndMask = cli.getParamValue("ip-and-mask")
let ports = cli.getParamValue("ports")
let ret = await net.scan(ipAndMask, ports)
$logger.info( JSON.stringify(ret, null, 4) )
});
parser.addCmdLine("telnet [--timeout] <ip-port> [port]", "验证远程 TCP 端口是否可用;", async function (cli) {
let ip = cli.getParamValue("ip-port")
let port = 80;
// 允许冒号形式指定端口
const re = /^(?<ip>.*):(?<port>[0-9]+)$/
const match = ip.match( re );
if ( match ) {
ip = match.groups.ip;
port = match.groups.port;
} else {
// 如果指定了 port 参数,那么使用 port 参数
let dport = cli.getParamValue("port")
if ( dport ) {
port = dport;
}
}
// 支持不填写 ip,默认访问本地
if( "" === ip.trim() ) {
ip = "127.0.0.1";
}
try {
await net.telnet(port, ip, cli.getParamValue("timeout"))
$logger.info("连接成功")
process.exit(0)
}catch (e) {
$logger.error( e )
process.exit(-1)
}
});
parser.addCmdLine("checklist <checkListFile>", "生成/验证文件 hash", async function (cli) {
let checkListFile = cli.getParamValue("checkListFile")
if ( !fs.existsSync( checkListFile ) ) {
$logger.error(`file ${checkListFile} 不存在`)
process.exit(-2)
}
const sm3 = require('sm-crypto').sm3
const list = fs.readFileSync(checkListFile, "UTF-8")
const files = list.split('\n').filter(line => line.trim() !== '');
for ( let file of files ) {
const infos = file.split(/\s+/)
const info = {}
if ( infos.length > 2 ) {
info.file = infos[0]
info.hash = infos[1]
info.alg = infos[2]
} else {
info.file = infos[0]
info.hash = infos[1]
info.alg = "md5"
}
if ( "sm3" === info.alg ) {
if ( !fs.existsSync( info.file ) ) {
$logger.error(`file ${info.file} 不存在`)
process.exit(-2)
}
const data = fs.readFileSync(info.file);
const arrayBuffer = Buffer.from(data);
const uint8Array = new Uint8Array(arrayBuffer);
let hashData = sm3( uint8Array ) // 杂凑
if ( hashData === info.hash ) {
$logger.info(`file ${file} 验证通过`)
} else {
$logger.error(`file ${file} 验证不通过`)
process.exit(-400)
}
} else {
$logger.error(`不支持 file ${file} `)
process.exit(-2)
}
}
$logger.info(`${checkListFile} 中的文件全部验证通过`)
})
parser.addCmdLine("sm3 [--file] [--text] [--hash]", "生成/验证 SM3 值;", async function (cli) {
let filePath = cli.getParamValue("file")
let text = cli.getParamValue("text")
if( !filePath && !text ) {
$logger.error(`file 和 text 入参不能同时有`)
process.exit(-1)
}
const sm3 = require('sm-crypto').sm3
if ( text ) {
let hashData = sm3( text ) // 杂凑
$logger.info( hashData )
}
if ( filePath ) {
if ( !fs.existsSync( filePath ) ) {
$logger.error(`file ${filePath} 不存在`)
process.exit(-2)
}
const data = fs.readFileSync(filePath);
const arrayBuffer = Buffer.from(data);
const uint8Array = new Uint8Array(arrayBuffer);
let hashData = sm3( uint8Array ) // 杂凑
$logger.info( hashData )
}
});
parser.addCmdLine("sm4en3 <key> <data>", "sm4 加密,密钥通过 sm3 制备;", function (cli) {
const {sm4en3} = require("./crypto");
let key = cli.getParamValue("key")
let data = cli.getParamValue("data")
let out = sm4en3( key, data )
$logger.info( out );
});
parser.addCmdLine("sm4de3 <key> <data>", "sm4 解密,密钥通过 sm3 制备;", function (cli) {
const {sm4de3} = require("./crypto");
let key = cli.getParamValue("key")
let data = cli.getParamValue("data")
let out = sm4de3( key, data )
$logger.info( out );
});
parser.addCmdLine("pkcs7verify <sign> <data>", "验证 pkCS7 签名;", function (cli) {
const {verifyPKCS7} = require("./crypto");
let sign = cli.getParamValue("sign")
let data = cli.getParamValue("data")
let out = verifyPKCS7( sign, data )
if ( out ) {
$logger.info("验证通过")
} else {
$logger.error("验证不通过")
}
});
parser.addCmdLine("pkcs7info <sign>", "从 pkCS7 签名中提取信息(证书、公钥、签名);", function (cli) {
const {getInfoFromPKCS7} = require("./crypto");
let sign = cli.getParamValue("sign")
let out = getInfoFromPKCS7( sign )
$logger.info(' cert:');
$logger.info(' subject:', result.cert?.subjectDN);
$logger.info(' issuer:', result.cert?.issuerDN);
$logger.info(' serialNumber:', result.cert?.serialNumber);
$logger.info(' notBefore:', result.cert?.notBefore);
$logger.info(' notAfter:', result.cert?.notAfter);
$logger.info(' pem:');
$logger.info(result.cert?.pem);
$logger.info(' publicKey:');
$logger.info(result.publicKey);
$logger.info(' publicKey (hex):');
$logger.info(result.publicKeyHex);
$logger.info(' signature (hex):');
$logger.info(result.signatureHex);
});
// parser.addCmdLine("base64 <-en/-de> <data>", "base64 编解码", function (cli) {
parser.addCmdLine("base64 [-en] [-de] [-hex] <data>", "base64 编解码;", function (cli) {
let data = cli.getParamValue("data")
if ( cli.hasFlag("de") ) {
if ( cli.hasFlag("hex") ) {
$logger.info( "执行 base64 decode to hex: " );
$logger.info( convutil.base64ToHex( data ) );
} else {
$logger.info( "执行 base64 decode to string: " );
$logger.info( convutil.base64ToString( data ) );
}
} else {
if ( cli.hasFlag("hex") ) {
$logger.info( "执行 hex encode to base64: " );
$logger.info( convutil.hexToBase64( data ) );
} else {
$logger.info( "执行 string encode to base64: " );
$logger.info( convutil.stringToBase64( data ) );
}
}
});
parser.addCmdLine("touch [-d] <file_name>", "文件时间修改为当前时间;", function (cli) {
const fs = require('fs');
const path = cli.getParamValue("file_name")
// 获取当前日期时间
const now = new Date();
// 修改文件的访问时间和修改时间为当前时间
fs.utimes(path, now, now, (err) => {
if (err) {
$logger.error('修改文件时间出错:', err);
} else {
$logger.info('文件时间修改成功');
}
});
});
parser.addCmdLine("replace [-d] <file_name> <re1> <re2>", "文件内容正则替换;", function (cli) {
$logger.info( "暂未实现" );
});
parser.addCmdLine("crlf [-d] [-r] [--path] [--filter-name] [--filter-path] <crlf:{CRLF,CR,LF,\\r\\n,\\r,\\n}>", "回车一致化", function (cli) {
$logger.info( "暂未实现" );
});
parser.addCmdLine("tcpc [-d] [-x] [--host] [--ip] [--port] [--headlen] [--timeout] [--filepath]", "tcp 报文发收", function (cli) {
$logger.info( "暂未实现" );
});
async function main(){
try {
let cmd = parser.parse(process.argv.slice(2))
if ( ! cmd ) {
console.log( parser.help() )
} else {
$logger.info(`执行命令 ${cmd.cmdline.cmd}`)
if ( cmd.canEval() ){
await cmd.eval()
} else {
console.log( parser.help() )
}
}
} catch ( e ) {
console.error( e )
}
}
main()
+103
View File
@@ -0,0 +1,103 @@
const config = require('./config')
const Context = require("./context")
// const layout = log4js.layouts.patternLayout('%m')
if ( ! global.$inited ) {
init()
} else {
// 不再初始化
}
function init(){
global.$inited = true
const package = require("../package.json")
global.$version = package.version
const builder = require("../build.json")
global.$build = builder.buildTime
// 上下文模块
global.$context = new Context();
// 载入日志
const log4js = require('log4js');
log4js.configure({
appenders: {
console: { type: 'stdout' , level: "info", layout: {
type: "pattern",
pattern: "%m",
},},
},
categories: {
default: { appenders: ['console'], level: 'debug' },
}
});
global.$logger = log4js.getLogger();
// 载入配置
global.$sConfig = config.SystemConfig
// 重载版本号
try {
global.$version = fs.readFileSync($sConfig.getLocalKitPath() + "/version", "UTF-8")
}catch ( e ) {}
let logPath = global.$sConfig.getLogPath();
global.$context.standalone("uConfig", function (){
return new config.UserConfig( $sConfig.getUserConfigFilePath() )
})
log4js.addLayout('customLayout', function(config) {
return function(logEvent) {
const now = new Date(logEvent.startTime)
let time = now.toLocaleTimeString();
time += "." + now.getMilliseconds().toString().padStart(3, '0');
const level = "" + logEvent.level;
const categoryName = logEvent.categoryName;
const message = logEvent.data[0];
if ( message && message.startsWith && message.startsWith("\r") ) {
return message;
}
let logMessage = `${time} ${level} - ${message}`;
// Add color to log message based on log level
switch (level) {
case 'INFO':
logMessage = `\x1b[32m${logMessage}\x1b[0m`; // Green color for INFO log level
break;
case 'WARN':
logMessage = `\x1b[33m${logMessage}\x1b[0m`; // Yellow color for WARN log level
break;
case 'ERROR':
logMessage = `\x1b[31m${logMessage}\x1b[0m`; // Red color for ERROR log level
break;
default:
break;
}
return logMessage;
};
});
// 重置日志配置
log4js.configure({
appenders: {
console: { type: 'console' , level: "info", layout: {
type: 'customLayout'
}},
file: { type: 'dateFile', filename: logPath + '/kit.log', "pattern": "yyyy-MM-dd", "compress": true , alwaysIncludePattern : true, keepFileExt : true, numBackups : 7}
},
categories: {
default: { appenders: ['console', 'file'], level: 'debug' },
}
});
// 注册:远程仓库
global.$context.standalone("remote", async function (){
const Remote = require("./remote")
const remote = new Remote();
await remote.init()
return remote
})
}
+170
View File
@@ -0,0 +1,170 @@
const shell = require("./shell");
const os = require("os");
const kitfunction = require("./kitfunction");
async function getAvailableRemote() {
const remote = await $context.get("remote");
if (!(await remote.check())) {
$logger.error("远程仓库不可用");
return null;
}
return remote;
}
module.exports = {
init : function () {
return this;
},
regTo: function (parser) {
parser.addCmdLine("help", "打开在线帮助;", async function () {
const uConfig = await $context.get("uConfig")
if (os.type() === 'Windows_NT') {
shell.execSync("start " + uConfig.get("man"))
} else {
console.log("请用浏览器访问 " + uConfig.get("man"))
}
});
parser.addCmdLine("manu", "人工维护配置;", function () {
if (os.type() === 'Windows_NT') {
shell.execSync("start " + $sConfig.getUserConfigPath())
} else {
console.log("cd " + $sConfig.getUserConfigPath())
}
});
parser.addCmdLine("run [--file] [func]", "运行项目脚本;", function (cli) {
$logger.info("kit 当前执行目录 " + process.cwd());
const fs = require("fs");
let step = cli.getParamValue("func");
if (!step) {
step = "main"
}
let file = cli.getParamValue("file");
if (!file) {
file = "kitx"
}
const scriptFile = `./${file}.js`;
const stat = fs.existsSync(scriptFile)
if (!stat) {
if (!cli.getParamValue("file")) {
fs.writeFileSync("./kitx.js", `
async function main(){
console.log("hello kit!");
}
`);
} else {
$logger.info(`文件 ${file} 不存在`);
return
}
}
const projectScript = fs.readFileSync(scriptFile, "utf-8");
const vm = require('vm');
const script = new vm.Script(projectScript + ";" + step + "()");
const context = kitfunction;
script.runInNewContext(context, {
timeout: 10 * 60 * 1000, breakOnSigint: true,
});
$logger.info("执行结束");
});
parser.addCmdLine("config <key> [value]", "查询/设置配置;", async function (cli) {
let key = cli.getParamValue("key");
let value = cli.getParamValue("value");
const uConfig = await $context.get("uConfig")
if (null != key && null == value) {
let v = uConfig.get(key);
$logger.info("{} : {}", key, v);
} else if (null != key && null != value) {
uConfig.set(key, value);
$logger.info("set {} : {}", key, value);
} else {
$logger.error("参数异常");
}
});
parser.addCmdLine("remote", "验证远程仓库可达;", async function () {
const remote = await getAvailableRemote();
if (remote) {
$logger.info("远程仓库可用");
}
});
parser.addCmdLine("check", "验证远程仓库可达;", async function () {
const remote = await getAvailableRemote();
if (remote) {
$logger.info("远程仓库可用");
}
});
parser.addCmdLine("publish [localFile] [remotePath]", "发布到远程仓库; localFile 为空时使用当前目录 meta.json", async function (cli) {
const fs = require("fs");
let localFile = cli.getParamValue("localFile");
let remotePath = cli.getParamValue("remotePath");
if (localFile && !fs.existsSync(localFile)) {
$logger.error("本地文件不存在");
return;
}
if (!localFile && !fs.existsSync("meta.json")) {
$logger.error("没有找到本地配置 meta.json");
return;
}
const remote = await getAvailableRemote();
if (!remote) {
return;
}
if (remotePath) {
$logger.info("发布到远程路径 " + remotePath);
} else {
$logger.info("使用 meta.json 中的 group/id/version 规则发布");
}
await remote.publish(localFile, remotePath);
$logger.info("发布成功");
});
parser.addCmdLine("install [module]", "安装/更新模块; 支持 myapp、myapp@1.0、/docker/net/myapp", async function (cli) {
const remote = await getAvailableRemote();
if (!remote) {
return;
}
await remote.install(cli.getParamValue("module"));
});
parser.addCmdLine("list [module]", "列出本地已安装版本;", async function (cli) {
let module = cli.getParamValue("module");
if (!module) {
module = "kit";
}
const remote = await $context.get("remote");
await remote.list(module);
});
parser.addCmdLine("use <module-version>", "切换到已安装版本,例如 myapp@1.0;", async function (cli) {
const remote = await $context.get("remote");
const version = await remote.use(cli.getParamValue("module-version"));
$logger.info("当前版本 " + version);
});
parser.addCmdLine("rollback <module>", "回滚到上一个已安装版本;", async function (cli) {
const remote = await $context.get("remote");
const version = await remote.rollback(cli.getParamValue("module"));
$logger.info("当前版本 " + version);
});
parser.addCmdLine("remove <module-version>", "删除本地已安装版本,例如 myapp@1.0;", async function (cli) {
const remote = await $context.get("remote");
await remote.remove(cli.getParamValue("module-version"));
$logger.info("删除成功");
});
}
}
+413
View File
@@ -0,0 +1,413 @@
const crypto = require("crypto");
const fs = require("fs");
const os = require("os");
const path = require("path");
const platform = os.type() === "Windows_NT" ? require("./windows/kitfunction") : require("./linux/kitfunction");
const vars = Object.create(null);
function setVar(name, value) {
if (name) {
vars[name] = value;
}
return value;
}
function getVar(name) {
return vars[name];
}
function resolveValue(value) {
if (typeof value !== "string") {
return value;
}
const varMatch = value.match(/^%([A-Za-z_][A-Za-z0-9_]*)%$/);
if (varMatch) {
return getVar(varMatch[1]);
}
const randomMatch = value.match(/^%random\((number),(\d+)\)%$/);
if (randomMatch) {
const length = Number(randomMatch[2]);
let result = "";
while (result.length < length) {
result += crypto.randomInt(0, 10).toString();
}
return result;
}
return value;
}
function info(msg) {
const text = String(resolveValue(msg) ?? "");
const line = "=".repeat(Math.max(24, text.length + 8));
console.log(`\n\x1b[36m${line}\x1b[0m`);
console.log(`\x1b[1m\x1b[33m>>> ${text}\x1b[0m`);
console.log(`\x1b[36m${line}\x1b[0m\n`);
}
function mkdir(dirPath) {
if (!dirPath) {
throw new Error("mkdir path is required");
}
fs.mkdirSync(path.resolve(dirPath), { recursive: true });
}
function rm(targetPath) {
if (!targetPath) {
throw new Error("rm path is required");
}
fs.rmSync(path.resolve(targetPath), { recursive: true, force: true });
}
function copyDir(from, to) {
fs.mkdirSync(to, { recursive: true });
for (const item of fs.readdirSync(from, { withFileTypes: true })) {
const source = path.join(from, item.name);
const destination = path.join(to, item.name);
if (item.isDirectory()) {
copyDir(source, destination);
} else if (item.isSymbolicLink()) {
fs.rmSync(destination, { recursive: true, force: true });
fs.symlinkSync(fs.readlinkSync(source), destination);
} else {
fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.copyFileSync(source, destination);
}
}
}
function cp(from, to) {
if (!from || !to) {
throw new Error("cp from and to are required");
}
const source = path.resolve(from);
if (!fs.existsSync(source)) {
return;
}
let destination = path.resolve(to);
const stat = fs.statSync(source);
if (!stat.isDirectory() && fs.existsSync(destination) && fs.statSync(destination).isDirectory()) {
destination = path.join(destination, path.basename(source));
}
fs.mkdirSync(path.dirname(destination), { recursive: true });
if (stat.isDirectory()) {
rm(destination);
copyDir(source, destination);
} else {
fs.copyFileSync(source, destination);
}
}
function mv(from, to) {
if (!from || !to) {
throw new Error("mv from and to are required");
}
const source = path.resolve(from);
if (!fs.existsSync(source)) {
return;
}
let destination = path.resolve(to);
const stat = fs.statSync(source);
if (!stat.isDirectory() && fs.existsSync(destination) && fs.statSync(destination).isDirectory()) {
destination = path.join(destination, path.basename(source));
}
fs.mkdirSync(path.dirname(destination), { recursive: true });
rm(destination);
try {
fs.renameSync(source, destination);
} catch (e) {
cp(source, destination);
rm(source);
}
}
function normalizeList(value) {
if (!value) {
return [];
}
return Array.isArray(value) ? value : [value];
}
function normalizeSlash(value) {
return value.replace(/\\/g, "/");
}
function matchRule(relativePath, rule) {
if (typeof rule === "function") {
return rule(relativePath);
}
if (rule instanceof RegExp) {
return rule.test(relativePath);
}
if (typeof rule === "string") {
const text = normalizeSlash(rule);
return relativePath === text || relativePath.startsWith(text.endsWith("/") ? text : `${text}/`) || relativePath.includes(text);
}
return false;
}
function shouldInclude(relativePath, params) {
const normalized = normalizeSlash(relativePath);
const filters = normalizeList(params.filter);
const excludes = normalizeList(params.execute || params.exclude);
if (filters.length > 0 && !filters.some((rule) => matchRule(normalized, rule))) {
return false;
}
if (excludes.some((rule) => matchRule(normalized, rule))) {
return false;
}
return true;
}
function copyFiltered(source, tempRoot, params) {
const sourceStat = fs.statSync(source);
const baseName = path.basename(source);
const root = sourceStat.isDirectory() ? source : path.dirname(source);
function walk(current) {
const relative = path.relative(root, current) || baseName;
const target = path.join(tempRoot, relative);
const stat = fs.statSync(current);
if (!shouldInclude(relative, params)) {
return;
}
if (stat.isDirectory()) {
fs.mkdirSync(target, { recursive: true });
for (const name of fs.readdirSync(current)) {
walk(path.join(current, name));
}
} else {
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.copyFileSync(current, target);
}
}
walk(source);
}
function zip(from, to, params = {}) {
if (!from || !to) {
throw new Error("zip from and to are required");
}
const source = path.resolve(from);
if (!fs.existsSync(source)) {
return;
}
const options = Object.assign({}, params, {
type: params.type || "zip",
password: resolveValue(params.password)
});
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "kit-zip-"));
try {
copyFiltered(source, tempRoot, options);
platform.zip(tempRoot, to, options);
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
}
function md5(filePath, params = {}) {
const target = filePath ? path.resolve(filePath) : "";
let value = "0000000000000000";
if (target && fs.existsSync(target) && fs.statSync(target).isFile()) {
value = crypto.createHash("md5").update(fs.readFileSync(target)).digest("hex");
}
return setVar(params.var, value);
}
function parseKey(key) {
if (!key) {
return [];
}
return String(key).split(".").filter((item) => item.length > 0).map((item) => /^\d+$/.test(item) ? Number(item) : item);
}
function readJsonValue(data, key) {
let current = data;
for (const item of parseKey(key)) {
if (current == null) {
return undefined;
}
current = current[item];
}
return current;
}
function ensureContainer(parent, key, nextKey) {
if (parent[key] == null || typeof parent[key] !== "object") {
parent[key] = typeof nextKey === "number" ? [] : {};
}
}
function writeJsonValue(data, key, value) {
const keys = parseKey(key);
if (keys.length === 0) {
return value;
}
let current = data;
for (let index = 0; index < keys.length - 1; index++) {
ensureContainer(current, keys[index], keys[index + 1]);
current = current[keys[index]];
}
current[keys[keys.length - 1]] = value;
return data;
}
function readJson(filePath, key, params = {}) {
if (!filePath) {
throw new Error("readJson filePath is required");
}
const data = JSON.parse(fs.readFileSync(path.resolve(filePath), "utf-8"));
return setVar(params.var, readJsonValue(data, key));
}
function writeJson(filePath, key, value) {
if (!filePath) {
throw new Error("writeJson filePath is required");
}
const target = path.resolve(filePath);
let data = {};
if (fs.existsSync(target)) {
data = JSON.parse(fs.readFileSync(target, "utf-8"));
}
const nextData = writeJsonValue(data, key, resolveValue(value));
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.writeFileSync(target, JSON.stringify(nextData, null, 4), "utf-8");
return nextData;
}
function resolvePlainPassword(params) {
if (params.plainPwd !== undefined) {
return params.plainPwd;
}
if (params.plainPassword !== undefined) {
return params.plainPassword;
}
if (params.plaintextPwd !== undefined) {
return params.plaintextPwd;
}
if (params.plaintextPassword !== undefined) {
return params.plaintextPassword;
}
return undefined;
}
function resolveScpPassword(params) {
const plainPassword = resolvePlainPassword(params);
if (plainPassword !== undefined) {
return plainPassword;
}
return resolveValue(params.pwd || params.password);
}
function scp(from, to, params = {}) {
if (!from || !to) {
throw new Error("scp from and to are required");
}
const pwd = resolveScpPassword(params);
platform.scp(from, to, Object.assign({}, params, {
pwd,
password: pwd,
key: resolveValue(params.key || params.identityFile),
identityFile: resolveValue(params.identityFile || params.key)
}));
}
function exit(code = 0) {
process.exit(code);
}
function isSecretLogKey(key) {
return /^(pwd|password|plainPwd|plainPassword|plaintextPwd|plaintextPassword)$/i.test(key);
}
function redactLogValue(value) {
if (Array.isArray(value)) {
return value.map(redactLogValue);
}
if (value && typeof value === "object") {
const result = {};
for (const key of Object.keys(value)) {
result[key] = isSecretLogKey(key) ? "******" : redactLogValue(value[key]);
}
return result;
}
return value;
}
function stringifyLogValue(value) {
if (typeof value === "string") {
return value;
}
if (value === undefined) {
return "undefined";
}
if (typeof value === "function") {
return `[Function ${value.name || "anonymous"}]`;
}
try {
return JSON.stringify(redactLogValue(value));
} catch (e) {
return String(value);
}
}
function printCallLog(name, args, result) {
console.log(`调用 ${name} 功能`);
console.log(`入参 ${args.map(stringifyLogValue).join(" ")}`);
if (result === undefined) {
console.log("执行结束");
} else {
console.log(`执行结果 ${stringifyLogValue(result)}`);
}
}
function withCallLog(name, fn) {
return function (...args) {
const result = fn.apply(this, args);
printCallLog(name, args, result);
return result;
};
}
module.exports = {
info: withCallLog("info", info),
mkdir: withCallLog("mkdir", mkdir),
rm: withCallLog("rm", rm),
cp: withCallLog("cp", cp),
mv: withCallLog("mv", mv),
zip: withCallLog("zip", zip),
md5: withCallLog("md5", md5),
readJson: withCallLog("readJson", readJson),
writeJson: withCallLog("writeJson", writeJson),
scp: withCallLog("scp", scp),
exit: withCallLog("exit", exit),
setVar: withCallLog("setVar", setVar),
getVar: withCallLog("getVar", getVar),
vars
};
+21
View File
@@ -0,0 +1,21 @@
/**
* 将 kit 或其他文件注册为开机启动
* kit 启动通过 auto-launch 模块实现,其他文件通过 kit 调用系统命令启动。
*/
const AutoLaunch = require('auto-launch');
function reg(){
var launch = new AutoLaunch({
name: 'Kit',
path: 'node D:\\fullstack\\TDevOps\\kit\\src\\index.js launch',
});
launch.enable();
}
reg()
+117
View File
@@ -0,0 +1,117 @@
const { execFileSync } = require("child_process");
const fs = require("fs");
const path = require("path");
function commandExists(command) {
try {
execFileSync("which", [command], { stdio: "ignore" });
return true;
} catch (e) {
return false;
}
}
function run(command, args, options = {}) {
return execFileSync(command, args, Object.assign({ stdio: "inherit" }, options));
}
function addToZip(sourceDir, to, password) {
fs.mkdirSync(path.dirname(path.resolve(to)), { recursive: true });
if (commandExists("zip")) {
const args = ["-r"];
if (password) {
args.push("-P", password);
}
args.push(path.resolve(to), ".");
run("zip", args, { cwd: sourceDir, stdio: "inherit" });
return;
}
if (password) {
throw new Error("Linux password zip requires zip in PATH");
}
const script = `import zipfile, pathlib\nroot = pathlib.Path(${JSON.stringify(path.resolve(sourceDir))})\nout = pathlib.Path(${JSON.stringify(path.resolve(to))})\nout.parent.mkdir(parents=True, exist_ok=True)\nmode = 'a' if out.exists() else 'w'\nwith zipfile.ZipFile(out, mode, zipfile.ZIP_DEFLATED) as z:\n for p in root.rglob('*'):\n if p.is_file():\n z.write(p, p.relative_to(root).as_posix())`;
run("python3", ["-c", script]);
}
function addToTar(sourceDir, to) {
fs.mkdirSync(path.dirname(path.resolve(to)), { recursive: true });
const args = fs.existsSync(to) ? ["-rf", path.resolve(to), "."] : ["-cf", path.resolve(to), "."];
run("tar", args, { cwd: sourceDir, stdio: "inherit" });
}
function zip(sourceDir, to, params = {}) {
if (params.type === "tar") {
addToTar(sourceDir, to);
} else {
addToZip(sourceDir, to, params.password);
}
}
function isRemotePath(value) {
if (typeof value !== "string") {
return false;
}
if (/^[A-Za-z]:[\\/]/.test(value)) {
return false;
}
return /^[^\s:]+:.+/.test(value);
}
function resolveScpPath(value) {
return isRemotePath(value) ? value : path.resolve(value);
}
function appendScpParams(args, params) {
if (params.recursive !== false) {
args.push("-r");
}
if (params.port) {
args.push("-P", String(params.port));
}
if (params.key) {
args.push("-i", params.key);
}
if (params.preserve) {
args.push("-p");
}
if (params.compress) {
args.push("-C");
}
if (params.strictHostKeyChecking !== undefined) {
args.push("-o", `StrictHostKeyChecking=${params.strictHostKeyChecking ? "yes" : "no"}`);
}
if (params.options) {
for (const name of Object.keys(params.options)) {
args.push("-o", `${name}=${params.options[name]}`);
}
}
if (params.args) {
args.push(...(Array.isArray(params.args) ? params.args : [params.args]));
}
}
function scp(from, to, params = {}) {
const source = resolveScpPath(from);
if (!isRemotePath(from) && !fs.existsSync(source)) {
return;
}
const args = [];
appendScpParams(args, params);
args.push(source, resolveScpPath(to));
if (params.pwd) {
if (!commandExists("sshpass")) {
throw new Error("Linux scp with pwd requires sshpass in PATH");
}
run("sshpass", ["-p", params.pwd, "scp", ...args]);
return;
}
run("scp", args);
}
module.exports = { zip, scp };
+5
View File
@@ -0,0 +1,5 @@
function getModuleInfo(){
}
module.exports = { getModuleInfo }
+33
View File
@@ -0,0 +1,33 @@
async function killport(port) {
$logger.info(`kill process on port ${port}`);
const util = require('util');
const exec = util.promisify(require('child_process').exec);
try {
// 查 PID
let { stdout : stdout1 } = exec(`lsof -t -sTCP:LISTEN -i:${port}`)
let pid = stdout1.trim();
$logger.debug(`绑定端口号 ${port} 的进程 PID=${pid}`)
// 查进程信息
let { stdout : stdout2 } = exec(`ps -p ${pid} -o comm=`)
let processName = stdout2.trim();
$logger.info(`绑定端口号=${port}; 进程 PID=${pid}; 程序=${processName};`);
// Kill 进程
exec(`kill -9 ${pid}`)
} catch (e) {
if ( 1 === e.code ) {
$logger.error("此端口未被使用,或清理进程失败。")
return
}
}
}
module.exports = {
killport
}
+22
View File
@@ -0,0 +1,22 @@
const osutil = require("./util/osutil")
const shell = require("./shell")
function free(level) {
osutil.choise({
[osutil.Windows] : ( level ) => {
throw Error("暂不支持")
},
[osutil.Linux] : ( level ) => {
if ( [1,2,3].indexOf( parseInt(level,10)) === -1 ) {
level = 1
}
shell.execSync("sync")
shell.execSync(`echo ${level} > /proc/sys/vm/drop_caches`)
},
})
}
module.exports = {
free
}
+377
View File
@@ -0,0 +1,377 @@
require("./init")
const osutil = require("./util/osutil")
const killer = require('cross-port-killer');
const net = require('net');
const dns = require('dns').promises;
/**
* 模拟 telnet ,检测端口是否开放
* @param port
* @param host
* @param timeout
* @returns {Promise<unknown>}
*/
async function telnet(port, host, timeout=3000){
$logger.debug(`port=${port} host=${host} timeout=${timeout}`)
return new Promise(function (resolve, reject){
const socket = new net.Socket();
socket.setTimeout(timeout, function() {
socket.end();
reject("链接超时")
});
socket.connect(port, host);
socket.on('connect', function() {
resolve('连接成功');
socket.end();
});
socket.on('error', function(err) {
reject('连接失败');
});
})
}
async function killport(port) {
return killer.kill( port );
// let func = osutil.choise([
// require("./windows/net").killport,
// require("./linux/net").killport,
// ])
// return func && func(port);
}
/**
* 获取端口信息
* 因为命令输出缓存问题,有缺陷。
* @param port
* @returns {Promise<void>}
*/
async function getport(port) {
$logger.info( "暂未实现" );
}
async function checkip(){
const url = "https://myip.ipip.net/json"
const http = url.startsWith("https:") ? require("node:https") : require('node:http');
return new Promise((resolve, reject) => {
http.get(url, (res) => {
let data = '';
// A chunk of data has been recieved.
res.on('data', (chunk) => {
data += chunk;
});
// The whole response has been received. Resolve the promise with result
res.on('end', () => {
resolve(JSON.parse(data));
});
}).on("error", (err) => {
// In case of any error reject the promise
reject(err);
});
});
}
/**
* 内网IP端口扫描函数
* @param {string} ipAndMask - IP地址或IP段,如:"192.168.1.1" 或 "192.168.1.0/24"
* @param {string|null} ports - 端口,可以是:null(使用默认端口)、单个端口、多个端口逗号分隔
* @returns {Promise<Array>} - 返回扫描结果数组
*/
async function scan(ipAndMask, ports) {
const defaultPorts = [22, 80, 443, 3389, 8889];
let targetIps = [];
let targetPorts = [];
// 1. 解析端口参数
if (!ports) {
targetPorts = defaultPorts;
} else if (ports.includes(',')) {
targetPorts = ports.split(',').map(p => parseInt(p.trim())).filter(p => p > 0 && p <= 65535);
} else {
targetPorts = [parseInt(ports)];
}
// 2. 解析IP地址/网段
if (ipAndMask.includes('/')) {
// CIDR表示法,如 192.168.1.0/24
targetIps = await parseCIDR(ipAndMask);
} else if (net.isIP(ipAndMask)) {
// 单个IP地址
targetIps = [ipAndMask];
} else {
// 可能是主机名,尝试解析
try {
const addresses = await dns.resolve4(ipAndMask);
targetIps = addresses;
} catch (error) {
throw new Error(`无法解析主机名: ${ipAndMask}`);
}
}
// 3. 扫描所有IP和端口
const scanPromises = targetIps.map(ip =>
scanPortsForIP(ip, targetPorts)
);
const results = await Promise.all(scanPromises);
// 4. 过滤出有开放端口的结果
return results.filter(result => result.openPorts.length > 0);
}
/**
* 解析CIDR格式的IP段
* @param {string} cidr - CIDR格式,如 "192.168.1.0/24"
* @returns {Array} - IP地址数组
*/
function parseCIDR(cidr) {
const [ipStr, maskStr] = cidr.split('/');
const mask = parseInt(maskStr);
if (mask < 0 || mask > 32) {
throw new Error('子网掩码必须在0-32之间');
}
// 将IP转换为32位整数
const ipToInt = (ip) => {
const parts = ip.split('.').map(Number);
return (parts[0] << 24) + (parts[1] << 16) + (parts[2] << 8) + parts[3];
};
// 将32位整数转换为IP字符串
const intToIp = (int) => {
return [
(int >>> 24) & 255,
(int >>> 16) & 255,
(int >>> 8) & 255,
int & 255
].join('.');
};
const ipInt = ipToInt(ipStr);
const hostBits = 32 - mask;
const networkMask = (0xFFFFFFFF << hostBits) >>> 0; // 无符号右移
const networkAddress = ipInt & networkMask;
const broadcastAddress = networkAddress | ((1 << hostBits) - 1);
// 排除网络地址和广播地址
const start = networkAddress + 1;
const end = broadcastAddress - 1;
const ips = [];
for (let i = start; i <= end; i++) {
ips.push(intToIp(i));
}
return ips;
}
/**
* 扫描单个IP的所有指定端口
* @param {string} ip - IP地址
* @param {Array} ports - 端口数组
* @param {number} timeout - 连接超时时间(毫秒)
* @returns {Promise<Object>} - 扫描结果
*/
async function scanPortsForIP(ip, ports, timeout = 2000) {
const results = {
ip: ip,
openPorts: [],
scannedPorts: ports.length,
startTime: Date.now()
};
// 并行扫描所有端口
const portPromises = ports.map(port =>
scanPort(ip, port, timeout)
);
const portResults = await Promise.allSettled(portPromises);
// 收集开放的端口
portResults.forEach((result, index) => {
if (result.status === 'fulfilled' && result.value.open) {
results.openPorts.push({
port: ports[index],
service: result.value.service
});
}
});
results.endTime = Date.now();
results.scanDuration = results.endTime - results.startTime;
return results;
}
/**
* 扫描单个端口
* @param {string} ip - IP地址
* @param {number} port - 端口号
* @param {number} timeout - 超时时间
* @returns {Promise<Object>} - 端口扫描结果
*/
function scanPort(ip, port, timeout) {
return new Promise((resolve) => {
const socket = new net.Socket();
const result = {
ip: ip,
port: port,
open: false,
service: getServiceName(port)
};
// 设置超时
socket.setTimeout(timeout);
socket.on('connect', () => {
result.open = true;
socket.destroy();
resolve(result);
});
socket.on('timeout', () => {
socket.destroy();
resolve(result);
});
socket.on('error', () => {
socket.destroy();
resolve(result);
});
socket.on('close', () => {
// 连接已关闭
});
// 开始连接
socket.connect(port, ip);
});
}
/**
* 根据端口号获取常见服务名称
* @param {number} port - 端口号
* @returns {string} - 服务名称
*/
function getServiceName(port) {
const commonServices = {
20: 'FTP Data',
21: 'FTP Control',
22: 'SSH',
23: 'Telnet',
25: 'SMTP',
53: 'DNS',
67: 'DHCP Server',
68: 'DHCP Client',
69: 'TFTP',
80: 'HTTP',
88: 'Kerberos',
110: 'POP3',
123: 'NTP',
135: 'MSRPC',
137: 'NetBIOS Name',
138: 'NetBIOS Datagram',
139: 'NetBIOS Session',
143: 'IMAP',
161: 'SNMP',
162: 'SNMP Trap',
389: 'LDAP',
443: 'HTTPS',
445: 'SMB',
465: 'SMTPS',
514: 'Syslog',
515: 'LPD/LPR',
587: 'SMTP Submission',
636: 'LDAPS',
993: 'IMAPS',
995: 'POP3S',
1433: 'MSSQL',
1521: 'Oracle',
1723: 'PPTP',
1883: 'MQTT',
1900: 'UPnP',
2082: 'cPanel',
2083: 'cPanel SSL',
2086: 'WHM',
2087: 'WHM SSL',
2095: 'Webmail',
2096: 'Webmail SSL',
2181: 'ZooKeeper',
2375: 'Docker',
2376: 'Docker TLS',
3000: 'Node.js',
3306: 'MySQL',
3389: 'RDP',
4333: 'mSQL',
4443: 'HTTPS Alt',
4567: 'Sinatra',
4848: 'GlassFish',
5432: 'PostgreSQL',
5500: 'VNC',
5601: 'Kibana',
5672: 'RabbitMQ',
5900: 'VNC',
5984: 'CouchDB',
6379: 'Redis',
6443: 'Kubernetes API',
6666: 'IRC',
6667: 'IRC',
7000: 'Cassandra',
7001: 'WebLogic',
7077: 'Spark',
7200: 'Cassandra Native',
7474: 'Neo4j',
7687: 'Neo4j Bolt',
7777: 'Game Server',
8000: 'HTTP Alt',
8001: 'Tor',
8080: 'HTTP Proxy',
8081: 'HTTP Proxy Alt',
8088: 'HTTP Alt',
8089: 'HTTP Alt',
8090: 'HTTP Alt',
8091: 'HTTP Alt',
8181: 'HTTP Alt',
8443: 'HTTPS',
8880: 'HTTP Alt',
8888: 'HTTP Alt',
8889: 'Sunyard HSM',
9000: 'PHP-FPM',
9001: 'Tor',
9090: 'HTTP Alt',
9091: 'HTTP Alt',
9100: 'Print Server',
9200: 'Elasticsearch',
9300: 'Elasticsearch',
9418: 'Git',
9999: 'HTTP Alt',
10000: 'Webmin',
11211: 'Memcached',
15672: 'RabbitMQ Management',
27017: 'MongoDB',
27018: 'MongoDB',
28017: 'MongoDB HTTP',
50000: 'SAP',
50030: 'Hadoop JobTracker',
50070: 'Hadoop NameNode',
61616: 'ActiveMQ',
// 添加更多端口...
};
return commonServices[port] || `Unknown (${port})`;
}
module.exports = {
killport, getport, telnet, checkip, scan
}
+111
View File
@@ -0,0 +1,111 @@
const os = require('os');
const si = require('systeminformation');
const { formatBytes } = require("./util/convutil")
const { exec } = require('child_process');
const { promisify } = require('util');
const fs = require('fs');
const path = require('path');
// 将 exec 函数转换为返回 Promise 的函数
const execPromise = promisify(exec);
// 将 readFile 函数转换为返回 Promise 的函数
const readFilePromise = promisify(fs.readFile);
// 获取操作系统信息
async function getOSInfo() {
return {
type: os.type(),
platform: os.platform(),
release: os.release(),
arch: os.arch(),
os: await getOSDescription()
};
}
// 获取 CPU 信息
function getCPUInfo() {
const cpus = os.cpus();
const physicalCpuCount = require('physical-cpu-count')
return {
model: cpus[0].model,
speed: cpus[0].speed,
cores: cpus.length,
physicalCores: physicalCpuCount
};
}
// 获取内存信息
function getMemoryInfo() {
let info = {
totalSize: os.totalmem(),
freeSize: os.freemem()
}
return Object.assign(info, {
total : formatBytes(info.totalSize),
free : formatBytes(info.freeSize),
});
}
// 主函数,调用上述函数获取所有信息
async function getAllDeviceInfo() {
const osInfo = await getOSInfo();
const cpuInfo = getCPUInfo();
const memoryInfo = getMemoryInfo();
const diskInfo = await si.fsSize();
return {
os: osInfo,
cpu: cpuInfo,
memory: memoryInfo,
disk: diskInfo
};
}
async function getOSDescription() {
if (os.platform() === 'win32') {
return undefined;
}
try {
// 尝试执行 lsb_release 命令
const { stdout } = await execPromise('lsb_release -d');
// 提取描述信息
const distroDescription = stdout.split(':')[1].trim();
return distroDescription;
} catch (lsbError) {
try {
// lsb_release 命令不可用,尝试读取 /etc/os-release 文件
const osReleasePath = path.join('/', 'etc', 'os-release');
const data = await readFilePromise(osReleasePath, 'utf8');
// 解析文件内容
const lines = data.split('\n');
const info = {};
lines.forEach(line => {
const [key, value] = line.split('=');
if (key && value) {
info[key] = value.replace(/^"|"$/g, '');
}
});
// 获取 PRETTY_NAME
const prettyName = info['PRETTY_NAME'];
if (prettyName) {
return prettyName;
}
} catch (fileError) {
console.error('读取 /etc/os-release 文件时出错:', fileError);
}
}
return undefined;
}
module.exports = {
getOSInfo,
getCPUInfo,
getMemoryInfo,
getAllDeviceInfo
};
+361
View File
@@ -0,0 +1,361 @@
const fs = require("fs");
const os = require("os");
const path = require("path");
const { pipeline } = require("stream/promises");
const JSON5 = require("json5");
function now() {
return new Date().toISOString();
}
function normalizeRemotePath(value) {
if (!value || value === "/") {
return "/";
}
return path.posix.normalize("/" + String(value).replace(/\\/g, "/")).replace(/\/+$/g, "") || "/";
}
function normalizeGroup(group) {
return normalizeRemotePath(group == null ? "/modules" : group);
}
function joinRemote(...items) {
return normalizeRemotePath(items.filter((item) => item !== undefined && item !== null && item !== "").join("/"));
}
function normalizeOs(value) {
const name = String(value || os.platform()).toLowerCase();
if (["windows_nt", "win32", "windows"].includes(name)) {
return "windows";
}
if (["macos", "mac", "darwin"].includes(name)) {
return "darwin";
}
if (["linux", "alpine"].includes(name)) {
return "linux";
}
return name;
}
function normalizeArch(value) {
const name = String(value || os.arch()).toLowerCase();
if (["x64", "x86-64", "x86_64", "amd64"].includes(name)) {
return "x86_64";
}
if (["arm64", "aarch64"].includes(name)) {
return "aarch64";
}
if (["ia32", "i386", "x86"].includes(name)) {
return "i386";
}
return name;
}
function localModuleRoot(projectId) {
return path.resolve($sConfig.getLocalStorePath(), "modules", projectId);
}
function readJsonFile(filePath, fallback) {
try {
if (fs.existsSync(filePath)) {
return JSON5.parse(fs.readFileSync(filePath, "utf-8"));
}
} catch (e) {
$logger.warn(`读取 JSON 失败 ${filePath}: ${e.message}`);
}
return fallback;
}
function writeJsonFile(filePath, data) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, JSON.stringify(data, null, 4), "utf-8");
}
function safeRemove(target) {
fs.rmSync(target, { recursive: true, force: true });
}
function copyRecursive(from, to) {
const stat = fs.statSync(from);
if (stat.isDirectory()) {
fs.mkdirSync(to, { recursive: true });
for (const item of fs.readdirSync(from)) {
copyRecursive(path.join(from, item), path.join(to, item));
}
} else {
fs.mkdirSync(path.dirname(to), { recursive: true });
fs.copyFileSync(from, to);
}
}
module.exports = class Remote {
constructor() {
this.func_publish = require("./publish");
this.func_install = require("./install");
this.root = "/";
}
async init() {
const webdav = await import("webdav");
this.webdav = webdav.createClient ? webdav : webdav.default;
const uConfig = await $context.get("uConfig");
let server = uConfig.get("remoteServer");
if (!server) {
return;
}
if (server.startsWith("webdav")) {
server = "http" + server.substring(6);
}
const options = {};
const auth = uConfig.get("remoteAuth");
if (auth) {
const [username, password] = auth.split(":", 2);
options.username = username;
options.password = password;
}
this.client = this.webdav.createClient(server, options);
}
async check() {
if (!this.client) {
$logger.info("remote client unavailable");
return false;
}
try {
await this.ensureDir("/");
await this.client.getDirectoryContents("/");
return true;
} catch (e) {
$logger.error("remote check 过程中出错 " + e.message);
return false;
}
}
async publish(localFile, remotePath) {
return await this.func_publish(this, localFile, remotePath);
}
async install(id) {
return await this.func_install(this, id);
}
async list(projectId) {
const parsed = this.parseProjectVersion(projectId);
const state = this.getInstallState(parsed.projectId);
for (const version of state.installed_versions) {
const current = version === state.current_version ? "*" : " ";
$logger.info(`${current} ${version}${current === "*" ? " (current)" : ""}`);
}
return state.installed_versions;
}
async use(projectVersion) {
const { projectId, version } = this.parseProjectVersion(projectVersion);
return this.activate(projectId, version, "switch");
}
async rollback(projectId) {
const parsed = this.parseProjectVersion(projectId);
const state = this.getInstallState(parsed.projectId);
const history = [...state.history].reverse();
const current = state.current_version;
const previous = history.find((item) => item.version && item.version !== current && state.installed_versions.includes(item.version));
if (!previous) {
throw new Error(`没有可回滚版本 ${parsed.projectId}`);
}
return this.activate(parsed.projectId, previous.version, "rollback");
}
async remove(projectVersion) {
const { projectId, version } = this.parseProjectVersion(projectVersion);
const root = localModuleRoot(projectId);
const state = this.getInstallState(projectId);
const targetVersion = version || state.current_version;
if (!targetVersion) {
return false;
}
if (state.current_version === targetVersion) {
throw new Error(`不能删除当前激活版本 ${projectId}@${targetVersion}`);
}
safeRemove(path.resolve(root, "versions", targetVersion));
state.installed_versions = state.installed_versions.filter((item) => item !== targetVersion);
this.pushHistory(state, "uninstall", targetVersion);
this.saveInstallState(projectId, state);
return true;
}
parseProjectVersion(value) {
const text = String(value || "kit");
const index = text.lastIndexOf("@");
const target = index <= 0 ? text : text.substring(0, index);
const version = index <= 0 ? undefined : text.substring(index + 1);
if (target.startsWith("/")) {
const normalized = normalizeRemotePath(target);
const projectId = path.posix.basename(normalized);
const group = normalizeGroup(path.posix.dirname(normalized));
return { projectId, version, group };
}
return { projectId: target, version, group: "/modules" };
}
getLocalModuleRoot(projectId) {
const root = localModuleRoot(projectId);
fs.mkdirSync(root, { recursive: true });
return root;
}
getInstallState(projectId) {
const statePath = path.resolve(this.getLocalModuleRoot(projectId), ".meta", "install.json");
const state = readJsonFile(statePath, {
project_id: projectId,
installed_versions: [],
current_version: undefined,
last_updated: undefined,
history: [],
options: {}
});
state.project_id = state.project_id || projectId;
state.installed_versions = state.installed_versions || [];
state.history = state.history || [];
state.options = state.options || {};
return state;
}
saveInstallState(projectId, state) {
state.last_updated = now();
writeJsonFile(path.resolve(this.getLocalModuleRoot(projectId), ".meta", "install.json"), state);
}
pushHistory(state, action, version, from) {
state.history.push({ timestamp: now(), action, version, from });
}
async activate(projectId, version, action = "switch") {
const root = this.getLocalModuleRoot(projectId);
const versionDir = path.resolve(root, "versions", version);
if (!fs.existsSync(versionDir)) {
throw new Error(`本地版本不存在 ${projectId}@${version}`);
}
const current = path.resolve(root, "current");
safeRemove(current);
try {
fs.symlinkSync(path.relative(root, versionDir), current, "junction");
} catch (e) {
copyRecursive(versionDir, current);
}
const state = this.getInstallState(projectId);
const from = state.current_version;
state.current_version = version;
if (!state.installed_versions.includes(version)) {
state.installed_versions.push(version);
}
this.pushHistory(state, action, version, from);
this.saveInstallState(projectId, state);
fs.writeFileSync(path.resolve(root, "version"), version, "utf-8");
return version;
}
getPlatformCandidates() {
return [
{ os: normalizeOs(os.platform()), arch: normalizeArch(os.arch()) },
{ os: "any", arch: "any" }
];
}
getRemoteProjectRoot(projectId, group = "/modules") {
return joinRemote(normalizeGroup(group), projectId);
}
async ensureDir(remotePath) {
const target = normalizeRemotePath(remotePath);
if (target === "/") {
return;
}
if (await this.client.exists(target)) {
return;
}
await this.ensureDir(path.posix.dirname(target));
await this.client.createDirectory(target);
}
async putJson(remotePath, data) {
await this.ensureDir(path.posix.dirname(remotePath));
await this.client.putFileContents(normalizeRemotePath(remotePath), JSON.stringify(data, null, 4), { overwrite: true });
}
async getJson(remotePath, fallback) {
try {
const text = await this.client.getFileContents(normalizeRemotePath(remotePath), { format: "text" });
return JSON5.parse(text);
} catch (e) {
return fallback;
}
}
async wUpload(from, to) {
await this.ensureDir(path.posix.dirname(to));
const rs = fs.createReadStream(from);
const ws = this.client.createWriteStream(normalizeRemotePath(to));
await pipeline(rs, ws);
}
async wDownload(from, to) {
fs.mkdirSync(path.dirname(to), { recursive: true });
const rs = this.client.createReadStream(normalizeRemotePath(from));
const ws = fs.createWriteStream(to);
await pipeline(rs, ws);
}
async wMkdir(folder) {
return this.ensureDir(folder);
}
async wGetModuleInfo(projectId, group = "/modules") {
return this.getProjectMetadata(projectId, group);
}
async getProjectMetadata(projectId, group = "/modules") {
const metadata = await this.getJson(joinRemote(this.getRemoteProjectRoot(projectId, group), "metadata.json"), undefined);
if (metadata) {
metadata.group = normalizeGroup(metadata.group == null ? group : metadata.group);
return metadata;
}
throw new Error(`远程模块不存在 ${normalizeGroup(group)}/${projectId}`);
}
async getVersionMetadata(projectId, version, group = "/modules") {
const metadata = await this.getJson(joinRemote(this.getRemoteProjectRoot(projectId, group), version, "version.json"), undefined);
if (metadata) {
return metadata;
}
throw new Error(`远程版本不存在 ${normalizeGroup(group)}/${projectId}@${version}`);
}
async downloadArtifact(projectId, version, artifact, versionDir, group = "/modules") {
const remotePath = joinRemote(this.getRemoteProjectRoot(projectId, group), version, artifact.path);
const localPath = path.resolve(versionDir, artifact.path);
await this.wDownload(remotePath, localPath);
return localPath;
}
getMatchingArtifacts(versionMetadata) {
const artifacts = versionMetadata.artifacts || [];
const candidates = this.getPlatformCandidates();
for (const candidate of candidates) {
const prefix = `${candidate.os}/${candidate.arch}/`;
const matched = artifacts.filter((item) => String(item.path).replace(/\\/g, "/").startsWith(prefix));
if (matched.length > 0) {
return matched;
}
}
return [];
}
}
+84
View File
@@ -0,0 +1,84 @@
const fs = require("fs");
const path = require("path");
const { execFileSync } = require("child_process");
function now() {
return new Date().toISOString();
}
function verifyChecksum(filePath, checksum) {
if (!checksum) {
return;
}
const [alg, expected] = checksum.split(":", 2);
if (!alg || !expected) {
return;
}
const crypto = require("crypto");
const actual = crypto.createHash(alg).update(fs.readFileSync(filePath)).digest("hex");
if (actual !== expected) {
throw new Error(`文件校验失败 ${filePath}`);
}
}
function unpackArtifact(artifact, localPath, versionDir) {
if (artifact.type !== "archive" || !artifact.unpack) {
return;
}
const extractDir = path.resolve(path.dirname(localPath), "_extracted");
fs.mkdirSync(extractDir, { recursive: true });
const format = artifact.archive_format || "zip";
if (format === "zip") {
if (process.platform === "win32") {
const source = localPath.replace(/'/g, "''");
const target = extractDir.replace(/'/g, "''");
execFileSync("powershell.exe", ["-NoProfile", "-Command", `Expand-Archive -Path '${source}' -DestinationPath '${target}' -Force`], { stdio: "inherit" });
} else {
execFileSync("unzip", ["-o", localPath, "-d", extractDir], { stdio: "inherit" });
}
} else if (["tar", "tar.gz", "tgz", "tar.xz"].includes(format)) {
execFileSync("tar", ["-xf", localPath, "-C", extractDir], { stdio: "inherit" });
}
artifact.extracted_path = path.relative(versionDir, extractDir).replace(/\\/g, "/");
if (artifact.entry_point) {
artifact.extracted_entry_point = path.posix.join(artifact.extracted_path, artifact.entry_point.replace(/\\/g, "/"));
}
}
module.exports = async function install(remote, id) {
const parsed = remote.parseProjectVersion(id || "kit");
const projectId = parsed.projectId;
const group = parsed.group || "/modules";
const project = await remote.getProjectMetadata(projectId, group);
const version = parsed.version || project.version;
if (!version) {
throw new Error(`远程模块没有可安装版本 ${projectId}`);
}
const versionMeta = await remote.getVersionMetadata(projectId, version, project.group || group);
const artifacts = remote.getMatchingArtifacts(versionMeta);
if (artifacts.length < 1) {
throw new Error(`没有匹配当前平台的文件 ${projectId}@${version}`);
}
const root = remote.getLocalModuleRoot(projectId);
const versionDir = path.resolve(root, "versions", version);
fs.mkdirSync(versionDir, { recursive: true });
for (const artifact of artifacts) {
const localPath = await remote.downloadArtifact(projectId, version, artifact, versionDir, project.group || group);
verifyChecksum(localPath, artifact.checksum || (versionMeta.checksums && versionMeta.checksums[artifact.path]));
unpackArtifact(artifact, localPath, versionDir);
}
const localVersionMeta = Object.assign({}, versionMeta, { installed_at: now(), artifacts });
fs.writeFileSync(path.resolve(versionDir, ".version.json"), JSON.stringify(localVersionMeta, null, 4), "utf-8");
const state = remote.getInstallState(projectId);
const action = state.current_version ? "switch" : "install";
await remote.activate(projectId, version, action);
$logger.info(`当前版本 ${projectId}@${version}`);
return version;
};
+214
View File
@@ -0,0 +1,214 @@
const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
const JSON5 = require("json5");
function now() {
return new Date().toISOString();
}
function normalizeRemotePath(value) {
if (!value || value === "/") {
return "/";
}
return path.posix.normalize("/" + String(value).replace(/\\/g, "/")).replace(/\/+$/g, "") || "/";
}
function normalizeGroup(group) {
return normalizeRemotePath(group == null ? "/modules" : group);
}
function joinRemote(...items) {
return normalizeRemotePath(items.filter((item) => item !== undefined && item !== null && item !== "").join("/"));
}
function normalizeOs(value) {
const name = String(value || "any").toLowerCase();
if (["windows_nt", "win32", "windows"].includes(name)) {
return "windows";
}
if (["macos", "mac", "darwin"].includes(name)) {
return "darwin";
}
if (["linux", "alpine"].includes(name)) {
return "linux";
}
if (["cross", "all", "any"].includes(name)) {
return "any";
}
return name;
}
function normalizeArch(value) {
const name = String(value || "any").toLowerCase();
if (["x64", "x86-64", "x86_64", "amd64"].includes(name)) {
return "x86_64";
}
if (["arm64", "aarch64"].includes(name)) {
return "aarch64";
}
if (["ia32", "i386", "x86"].includes(name)) {
return "i386";
}
if (["cross", "all", "any"].includes(name)) {
return "any";
}
return name;
}
function sha256(filePath) {
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
}
function guessArtifactType(filePath) {
const ext = path.extname(filePath).toLowerCase();
if ([".zip", ".tgz", ".gz", ".xz", ".tar"].includes(ext)) {
return "archive";
}
return "executable";
}
function archiveFormat(filePath) {
const name = path.basename(filePath).toLowerCase();
if (name.endsWith(".tar.gz") || name.endsWith(".tgz")) {
return "tar.gz";
}
if (name.endsWith(".tar.xz")) {
return "tar.xz";
}
if (name.endsWith(".zip")) {
return "zip";
}
if (name.endsWith(".tar")) {
return "tar";
}
return undefined;
}
function readLocalMeta(localFile) {
const base = localFile ? path.resolve(localFile) : process.cwd();
const metaFile = fs.statSync(base).isDirectory() ? path.resolve(base, "meta.json") : path.resolve(process.cwd(), "meta.json");
if (fs.existsSync(metaFile)) {
return JSON5.parse(fs.readFileSync(metaFile, "utf-8"));
}
if (!localFile) {
throw new Error("当前路径不包含 meta.json");
}
return {
id: path.basename(localFile, path.extname(localFile)).toLowerCase(),
group: "/modules",
version: "",
desc: "",
dist: [{ path: localFile, os: "any", platform: "any", filename: path.basename(localFile) }]
};
}
function buildArtifacts(localMeta) {
const dists = localMeta.dist || localMeta.artifacts || [];
return dists.map((dist) => {
const localPath = path.resolve(dist.path || dist.localPath || dist.file || dist.filename);
const osName = normalizeOs(dist.os);
const archName = normalizeArch(dist.arch || dist.platform);
const filename = dist.filename || path.basename(localPath);
const artifactPath = dist.remotePath || `${osName}/${archName}/${filename}`;
const type = dist.type || guessArtifactType(localPath);
const artifact = {
path: artifactPath.replace(/\\/g, "/"),
type,
checksum: `sha256:${sha256(localPath)}`,
localPath
};
if (type === "archive") {
artifact.archive_format = dist.archive_format || archiveFormat(localPath);
artifact.entry_point = dist.entry_point;
artifact.unpack = Boolean(dist.unpack);
}
return artifact;
});
}
function bumpTailVersion(version) {
const text = String(version || "").trim();
if (!text) {
return "1.0";
}
const match = text.match(/^(.*?)(\d+)$/);
if (!match) {
return `${text}.1`;
}
return `${match[1]}${Number(match[2]) + 1}`;
}
async function resolveVersion(remote, projectRoot, localMeta) {
const explicitVersion = String(localMeta.version || "").trim();
if (explicitVersion) {
if (await remote.client.exists(joinRemote(projectRoot, explicitVersion))) {
throw new Error(`远程版本已存在 ${projectRoot}@${explicitVersion}`);
}
return explicitVersion;
}
const metadata = await remote.getJson(joinRemote(projectRoot, "metadata.json"), undefined);
if (metadata && metadata.version) {
return bumpTailVersion(metadata.version);
}
return "1.0";
}
module.exports = async function publish(remote, localFile, remotePath) {
if (remotePath) {
const target = path.posix.resolve("/", remotePath.replace(/\\/g, "/"));
await remote.wUpload(localFile, path.posix.join(target, path.basename(localFile)));
$logger.info("文件上传成功");
return;
}
const localMeta = readLocalMeta(localFile);
const projectId = localMeta.project_id || localMeta.id || localMeta.name;
if (!projectId) {
throw new Error("meta.json 缺少 project_id/id/name");
}
const group = normalizeGroup(localMeta.group);
const projectRoot = remote.getRemoteProjectRoot ? remote.getRemoteProjectRoot(projectId, group) : joinRemote(group, projectId);
const version = await resolveVersion(remote, projectRoot, localMeta);
const artifacts = buildArtifacts(localMeta);
await remote.ensureDir(joinRemote(projectRoot, version));
const versionMeta = {
version,
release_date: localMeta.release_date || now(),
official_download_url: localMeta.official_download_url || "",
artifacts: artifacts.map(({ localPath, ...artifact }) => artifact),
checksums: Object.fromEntries(artifacts.map((artifact) => [artifact.path, artifact.checksum]))
};
for (const artifact of artifacts) {
await remote.wUpload(artifact.localPath, joinRemote(projectRoot, version, artifact.path));
}
await remote.putJson(joinRemote(projectRoot, version, "version.json"), versionMeta);
const metadata = await remote.getJson(joinRemote(projectRoot, "metadata.json"), {
project_id: projectId,
group,
official_url: localMeta.official_url || localMeta.vcs || "",
description: localMeta.description || localMeta.desc || "",
version,
fetch_rules: localMeta.fetch_rules || [],
extra: {}
});
metadata.project_id = projectId;
metadata.group = group;
metadata.official_url = metadata.official_url || localMeta.official_url || localMeta.vcs || "";
metadata.description = localMeta.description || localMeta.desc || metadata.description || "";
metadata.version = version;
delete metadata.versions;
delete metadata.latest_version;
metadata.extra = Object.assign({}, metadata.extra || {}, localMeta.extra || {});
await remote.putJson(joinRemote(projectRoot, "metadata.json"), metadata);
$logger.info(`文件上传成功 ${group}/${projectId}@${version}`);
};
+33
View File
@@ -0,0 +1,33 @@
/**
*
* child_process
* - exec 适合短时间命令,获取完整输出
* - execFile 适合实时交互
* - spawn 适合长时间命令
*
* shelljs - https://www.npmjs.com/package/shelljs
* execa - https://www.npmjs.com/package/execa
* ssh2 - https://www.npmjs.com/package/ssh2
*
*/
const { execSync } = require('child_process');
module.exports = {
execSync(cmd){
$logger.debug(`execSync ${cmd}`)
const output = execSync(cmd);
return output.toString()
},
callSync(cmd){
$logger.debug(`callSync ${cmd}`)
const output = execSync(cmd);
console.log(output.toString())
}
}
+576
View File
@@ -0,0 +1,576 @@
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return "0 B"; // 处理 0 字节的情况
const units = ["B", "K", "M", "G", "T", "P", "E", "Z", "Y"];
let unitIndex = 0;
// 循环除以 1024,直到字节数小于 1024 或单位用完
while (bytes >= 1024 && unitIndex < units.length - 1) {
bytes /= 1024;
unitIndex++;
}
// 保留小数位数,并移除末尾多余的零(如 2.0 → 2)
const formattedNumber = bytes.toFixed(decimals).replace(/\.0+$/, "");
return `${formattedNumber} ${units[unitIndex]}`;
}
var codePoint2utf8 = function (code) {
var bin = parseInt(code).toString(2)
var ln = 0;
var mx = 0;
if (bin.length <= 7) {
ln = 1;
mx = 7;
} else if (bin.length > 7 && bin.length <= 11) {
ln = 2;
mx = 11;
} else if (bin.length > 11 && bin.length <= 16) {
ln = 3;
mx = 16;
} else if (bin.length > 16 && bin.length <= 21) {
ln = 4;
mx = 21;
} else if (bin.length > 21 && bin.length <= 26) {
ln = 5;
mx = 26;
} else if (bin.length > 26 && bin.length <= 31) {
ln = 6;
mx = 31;
};
var longbin = ("0".repeat(mx) + bin).slice(-mx)
var result = '';
for (var i = 0; i < ln; i++) {
if (ln != 1) {
if (i == 0) {
result += parseInt("1".repeat(ln) + "0" + longbin.slice(0,7-ln),2).toString(16);
longbin = longbin.slice(7-ln);
} else {
result += parseInt("10" + longbin.slice(0,6),2).toString(16);
longbin = longbin.slice(6);
};
} else {
result += ("00" + parseInt(longbin,2).toString(16)).slice(-2);
};
};
return result;
};
var stringToHex = function (inputstring) {
var result = '';
for (var i = 0; i < inputstring.length; i++) {
result += codePoint2utf8(inputstring.codePointAt(i))
};
return result;
};
var hexToString = function (inputstring) {
try {
var result = decodeURIComponent(inputstring.toLowerCase().replace(/\s+/g, '').replace(/[0-9a-f]{2}/g, '%$&'));
} catch(e) {
throw "Not valid UTF-8 hex code!";
};
return result;
};
function stringToByte(str) {
var bytes = new Array();
var len, c;
len = str.length;
for(var i = 0; i < len; i++) {
c = str.charCodeAt(i);
if(c >= 0x010000 && c <= 0x10FFFF) {
bytes.push(((c >> 18) & 0x07) | 0xF0);
bytes.push(((c >> 12) & 0x3F) | 0x80);
bytes.push(((c >> 6) & 0x3F) | 0x80);
bytes.push((c & 0x3F) | 0x80);
} else if(c >= 0x000800 && c <= 0x00FFFF) {
bytes.push(((c >> 12) & 0x0F) | 0xE0);
bytes.push(((c >> 6) & 0x3F) | 0x80);
bytes.push((c & 0x3F) | 0x80);
} else if(c >= 0x000080 && c <= 0x0007FF) {
bytes.push(((c >> 6) & 0x1F) | 0xC0);
bytes.push((c & 0x3F) | 0x80);
} else {
bytes.push(c & 0xFF);
}
}
return bytes;
}
// utf8
function byteToString(arr) {
if(typeof arr === 'string') {
return arr;
}
var str = '',
_arr = arr;
for(var i = 0; i < _arr.length; i++) {
var one = _arr[i].toString(2),
v = one.match(/^1+?(?=0)/);
if(v && one.length == 8) {
var bytesLength = v[0].length;
var store = _arr[i].toString(2).slice(7 - bytesLength);
for(var st = 1; st < bytesLength; st++) {
store += _arr[st + i].toString(2).slice(2);
}
str += String.fromCharCode(parseInt(store, 2));
i += bytesLength - 1;
} else {
str += String.fromCharCode(_arr[i]);
}
}
return str;
}
/**
* Convert the given string to byte array
*
* @param {string} str the string to be converted to byte array
* @returns {number[]}
*/
var str2bytes = function (str) {
var i = 0;
var ch = null;
var hex = null;
var encoded = encodeURIComponent(str);
var length = encoded.length;
var bytes = [];
while (i < length) {
ch = encoded.charAt(i++);
if (ch === '%') {
hex = encoded.charAt(i++)
hex += encoded.charAt(i++);
bytes.push(parseInt(hex, 16));
} else {
bytes.push(ch.charCodeAt(0));
}
}
return bytes;
};
/**
* Convert the given byte array to string
*
* @param {number[]} bytes The byte array to be converted to string
* @returns {string}
*/
var bytes2str = function (bytes) {
var i = 0;
var hex = null;
var byte = 0;
var hexArray = [];
var length = bytes.length;
while (i < length) {
byte = bytes[i++];
hex = byte.toString(16);
hex = hex.length < 2 ? ('%0' + hex) : ('%' + hex);
hexArray.push(hex);
}
return decodeURIComponent(hexArray.join(''));
};
function base64ArrayBuffer(arrayBuffer) {
var base64 = ''
var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
var bytes = new Uint8Array(arrayBuffer)
var byteLength = bytes.byteLength
var byteRemainder = byteLength % 3
var mainLength = byteLength - byteRemainder
var a, b, c, d
var chunk
// Main loop deals with bytes in chunks of 3
for (var i = 0; i < mainLength; i = i + 3) {
// Combine the three bytes into a single integer
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]
// Use bitmasks to extract 6-bit segments from the triplet
a = (chunk & 16515072) >> 18 // 16515072 = (2^6 - 1) << 18
b = (chunk & 258048) >> 12 // 258048 = (2^6 - 1) << 12
c = (chunk & 4032) >> 6 // 4032 = (2^6 - 1) << 6
d = chunk & 63 // 63 = 2^6 - 1
// Convert the raw binary segments to the appropriate ASCII encoding
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]
}
// Deal with the remaining bytes and padding
if (byteRemainder == 1) {
chunk = bytes[mainLength]
a = (chunk & 252) >> 2 // 252 = (2^6 - 1) << 2
// Set the 4 least significant bits to zero
b = (chunk & 3) << 4 // 3 = 2^2 - 1
base64 += encodings[a] + encodings[b] + '=='
} else if (byteRemainder == 2) {
chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]
a = (chunk & 64512) >> 10 // 64512 = (2^6 - 1) << 10
b = (chunk & 1008) >> 4 // 1008 = (2^6 - 1) << 4
// Set the 2 least significant bits to zero
c = (chunk & 15) << 2 // 15 = 2^4 - 1
base64 += encodings[a] + encodings[b] + encodings[c] + '='
}
return base64
}
const Base64 = {
_keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
encode: function(e) {
var t = "";
var n, r, i, s, o, u, a;
var f = 0;
e = Base64._utf8_encode(e);
while (f < e.length) {
n = e.charCodeAt(f++);
r = e.charCodeAt(f++);
i = e.charCodeAt(f++);
s = n >> 2;
o = (n & 3) << 4 | r >> 4;
u = (r & 15) << 2 | i >> 6;
a = i & 63;
if (isNaN(r)) {
u = a = 64
} else if (isNaN(i)) {
a = 64
}
t = t + this._keyStr.charAt(s) + this._keyStr.charAt(o) + this._keyStr.charAt(u) + this._keyStr.charAt(a)
}
return t
},
decode: function(e) {
var t = "";
var n, r, i;
var s, o, u, a;
var f = 0;
e = e.replace(/[^A-Za-z0-9+/=]/g, "");
while (f < e.length) {
s = this._keyStr.indexOf(e.charAt(f++));
o = this._keyStr.indexOf(e.charAt(f++));
u = this._keyStr.indexOf(e.charAt(f++));
a = this._keyStr.indexOf(e.charAt(f++));
n = s << 2 | o >> 4;
r = (o & 15) << 4 | u >> 2;
i = (u & 3) << 6 | a;
t = t + String.fromCharCode(n);
if (u != 64) {
t = t + String.fromCharCode(r)
}
if (a != 64) {
t = t + String.fromCharCode(i)
}
}
t = Base64._utf8_decode(t);
return t
},
_utf8_encode: function(e) {
e = e.replace(/rn/g, "n");
var t = "";
for (var n = 0; n < e.length; n++) {
var r = e.charCodeAt(n);
if (r < 128) {
t += String.fromCharCode(r)
} else if (r > 127 && r < 2048) {
t += String.fromCharCode(r >> 6 | 192);
t += String.fromCharCode(r & 63 | 128)
} else {
t += String.fromCharCode(r >> 12 | 224);
t += String.fromCharCode(r >> 6 & 63 | 128);
t += String.fromCharCode(r & 63 | 128)
}
}
return t
},
_utf8_decode: function(e) {
var t = "";
var n = 0;
var r = 0;
var c1 =0;
var c2 =0;
var c3 =0;
while (n < e.length) {
r = e.charCodeAt(n);
if (r < 128) {
t += String.fromCharCode(r);
n++
} else if (r > 191 && r < 224) {
c2 = e.charCodeAt(n + 1);
t += String.fromCharCode((r & 31) << 6 | c2 & 63);
n += 2
} else {
c2 = e.charCodeAt(n + 1);
c3 = e.charCodeAt(n + 2);
t += String.fromCharCode((r & 15) << 12 | (c2 & 63) << 6 | c3 & 63);
n += 3
}
}
return t
}
}
// (function() {
// var base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
// base64DecodeChars = new Array(( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), 62, ( - 1), ( - 1), ( - 1), 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), ( - 1), 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, ( - 1), ( - 1), ( - 1), ( - 1), ( - 1));
// this.base64encode = function(e) {
// var r, a, c, h, o, t;
// for (c = e.length, a = 0, r = ''; a < c;) {
// if (h = 255 & e.charCodeAt(a++), a == c) {
// r += base64EncodeChars.charAt(h >> 2),
// r += base64EncodeChars.charAt((3 & h) << 4),
// r += '==';
// break
// }
// if (o = e.charCodeAt(a++), a == c) {
// r += base64EncodeChars.charAt(h >> 2),
// r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4),
// r += base64EncodeChars.charAt((15 & o) << 2),
// r += '=';
// break
// }
// t = e.charCodeAt(a++),
// r += base64EncodeChars.charAt(h >> 2),
// r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4),
// r += base64EncodeChars.charAt((15 & o) << 2 | (192 & t) >> 6),
// r += base64EncodeChars.charAt(63 & t)
// }
// return r
// }
// this.base64decode = function(e) {
// var r, a, c, h, o, t, d;
// for (t = e.length, o = 0, d = ''; o < t;) {
// do r = base64DecodeChars[255 & e.charCodeAt(o++)];
// while (o < t && r == -1);
// if (r == -1) break;
// do a = base64DecodeChars[255 & e.charCodeAt(o++)];
// while (o < t && a == -1);
// if (a == -1) break;
// d += String.fromCharCode(r << 2 | (48 & a) >> 4);
// do {
// if (c = 255 & e.charCodeAt(o++), 61 == c) return d;
// c = base64DecodeChars[c]
// } while ( o < t && c == - 1 );
// if (c == -1) break;
// d += String.fromCharCode((15 & a) << 4 | (60 & c) >> 2);
// do {
// if (h = 255 & e.charCodeAt(o++), 61 == h) return d;
// h = base64DecodeChars[h]
// } while ( o < t && h == - 1 );
// if (h == -1) break;
// d += String.fromCharCode((3 & c) << 6 | h)
// }
// return d
// }
// this.HexToBase64 = function(str) {
// return base64encode(String.fromCharCode.apply(null, str.replace(/\r|\n/g, "").replace(/([\da-fA-F]{2}) ?/g, "0x$1 ").replace(/ +$/, "").split(" ")));
// }
// this.Base64Tohex = function(str) {
// for (var i = 0,
// bin = base64decode(str.replace(/[ \r\n]+$/, "")), hex = []; i < bin.length; ++i) {
// var tmp = bin.charCodeAt(i).toString(16);
// if (tmp.length === 1) tmp = "0" + tmp;
// hex[hex.length] = tmp;
// }
// return hex.join("");
// },
// this.toBytes = function(str) {
// for (var bytes = [], c = 0; c < str.length; c += 2)
// bytes.push(parseInt(str.substr(c, 2), 16));
// return bytes;
// }
//
// }) ();
//hexToBase64 Base64Tohex base64decode base64encode
var bytesToString = function(bytes){
return hexToString(bytesToHex(bytes));
}
var bytesToBase64 = function(bytes){
return base64ArrayBuffer(bytes);
}
// Convert a byte array to a hex string
var bytesToHex = function(bytes) {
for (var hex = [], i = 0; i < bytes.length; i++) {
hex.push((bytes[i] >>> 4).toString(16));
hex.push((bytes[i] & 0xF).toString(16));
}
return hex.join("");
}
var stringToBase64 = function(str){
return base64encode(str);
}
var stringToBytes = function(str){
return hexToBytes(stringToHex(str));
}
// Convert a ASCII string to a hex string
var stringToHex = function(str) {
return str.split("").map(function(c) {
return ("0" + c.charCodeAt(0).toString(16)).slice(-2);
}).join("");
}
var hexToBytes= function(hex) {
for (var bytes = [], c = 0; c < hex.length; c += 2)
bytes.push(parseInt(hex.substr(c, 2), 16));
return bytes;
}
// Convert a hex string to a ASCII string
var hexToString = function(hexStr) {
var hex = hexStr.toString();//force conversion
var str = '';
for (var i = 0; i < hex.length; i += 2)
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
return str;
}
var hexToBase64 = function(hexStr){
return HexToBase64(hexStr);
}
var base64ToString = function(base64str){
return base64decode(base64str);
}
function encodeUtf8(text) {
const code = encodeURIComponent(text);
const bytes = [];
for (var i = 0; i < code.length; i++) {
const c = code.charAt(i);
if (c === '%') {
const hex = code.charAt(i + 1) + code.charAt(i + 2);
const hexVal = parseInt(hex, 16);
bytes.push(hexVal);
i += 2;
} else bytes.push(c.charCodeAt(0));
}
return bytes;
}
function decodeUtf8(bytes) {
var encoded = "";
for (var i = 0; i < bytes.length; i++) {
encoded += '%' + bytes[i].toString(16);
}
return decodeURIComponent(encoded);
}
function strToBytesUTF8(str) {
let utf8 = [];
for (let i = 0; i < str.length; i++) {
let charcode = str.charCodeAt(i);
if (charcode < 0x80) utf8.push(charcode);
else if (charcode < 0x800) {
utf8.push(0xc0 | (charcode >> 6),
0x80 | (charcode & 0x3f));
}
else if (charcode < 0xd800 || charcode >= 0xe000) {
utf8.push(0xe0 | (charcode >> 12),
0x80 | ((charcode>>6) & 0x3f),
0x80 | (charcode & 0x3f));
}
// surrogate pair
else {
i++;
// UTF-16 encodes 0x10000-0x10FFFF by
// subtracting 0x10000 and splitting the
// 20 bits of 0x0-0xFFFFF into two halves
charcode = 0x10000 + (((charcode & 0x3ff)<<10)
| (str.charCodeAt(i) & 0x3ff));
utf8.push(0xf0 | (charcode >>18),
0x80 | ((charcode>>12) & 0x3f),
0x80 | ((charcode>>6) & 0x3f),
0x80 | (charcode & 0x3f));
}
}
return utf8;
}
function utf8ToBase64(str){
var ret11 = strToBytesUTF8( str )
return bytesToBase64( ret11 )
}
function base64ToUtf8( str ){
var hex = base64ToHex( str );
var ret = hexToBytes( hex )
// var ret = base64ToBytes( str )
return decodeUtf8(ret);
}
var base64ToHex = function(base64str){
return Base64Tohex(base64str);
}
var base64ToBytes = function(base64str){
return toBytes(base64str);
//null
}
var utf8ToHex = function (str) {
var ret11 = strToBytesUTF8( str )
return bytesToHex( ret11 )
}
var hexToUtf8 = function (hex) {
var ret = hexToBytes( hex )
// var ret = base64ToBytes( str )
return decodeUtf8(ret);
}
if ( module ) {
module.exports = {
stringToByte,
byteToString,
str2bytes,
bytes2str,
base64ArrayBuffer,
bytesToString,
bytesToBase64,
bytesToHex,
stringToBase64,
stringToBytes,
stringToHex,
hexToBytes,
hexToString,
hexToBase64,
base64ToString,
base64ToHex,
base64ToBytes,
Base64,
encodeUtf8,
decodeUtf8,
strToBytesUTF8,
utf8ToBase64,
base64ToUtf8,
utf8ToHex,
hexToUtf8,
formatBytes
}
}
+27
View File
@@ -0,0 +1,27 @@
module.exports = {
// 定义格式化日期的函数
formatDate : function (date){
// 获取年份
const year = date.getFullYear();
// 获取月份,注意月份是从 0 开始的,所以要加 1
const month = String(date.getMonth() + 1).padStart(2, '0');
// 获取日期
const day = String(date.getDate()).padStart(2, '0');
// 获取小时
const hours = String(date.getHours()).padStart(2, '0');
// 获取分钟
const minutes = String(date.getMinutes()).padStart(2, '0');
// 获取秒数
const seconds = String(date.getSeconds()).padStart(2, '0');
// 按照 yyyyMMddHHmmss 的格式拼接字符串
return `${year}${month}${day}${hours}${minutes}${seconds}`;
},
now : function (){
// 获取当前日期和时间
const currentDate = new Date();
// 调用格式化函数
const formattedDate = this.formatDate(currentDate);
return formattedDate
}
}
+6
View File
@@ -0,0 +1,6 @@
const JSON5 = require('json5')
module.exports = {
copy(obj) {
return JSON5.parse( JSON5.stringify( obj ) )
}
}
+51
View File
@@ -0,0 +1,51 @@
const os = require('os');
const fs = require("fs");
const Windows = "Windows";
const Linux = "Linux";
module.exports = {
Windows, Linux,
/**
* 根据 os 不同,载入不同的方法
*/
choise : function ( cbs ) {
if (os.type() === 'Windows_NT') {
// windows平台
return cbs && cbs[ Windows ]
}
else if (os.type() === 'Linux') {
// Linux平台
return cbs && cbs[ Linux ]
}
else {
throw { message : "不支持的 OS" }
}
},
cur : function () {
let isLinux = false;
// 操作系统不同,安装方式不同
let curOS = os.type()
if ( curOS === "Windows_NT" ) {
curOS = "Windows"
}
else if ( curOS === "Linux" ) {
isLinux = true;
const fs = require('fs');
if( fs.existsSync('/etc/alpine-release') ){
curOS = "Alpine"
}
}
let curPlatform = os.arch()
if ( curPlatform === "x64" ) {
curPlatform = "X86_64"
} else if ( curPlatform.startsWith("arm") ) {
curPlatform = "ARM64"
}
return { isLinux, curOS, curPlatform }
}
}
+121
View File
@@ -0,0 +1,121 @@
const { execFileSync } = require("child_process");
const fs = require("fs");
const path = require("path");
function commandExists(command) {
try {
execFileSync("where.exe", [command], { stdio: "ignore" });
return true;
} catch (e) {
return false;
}
}
function run(command, args, options = {}) {
return execFileSync(command, args, Object.assign({ stdio: "inherit" }, options));
}
function addToZip(sourceDir, to, password) {
fs.mkdirSync(path.dirname(path.resolve(to)), { recursive: true });
if (password) {
if (!commandExists("7z.exe") && !commandExists("7z")) {
throw new Error("Windows password zip requires 7z in PATH");
}
const bin = commandExists("7z.exe") ? "7z.exe" : "7z";
run(bin, ["a", "-tzip", `-p${password}`, "-y", path.resolve(to), "."], { cwd: sourceDir, stdio: "inherit" });
return;
}
const destination = path.resolve(to).replace(/'/g, "''");
const source = path.resolve(sourceDir, "*").replace(/'/g, "''");
const update = fs.existsSync(to) ? " -Update" : "";
const script = `Compress-Archive -Path '${source}' -DestinationPath '${destination}'${update} -Force`;
run("powershell.exe", ["-NoProfile", "-Command", script]);
}
function addToTar(sourceDir, to) {
fs.mkdirSync(path.dirname(path.resolve(to)), { recursive: true });
const args = fs.existsSync(to) ? ["-rf", path.resolve(to), "."] : ["-cf", path.resolve(to), "."];
run("tar.exe", args, { cwd: sourceDir, stdio: "inherit" });
}
function zip(sourceDir, to, params = {}) {
if (params.type === "tar") {
addToTar(sourceDir, to);
} else {
addToZip(sourceDir, to, params.password);
}
}
function isRemotePath(value) {
if (typeof value !== "string") {
return false;
}
if (/^[A-Za-z]:[\\/]/.test(value)) {
return false;
}
return /^[^\s:]+:.+/.test(value);
}
function resolveScpPath(value) {
return isRemotePath(value) ? value : path.resolve(value);
}
function appendScpParams(args, params, supportOpenSshOptions) {
if (params.recursive !== false) {
args.push("-r");
}
if (params.port) {
args.push("-P", String(params.port));
}
if (params.key) {
args.push("-i", params.key);
}
if (params.preserve) {
args.push("-p");
}
if (params.compress) {
args.push("-C");
}
if (supportOpenSshOptions && params.strictHostKeyChecking !== undefined) {
args.push("-o", `StrictHostKeyChecking=${params.strictHostKeyChecking ? "yes" : "no"}`);
}
if (supportOpenSshOptions && params.options) {
for (const name of Object.keys(params.options)) {
args.push("-o", `${name}=${params.options[name]}`);
}
}
if (params.args) {
args.push(...(Array.isArray(params.args) ? params.args : [params.args]));
}
}
function scp(from, to, params = {}) {
const source = resolveScpPath(from);
if (!isRemotePath(from) && !fs.existsSync(source)) {
return;
}
if (params.pwd) {
if (!commandExists("pscp.exe") && !commandExists("pscp")) {
throw new Error("Windows scp with pwd requires pscp in PATH");
}
const bin = commandExists("pscp.exe") ? "pscp.exe" : "pscp";
const args = ["-pw", params.pwd];
appendScpParams(args, params, false);
args.push(source, resolveScpPath(to));
run(bin, args);
return;
}
if (!commandExists("scp.exe")) {
throw new Error("Windows scp requires scp.exe in PATH");
}
const args = [];
appendScpParams(args, params, true);
args.push(source, resolveScpPath(to));
run("scp.exe", args);
}
module.exports = { zip, scp };
+38
View File
@@ -0,0 +1,38 @@
const fs = require("fs")
const os = require("os")
const path = require("path");
// 废弃,权限问题难以处理
// const KIT_HOME = "C:\\Program Files\\kit"
// const HOME = "C:\\Program Files\\kit\\.kit"
const KIT_HOME = path.resolve(os.homedir(), "AppData", "Local", "kit")
const HOME = path.resolve(os.homedir(), "AppData", "Local", "kit", ".kit")
function getModuleInfo(id){
let homeDir = path.resolve(HOME, "modules", id)
if ( "kit" === id ) {
homeDir = KIT_HOME;
}
if ( ! fs.existsSync( homeDir ) ) {
fs.mkdirSync( homeDir, { recursive : true } )
return {
HOME, KIT_HOME, exist : false, homeDir
}
}
let versionFile = path.resolve(homeDir, "version")
if ( ! fs.existsSync( versionFile ) ) {
return {
HOME, KIT_HOME, exist : true, homeDir
}
}
return {
HOME, KIT_HOME, exist : true, version : fs.readFileSync(versionFile, "UTF-8"), homeDir
}
}
module.exports = { getModuleInfo }
+40
View File
@@ -0,0 +1,40 @@
async function killport(port) {
$logger.info(`kill process on port ${port}`);
const util = require('util');
const exec = util.promisify(require('child_process').exec);
try {
// 查 PID
let { stdout : stdout1 } = await exec(`netstat -ano | findstr "LISTENING" | findstr /C:":${port} "`)
const output = stdout1.split('\n');
const outputLines = output[0].trim().split(' ')
const pid = outputLines[outputLines.length - 1];
$logger.debug(`绑定端口号 ${port} 的进程 PID=${pid}`)
// 查进程信息
let { stdout : stdout2 } = await exec(`tasklist /FI "PID eq ${pid}" /V /FO CSV`);
const tasklistOutput = stdout2.trim();
const processInfo = tasklistOutput.split('\n')[1].split('","');
const processName = processInfo[0].slice(1) ;
$logger.info(`绑定端口号=${port}; 进程 PID=${pid}; 程序=${processName};`);
// Kill 进程
await exec(`taskkill /PID ${pid} /F`)
} catch (e) {
if ( 1 === e.code ) {
$logger.error("此端口未被使用,或清理进程失败。")
return
}
}
}
module.exports = {
killport
}
+138
View File
@@ -0,0 +1,138 @@
const crypto = require('../src/crypto');
const fs = require('fs');
const path = require('path');
// 测试用例
console.log('=== 开始测试 crypto 模块 ===');
// 测试 1: getInfoFromPKCS7 接口(RSA)
try {
console.log('\n1. 测试 getInfoFromPKCS7 接口(RSA)');
// 使用真实的 PKCS7 签名数据进行测试
const mockP7Sign = `-----BEGIN PKCS7-----
MIIDNwYJKoZIhvcNAQcCoIIDKDCCAyQCAQExDzANBglghkgBZQMEAgEFADA8Bgkq
hkiG9w0BBwGgLwQtVGhpcyBpcyB0aGUgdGVzdCBjb250ZW50IGZvciBQS0NTNyBz
aWduYXR1cmUuoIIB5jCCAeIwggFLoAMCAQICAQEwDQYJKoZIhvcNAQELBQAwNzEZ
MBcGA1UEAxMQdGVzdC5leGFtcGxlLmNvbTEaMBgGA1UEChMRVGVzdCBPcmdhbml6
YXRpb24wHhcNMjYwNDA0MDAxMDIyWhcNMjcwNDA0MDAxMDIyWjA3MRkwFwYDVQQD
ExB0ZXN0LmV4YW1wbGUuY29tMRowGAYDVQQKExFUZXN0IE9yZ2FuaXphdGlvbjCB
nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1e+05xb8T3Lo2v9MZQen5HYffpnc
yY9mnT/ygvfxZNM9TeYkq4fACiuS99FONnlHFQdlXEPJluIk1NBFUoLpehAbzOSY
Fu0WEjBOVJIg9FIWm7TUojU8yvPsfPGFYTEGEOGAse8AfHw4pl+w5SvxQUF8f2jB
XsRY0MrDyb07xC8CAwEAATANBgkqhkiG9w0BAQsFAAOBgQAOEttx/p3xosIm/8sM
kIaxR2c/fbYoMbBsX+geaiRu+xNOItbtQpGOhW6Ql981c3EwIjyd34KavplofZIr
Slj4jHQFySZaJkegHY+LCAQz9GEg2TMke3MsPJ7nSZS7MUqB2AQt1tLuiXUuRh3V
jF4yiqi6lGVmvPJf+y1GXk47KjGB5TCB4gIBATA8MDcxGTAXBgNVBAMTEHRlc3Qu
ZXhhbXBsZS5jb20xGjAYBgNVBAoTEVRlc3QgT3JnYW5pemF0aW9uAgEBMA0GCWCG
SAFlAwQCAQUAMA0GCSqGSIb3DQEBAQUABIGAMxSC81vzjSiZcJTcSicgCG9879mu
p/bikqdHZPyF8UgABjikXXoVJ9IClFRd5/3wRf8ez+hwnxKR8j1vAsDI7U6dXJtr
wp0aO7Ua6ecKhI5V0F8nUg5CjEK9FR8vNg0Il8U5AXDuan0iYKvguhWkiuJlvQlV
X9ncPP4VRERPw8U=
-----END PKCS7-----`;
const result = crypto.getInfoFromPKCS7(mockP7Sign);
console.log('测试结果:');
console.log(' cert:');
console.log(' subject:', result.cert?.subjectDN);
console.log(' issuer:', result.cert?.issuerDN);
console.log(' serialNumber:', result.cert?.serialNumber);
console.log(' notBefore:', result.cert?.notBefore);
console.log(' notAfter:', result.cert?.notAfter);
console.log(' pem:');
console.log(result.cert?.pem);
console.log(' publicKey:');
console.log(result.publicKey);
console.log(' publicKey (hex):');
console.log(result.publicKeyHex);
console.log(' signature (hex):');
console.log(result.signatureHex);
console.log('✓ getInfoFromPKCS7 接口(RSA)测试通过');
} catch (error) {
console.error('✗ getInfoFromPKCS7 接口(RSA)测试失败:', error.message);
console.error(error.stack);
}
// 测试 2: getInfoFromPKCS7 接口(SM2)
try {
console.log('\n2. 测试 getInfoFromPKCS7 接口(SM2)');
// 使用用户提供的真实 SM2 PKCS7 签名数据
const sm2P7Base64 = 'MIIDKgYKKoEcz1UGAQQCAqCCAxowggMWAgEBMQ4wDAYIKoEcz1UBgxEFADAMBgoqgRzPVQYBBAIBoIICNzCCAjMwggHWoAMCAQICBgD1tfRoKTAMBggqgRzPVQGDdQUAMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBMB4XDTI2MDQwMjA4MDEzOVoXDTM2MDMzMDA4MDEzOVowTTELMAkGA1UEBhMCQ04xDzANBgNVBAoMBlBCQ0NSQzEtMCsGA1UEAwwkOTRmMWY5MzgtZjBiZC00NzYyLTkyOGUtZjY4YTdjY2UxNDFkMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu3dU5Vn20VjVL9xGCCRPB/EpATY6HW24+zIZv46Cfv0R2PGM+ZJs/dYarw7qJCnpIbe5T0PKmyyJITNVrVnnMaOBrDCBqTAfBgNVHSMEGDAWgBRUqfVQHMpD+o+YdrPK1JlRJL757zAJBgNVHRMEAjAAME8GA1UdHwRIMEYwRKBCoECkPjA8MQ4wDAYDVQQDDAVjcmw3MTEMMAoGA1UECwwDY3JsMQ8wDQYDVQQKDAZQQkNDUkMxCzAJBgNVBAYTAkNOMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU5bG6U0lYzWtkPJE74M/3HdafKKcwDAYIKoEcz1UBg3UFAANJADBGAiEAuLTE36/Eobcui23bFiPvOU4MOT0O0r9SyyHZ1JuvLwMCIQCejuSk03jyUAkbh1SaPx5n7WlDjI6tx9v9RJ7iw5rlojGBtzCBtAIBATBKMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBAgYA9bX0aCkwDAYIKoEcz1UBgxEFADANBgkqgRzPVQGCLQEFAARGMEQCIALTgWMD7io1JHfzInBwzDQpe9bonchuLFSED//JLDtnAiBmEUtr+ssAQIzFG4wY0CAj9QZxhENyCU2UYL1fhaysUA==';
const result = crypto.getInfoFromPKCS7(sm2P7Base64);
console.log('测试结果:');
console.log(' cert:');
console.log(' subject:', result.cert?.subjectDN);
console.log(' issuer:', result.cert?.issuerDN);
console.log(' serialNumber:', result.cert?.serialNumber);
console.log(' notBefore:', result.cert?.notBefore);
console.log(' notAfter:', result.cert?.notAfter);
console.log(' pem:');
console.log(result.cert?.pem);
console.log(' publicKey:');
console.log(result.publicKey);
console.log(' publicKey (hex):');
console.log(result.publicKeyHex);
console.log(' signature (hex):');
console.log(result.signatureHex);
console.log('✓ getInfoFromPKCS7 接口(SM2)测试通过');
} catch (error) {
console.error('✗ getInfoFromPKCS7 接口(SM2)测试失败:', error.message);
console.error(error.stack);
}
// 测试 3: verifyPKCS7 接口(RSA)
try {
console.log('\n3. 测试 verifyPKCS7 接口(RSA)');
// 使用真实的 PKCS7 签名数据和原始数据进行测试
const mockP7Sign = `-----BEGIN PKCS7-----
MIIDNwYJKoZIhvcNAQcCoIIDKDCCAyQCAQExDzANBglghkgBZQMEAgEFADA8Bgkq
hkiG9w0BBwGgLwQtVGhpcyBpcyB0aGUgdGVzdCBjb250ZW50IGZvciBQS0NTNyBz
aWduYXR1cmUuoIIB5jCCAeIwggFLoAMCAQICAQEwDQYJKoZIhvcNAQELBQAwNzEZ
MBcGA1UEAxMQdGVzdC5leGFtcGxlLmNvbTEaMBgGA1UEChMRVGVzdCBPcmdhbml6
YXRpb24wHhcNMjYwNDA0MDAxMDIyWhcNMjcwNDA0MDAxMDIyWjA3MRkwFwYDVQQD
ExB0ZXN0LmV4YW1wbGUuY29tMRowGAYDVQQKExFUZXN0IE9yZ2FuaXphdGlvbjCB
nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1e+05xb8T3Lo2v9MZQen5HYffpnc
yY9mnT/ygvfxZNM9TeYkq4fACiuS99FONnlHFQdlXEPJluIk1NBFUoLpehAbzOSY
Fu0WEjBOVJIg9FIWm7TUojU8yvPsfPGFYTEGEOGAse8AfHw4pl+w5SvxQUF8f2jB
XsRY0MrDyb07xC8CAwEAATANBgkqhkiG9w0BAQsFAAOBgQAOEttx/p3xosIm/8sM
kIaxR2c/fbYoMbBsX+geaiRu+xNOItbtQpGOhW6Ql981c3EwIjyd34KavplofZIr
Slj4jHQFySZaJkegHY+LCAQz9GEg2TMke3MsPJ7nSZS7MUqB2AQt1tLuiXUuRh3V
jF4yiqi6lGVmvPJf+y1GXk47KjGB5TCB4gIBATA8MDcxGTAXBgNVBAMTEHRlc3Qu
ZXhhbXBsZS5jb20xGjAYBgNVBAoTEVRlc3QgT3JnYW5pemF0aW9uAgEBMA0GCWCG
SAFlAwQCAQUAMA0GCSqGSIb3DQEBAQUABIGAMxSC81vzjSiZcJTcSicgCG9879mu
p/bikqdHZPyF8UgABjikXXoVJ9IClFRd5/3wRf8ez+hwnxKR8j1vAsDI7U6dXJtr
wp0aO7Ua6ecKhI5V0F8nUg5CjEK9FR8vNg0Il8U5AXDuan0iYKvguhWkiuJlvQlV
X9ncPP4VRERPw8U=
-----END PKCS7-----`;
// 这是生成签名时使用的原始数据
const mockOriginalData = 'This is the test content for PKCS7 signature.';
const result = crypto.verifyPKCS7(mockP7Sign, mockOriginalData);
console.log('测试结果:', result);
if( !result ) {
throw new Error("验签不通过")
}
console.log('✓ verifyPKCS7 接口(RSA)测试通过');
} catch (error) {
console.error('✗ verifyPKCS7 接口(RSA)测试失败:', error.message);
console.error(error.stack);
}
// 测试 4: verifyPKCS7 接口(SM2)
try {
console.log('\n4. 测试 verifyPKCS7 接口(SM2)');
// 这里应该使用实际的 PKCS7 签名数据和原始数据进行测试
// 由于没有实际数据,这里使用模拟数据
const mockP7Sign = 'MIIDKgYKKoEcz1UGAQQCAqCCAxowggMWAgEBMQ4wDAYIKoEcz1UBgxEFADAMBgoqgRzPVQYBBAIBoIICNzCCAjMwggHWoAMCAQICBgD1tfRoKTAMBggqgRzPVQGDdQUAMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBMB4XDTI2MDQwMjA4MDEzOVoXDTM2MDMzMDA4MDEzOVowTTELMAkGA1UEBhMCQ04xDzANBgNVBAoMBlBCQ0NSQzEtMCsGA1UEAwwkOTRmMWY5MzgtZjBiZC00NzYyLTkyOGUtZjY4YTdjY2UxNDFkMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu3dU5Vn20VjVL9xGCCRPB/EpATY6HW24+zIZv46Cfv0R2PGM+ZJs/dYarw7qJCnpIbe5T0PKmyyJITNVrVnnMaOBrDCBqTAfBgNVHSMEGDAWgBRUqfVQHMpD+o+YdrPK1JlRJL757zAJBgNVHRMEAjAAME8GA1UdHwRIMEYwRKBCoECkPjA8MQ4wDAYDVQQDDAVjcmw3MTEMMAoGA1UECwwDY3JsMQ8wDQYDVQQKDAZQQkNDUkMxCzAJBgNVBAYTAkNOMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU5bG6U0lYzWtkPJE74M/3HdafKKcwDAYIKoEcz1UBg3UFAANJADBGAiEAuLTE36/Eobcui23bFiPvOU4MOT0O0r9SyyHZ1JuvLwMCIQCejuSk03jyUAkbh1SaPx5n7WlDjI6tx9v9RJ7iw5rlojGBtzCBtAIBATBKMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBAgYA9bX0aCkwDAYIKoEcz1UBgxEFADANBgkqgRzPVQGCLQEFAARGMEQCIALTgWMD7io1JHfzInBwzDQpe9bonchuLFSED//JLDtnAiBmEUtr+ssAQIzFG4wY0CAj9QZxhENyCU2UYL1fhaysUA==';
const mockOriginalData = 'QDJNHHVB_FWQ';
const result = crypto.verifyPKCS7(mockP7Sign, mockOriginalData);
console.log('测试结果:', result);
if( !result ) {
throw new Error("验签不通过")
}
console.log('✓ verifyPKCS7 接口(SM2)测试通过');
} catch (error) {
console.error('✗ verifyPKCS7 接口(SM2)测试失败:', error.message);
}
console.log('\n=== 测试完成 ===');
+60
View File
@@ -0,0 +1,60 @@
const forge = require('node-forge');
const mockP7Sign = `-----BEGIN PKCS7-----
MIIDNwYJKoZIhvcNAQcCoIIDKDCCAyQCAQExDzANBglghkgBZQMEAgEFADA8Bgkq
hkiG9w0BBwGgLwQtVGhpcyBpcyB0aGUgdGVzdCBjb250ZW50IGZvciBQS0NTNyBz
aWduYXR1cmUuoIIB5jCCAeIwggFLoAMCAQICAQEwDQYJKoZIhvcNAQELBQAwNzEZ
MBcGA1UEAxMQdGVzdC5leGFtcGxlLmNvbTEaMBgGA1UEChMRVGVzdCBPcmdhbml6
YXRpb24wHhcNMjYwNDA0MDAxMDIyWhcNMjcwNDA0MDAxMDIyWjA3MRkwFwYDVQQD
ExB0ZXN0LmV4YW1wbGUuY29tMRowGAYDVQQKExFUZXN0IE9yZ2FuaXphdGlvbjCB
nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1e+05xb8T3Lo2v9MZQen5HYffpnc
yY9mnT/ygvfxZNM9TeYkq4fACiuS99FONnlHFQdlXEPJluIk1NBFUoLpehAbzOSY
Fu0WEjBOVJIg9FIWm7TUojU8yvPsfPGFYTEGEOGAse8AfHw4pl+w5SvxQUF8f2jB
XsRY0MrDyb07xC8CAwEAATANBgkqhkiG9w0BAQsFAAOBgQAOEttx/p3xosIm/8sM
kIaxR2c/fbYoMbBsX+geaiRu+xNOItbtQpGOhW6Ql981c3EwIjyd34KavplofZIr
Slj4jHQFySZaJkegHY+LCAQz9GEg2TMke3MsPJ7nSZS7MUqB2AQt1tLuiXUuRh3V
jF4yiqi6lGVmvPJf+y1GXk47KjGB5TCB4gIBATA8MDcxGTAXBgNVBAMTEHRlc3Qu
ZXhhbXBsZS5jb20xGjAYBgNVBAoTEVRlc3QgT3JnYW5pemF0aW9uAgEBMA0GCWCG
SAFlAwQCAQUAMA0GCSqGSIb3DQEBAQUABIGAMxSC81vzjSiZcJTcSicgCG9879mu
p/bikqdHZPyF8UgABjikXXoVJ9IClFRd5/3wRf8ez+hwnxKR8j1vAsDI7U6dXJtr
wp0aO7Ua6ecKhI5V0F8nUg5CjEK9FR8vNg0Il8U5AXDuan0iYKvguhWkiuJlvQlV
X9ncPP4VRERPw8U=
-----END PKCS7-----`;
const p7 = forge.pkcs7.messageFromPem(mockP7Sign);
console.log('=== p7.rawCapture ===');
console.log('rawCapture keys:', Object.keys(p7.rawCapture || {}));
if (p7.rawCapture) {
for (const key of Object.keys(p7.rawCapture)) {
const value = p7.rawCapture[key];
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
console.log(`${key}: Buffer/Uint8Array, length=${value.length}`);
console.log(`${key} hex:`, Buffer.from(value).toString('hex').substring(0, 100) + '...');
} else if (typeof value === 'string') {
console.log(`${key}: string, length=${value.length}`);
console.log(`${key} content:`, value.substring(0, 100) + '...');
} else if (value && typeof value === 'object') {
console.log(`${key}: object, keys=${Object.keys(value).join(',')}`);
} else {
console.log(`${key}:`, value);
}
}
}
console.log('\n=== p7.signerInfos ===');
console.log('signerInfos:', p7.signerInfos);
console.log('\n=== p7.signers ===');
console.log('signers:', p7.signers);
console.log('\n=== p7.certificates ===');
if (p7.certificates && p7.certificates.length > 0) {
const cert = p7.certificates[0];
console.log('cert publicKey:', cert.publicKey ? 'exists' : 'null');
if (cert.publicKey) {
console.log('publicKey type:', typeof cert.publicKey);
console.log('publicKey keys:', Object.keys(cert.publicKey));
}
}
+24
View File
@@ -0,0 +1,24 @@
const rs = require('jsrsasign');
// 用户提供的 SM2 PKCS7 签名数据
const sm2P7Base64 = 'MIIDKgYKKoEcz1UGAQQCAqCCAxowggMWAgEBMQ4wDAYIKoEcz1UBgxEFADAMBgoqgRzPVQYBBAIBoIICNzCCAjMwggHWoAMCAQICBgD1tfRoKTAMBggqgRzPVQGDdQUAMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBMB4XDTI2MDQwMjA4MDEzOVoXDTM2MDMzMDA4MDEzOVowTTELMAkGA1UEBhMCQ04xDzANBgNVBAoMBlBCQ0NSQzEtMCsGA1UEAwwkOTRmMWY5MzgtZjBiZC00NzYyLTkyOGUtZjY4YTdjY2UxNDFkMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu3dU5Vn20VjVL9xGCCRPB/EpATY6HW24+zIZv46Cfv0R2PGM+ZJs/dYarw7qJCnpIbe5T0PKmyyJITNVrVnnMaOBrDCBqTAfBgNVHSMEGDAWgBRUqfVQHMpD+o+YdrPK1JlRJL757zAJBgNVHRMEAjAAME8GA1UdHwRIMEYwRKBCoECkPjA8MQ4wDAYDVQQDDAVjcmw3MTEMMAoGA1UECwwDY3JsMQ8wDQYDVQQKDAZQQkNDUkMxCzAJBgNVBAYTAkNOMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU5bG6U0lYzWtkPJE74M/3HdafKKcwDAYIKoEcz1UBg3UFAANJADBGAiEAuLTE36/Eobcui23bFiPvOU4MOT0O0r9SyyHZ1JuvLwMCIQCejuSk03jyUAkbh1SaPx5n7WlDjI6tx9v9RJ7iw5rlojGBtzCBtAIBATBKMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBAgYA9bX0aCkwDAYIKoEcz1UBgxEFADANBgkqgRzPVQGCLQEFAARGMEQCIALTgWMD7io1JHfzInBwzDQpe9bonchuLFSED//JLDtnAiBmEUtr+ssAQIzFG4wY0CAj9QZxhENyCU2UYL1fhaysUA==';
console.log('=== 调试签名格式 ===');
const parser = new rs.KJUR.asn1.cms.CMSParser();
const p7 = parser.getCMSSignedData(rs.b64tohex(sm2P7Base64));
if (p7.sinfos && p7.sinfos.length > 0) {
const signer = p7.sinfos[0];
console.log('签名算法:', signer.sigalg);
console.log('签名值 (signer.sighex):', signer.sighex);
console.log('签名值长度:', signer.sighex.length / 2, '字节');
console.log('签名值开头:', signer.sighex.substring(0, 10));
// 检查是否是 ASN.1 DER 格式
if (signer.sighex.startsWith('30')) {
console.log('签名格式: ASN.1 DER');
} else {
console.log('签名格式: 原始 r|s');
}
}
+25
View File
@@ -0,0 +1,25 @@
const smCrypto = require('sm-crypto');
console.log('=== 调试 sm-crypto API ===');
console.log('sm-crypto 模块:', Object.keys(smCrypto));
console.log('sm2 对象:', Object.keys(smCrypto.sm2));
console.log('sm3 对象:', typeof smCrypto.sm3);
// 测试 SM3
const testData = 'test';
const hash = smCrypto.sm3(testData);
console.log('\nSM3 哈希:', hash);
// 生成密钥对
const keypair = smCrypto.sm2.generateKeyPairHex();
console.log('\n生成的密钥对:');
console.log(' 私钥:', keypair.privateKey.substring(0, 20) + '...');
console.log(' 公钥:', keypair.publicKey.substring(0, 20) + '...');
// 签名
const sig = smCrypto.sm2.doSignature(hash, keypair.privateKey);
console.log('\n签名:', sig);
// 验证签名
const verifyResult = smCrypto.sm2.doVerifySignature(hash, sig, keypair.publicKey);
console.log('验证结果:', verifyResult);
+22
View File
@@ -0,0 +1,22 @@
const sm2 = require('sm-crypto').sm2;
console.log('=== 检查 sm-crypto 0.3.13 API ===');
// 检查 sm2 对象的属性
console.log('sm2 对象属性:', Object.keys(sm2));
// 检查 doVerifySignature 函数
console.log('\ndoVerifySignature 函数:', sm2.doVerifySignature.toString());
// 测试生成的签名和公钥
const testData = 'test';
const keypair = sm2.generateKeyPairHex();
const sig = sm2.doSignature(testData, keypair.privateKey);
console.log('\n测试数据:', testData);
console.log('生成的公钥:', keypair.publicKey);
console.log('生成的签名:', sig);
// 验证生成的签名
const verifyResult = sm2.doVerifySignature(testData, sig, keypair.publicKey);
console.log('验证结果:', verifyResult);
+55
View File
@@ -0,0 +1,55 @@
const sm2 = require('sm-crypto').sm2;
const sm3 = require('sm-crypto').sm3;
console.log('=== 调试 sm-crypto 0.3.13 API ===');
// 测试基本功能
const testData = 'test';
const hash = sm3(testData);
console.log('测试数据:', testData);
console.log('SM3 哈希:', hash);
// 生成密钥对
const keypair = sm2.generateKeyPairHex();
console.log('\n生成的密钥对:');
console.log(' 私钥:', keypair.privateKey);
console.log(' 公钥:', keypair.publicKey);
// 签名
const sig = sm2.doSignature(testData, keypair.privateKey);
console.log('\n签名:', sig);
console.log('签名长度:', sig.length);
// 验证签名
const verifyResult = sm2.doVerifySignature(testData, sig, keypair.publicKey);
console.log('验证结果:', verifyResult);
// 测试用户提供的数据
console.log('\n=== 测试用户数据 ===');
const data = 'QDJNHHVB_FWQ';
const signature = '02D3816303EE2A352477F3227070CC34297BD6E89DC86E2C54840FFFC92C3B6766114B6BFA CB00408CC51B8C18D02023F50671844372094D9460BD5F85ACAC50'.replace(/\s/g, '');
const publicKey = '04bb7754e559f6d158d52fdc4608244f07f12901363a1d6db8fb3219bf8e827efd11d8f18cf9926cfdd61aaf0eea2429e921b7b94f43ca9b2c89213355ad59e731';
console.log('数据:', data);
console.log('签名:', signature);
console.log('公钥:', publicKey);
// 尝试验证
console.log('\n尝试验证:');
try {
// 注意:sm-crypto 0.3.13 的 API 可能不同
// 尝试不带选项
const result1 = sm2.doVerifySignature(data, signature, publicKey);
console.log(' 验证结果 (无选项):', result1);
} catch (e) {
console.error(' 错误 (无选项):', e.message);
}
try {
// 尝试带哈希选项
const result2 = sm2.doVerifySignature(hash, signature, publicKey, true); // 第二个参数是 hash 选项
console.log(' 验证结果 (hash=true):', result2);
} catch (e) {
console.error(' 错误 (hash=true):', e.message);
}
+37
View File
@@ -0,0 +1,37 @@
const rs = require('jsrsasign');
// 用户提供的 SM2 PKCS7 签名数据(没有头尾)
const sm2P7Base64 = 'MIIDKgYKKoEcz1UGAQQCAqCCAxowggMWAgEBMQ4wDAYIKoEcz1UBgxEFADAMBgoqgRzPVQYBBAIBoIICNzCCAjMwggHWoAMCAQICBgD1tfRoKTAMBggqgRzPVQGDdQUAMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBMB4XDTI2MDQwMjA4MDEzOVoXDTM2MDMzMDA4MDEzOVowTTELMAkGA1UEBhMCQ04xDzANBgNVBAoMBlBCQ0NSQzEtMCsGA1UEAwwkOTRmMWY5MzgtZjBiZC00NzYyLTkyOGUtZjY4YTdjY2UxNDFkMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu3dU5Vn20VjVL9xGCCRPB/EpATY6HW24+zIZv46Cfv0R2PGM+ZJs/dYarw7qJCnpIbe5T0PKmyyJITNVrVnnMaOBrDCBqTAfBgNVHSMEGDAWgBRUqfVQHMpD+o+YdrPK1JlRJL757zAJBgNVHRMEAjAAME8GA1UdHwRIMEYwRKBCoECkPjA8MQ4wDAYDVQQDDAVjcmw3MTEMMAoGA1UECwwDY3JsMQ8wDQYDVQQKDAZQQkNDUkMxCzAJBgNVBAYTAkNOMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU5bG6U0lYzWtkPJE74M/3HdafKKcwDAYIKoEcz1UBg3UFAANJADBGAiEAuLTE36/Eobcui23bFiPvOU4MOT0O0r9SyyHZ1JuvLwMCIQCejuSk03jyUAkbh1SaPx5n7WlDjI6tx9v9RJ7iw5rlojGBtzCBtAIBATBKMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBAgYA9bX0aCkwDAYIKoEcz1UBgxEFADANBgkqgRzPVQGCLQEFAARGMEQCIALTgWMD7io1JHfzInBwzDQpe9bonchuLFSED//JLDtnAiBmEUtr+ssAQIzFG4wY0CAj9QZxhENyCU2UYL1fhaysUA==';
const parser = new rs.KJUR.asn1.cms.CMSParser();
const p7 = parser.getCMSSignedData(rs.b64tohex(sm2P7Base64));
console.log('=== 调试 SM2 证书提取 ===');
console.log('p7 keys:', Object.keys(p7));
console.log('p7.certs:', p7.certs);
console.log('p7.sinfos:', p7.sinfos);
// 尝试直接解析证书
console.log('\n=== 直接解析证书 ===');
try {
// 从 hex 中提取证书
const hex = rs.b64tohex(sm2P7Base64);
console.log('hex length:', hex.length);
// 使用 ASN1HEX 解析
const hCert = rs.ASN1HEX.getTLVbyList(hex, 0, [0, 3, 0, 0]);
console.log('hCert:', hCert ? hCert.substring(0, 50) + '...' : 'null');
if (hCert) {
const certPem = rs.KJUR.asn1.ASN1Util.getPEMStringFromHex(hCert, 'CERTIFICATE');
console.log('certPem:', certPem.substring(0, 200));
const x509 = new rs.X509();
x509.readCertPEM(certPem);
console.log('\n主题:', x509.getSubjectString());
console.log('颁发者:', x509.getIssuerString());
}
} catch (e) {
console.error('错误:', e.message);
}
+29
View File
@@ -0,0 +1,29 @@
const sm2 = require('sm-crypto').sm2;
console.log('=== 调试 SM2 DER 格式 ===');
// 测试数据
const data = 'QDJNHHVB_FWQ';
const userId = '1234567812345678';
// 生成密钥对
const keypair = sm2.generateKeyPairHex();
console.log('公钥:', keypair.publicKey);
// 生成签名(带 der 选项)
const sigWithDer = sm2.doSignature(data, keypair.privateKey, { der: true });
console.log('\n带 DER 的签名:', sigWithDer);
console.log('带 DER 的签名长度:', sigWithDer.length);
// 生成签名(不带 der 选项)
const sigWithoutDer = sm2.doSignature(data, keypair.privateKey);
console.log('\n不带 DER 的签名:', sigWithoutDer);
console.log('不带 DER 的签名长度:', sigWithoutDer.length);
// 验证带 DER 的签名
const verifyWithDer = sm2.doVerifySignature(data, sigWithDer, keypair.publicKey, { der: true });
console.log('\n验证带 DER 的签名:', verifyWithDer);
// 验证不带 DER 的签名
const verifyWithoutDer = sm2.doVerifySignature(data, sigWithoutDer, keypair.publicKey);
console.log('验证不带 DER 的签名:', verifyWithoutDer);
+42
View File
@@ -0,0 +1,42 @@
const crypto = require('../src/crypto.js');
// 用户提供的 SM2 PKCS7 签名数据
const sm2P7Base64 = 'MIIDKgYKKoEcz1UGAQQCAqCCAxowggMWAgEBMQ4wDAYIKoEcz1UBgxEFADAMBgoqgRzPVQYBBAIBoIICNzCCAjMwggHWoAMCAQICBgD1tfRoKTAMBggqgRzPVQGDdQUAMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBMB4XDTI2MDQwMjA4MDEzOVoXDTM2MDMzMDA4MDEzOVowTTELMAkGA1UEBhMCQ04xDzANBgNVBAoMBlBCQ0NSQzEtMCsGA1UEAwwkOTRmMWY5MzgtZjBiZC00NzYyLTkyOGUtZjY4YTdjY2UxNDFkMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu3dU5Vn20VjVL9xGCCRPB/EpATY6HW24+zIZv46Cfv0R2PGM+ZJs/dYarw7qJCnpIbe5T0PKmyyJITNVrVnnMaOBrDCBqTAfBgNVHSMEGDAWgBRUqfVQHMpD+o+YdrPK1JlRJL757zAJBgNVHRMEAjAAME8GA1UdHwRIMEYwRKBCoECkPjA8MQ4wDAYDVQQDDAVjcmw3MTEMMAoGA1UECwwDY3JsMQ8wDQYDVQQKDAZQQkNDUkMxCzAJBgNVBAYTAkNOMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU5bG6U0lYzWtkPJE74M/3HdafKKcwDAYIKoEcz1UBg3UFAANJADBGAiEAuLTE36/Eobcui23bFiPvOU4MOT0O0r9SyyHZ1JuvLwMCIQCejuSk03jyUAkbh1SaPx5n7WlDjI6tx9v9RJ7iw5rlojGBtzCBtAIBATBKMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBAgYA9bX0aCkwDAYIKoEcz1UBgxEFADANBgkqgRzPVQGCLQEFAARGMEQCIALTgWMD7io1JHfzInBwzDQpe9bonchuLFSED//JLDtnAiBmEUtr+ssAQIzFG4wY0CAj9QZxhENyCU2UYL1fhaysUA==';
console.log('=== 测试不同的原始数据 ===');
// 测试不同的原始数据
const testDataList = [
'QDJNHHVB_FWQ',
'QDJNHHVB_FWQ\n',
'QDJNHHVB_FWQ\r\n',
'QDJNHHVB FWQ', // 空格替换下划线
'QDJNHHVB_FW', // 少一个字符
'QDJNHHVB_FWQQ', // 多一个字符
'qDJNHHVB_FWQ', // 小写
'QDJnhhVB_FWQ', // 混合大小写
'1234567890', // 数字
'', // 空字符串
];
testDataList.forEach((data, index) => {
console.log(`\n测试数据 ${index + 1}: "${data}"`);
console.log(` 长度: ${data.length}`);
console.log(` UTF-8 编码: ${Buffer.from(data, 'utf8').toString('hex')}`);
const result = crypto.verifyPKCS7(sm2P7Base64, data);
console.log(` 验证结果: ${result}`);
if (result) {
console.log(' ✅ 验证成功!');
}
});
// 也测试一下 attach 模式
console.log('\n=== 测试 attach 模式 ===');
try {
const result = crypto.verifyPKCS7(sm2P7Base64);
console.log('attach 模式验证结果:', result);
} catch (e) {
console.error('attach 模式错误:', e.message);
}
+67
View File
@@ -0,0 +1,67 @@
const rs = require('jsrsasign');
// 用户提供的 SM2 PKCS7 签名数据(没有头尾)
const sm2P7Base64 = 'MIIDKgYKKoEcz1UGAQQCAqCCAxowggMWAgEBMQ4wDAYIKoEcz1UBgxEFADAMBgoqgRzPVQYBBAIBoIICNzCCAjMwggHWoAMCAQICBgD1tfRoKTAMBggqgRzPVQGDdQUAMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBMB4XDTI2MDQwMjA4MDEzOVoXDTM2MDMzMDA4MDEzOVowTTELMAkGA1UEBhMCQ04xDzANBgNVBAoMBlBCQ0NSQzEtMCsGA1UEAwwkOTRmMWY5MzgtZjBiZC00NzYyLTkyOGUtZjY4YTdjY2UxNDFkMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu3dU5Vn20VjVL9xGCCRPB/EpATY6HW24+zIZv46Cfv0R2PGM+ZJs/dYarw7qJCnpIbe5T0PKmyyJITNVrVnnMaOBrDCBqTAfBgNVHSMEGDAWgBRUqfVQHMpD+o+YdrPK1JlRJL757zAJBgNVHRMEAjAAME8GA1UdHwRIMEYwRKBCoECkPjA8MQ4wDAYDVQQDDAVjcmw3MTEMMAoGA1UECwwDY3JsMQ8wDQYDVQQKDAZQQkNDUkMxCzAJBgNVBAYTAkNOMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU5bG6U0lYzWtkPJE74M/3HdafKKcwDAYIKoEcz1UBg3UFAANJADBGAiEAuLTE36/Eobcui23bFiPvOU4MOT0O0r9SyyHZ1JuvLwMCIQCejuSk03jyUAkbh1SaPx5n7WlDjI6tx9v9RJ7iw5rlojGBtzCBtAIBATBKMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBAgYA9bX0aCkwDAYIKoEcz1UBgxEFADANBgkqgRzPVQGCLQEFAARGMEQCIALTgWMD7io1JHfzInBwzDQpe9bonchuLFSED//JLDtnAiBmEUtr+ssAQIzFG4wY0CAj9QZxhENyCU2UYL1fhaysUA==';
const parser = new rs.KJUR.asn1.cms.CMSParser();
const p7 = parser.getCMSSignedData(rs.b64tohex(sm2P7Base64));
console.log('=== 调试 SM2 公钥提取 ===');
if (p7.certs && p7.certs.array && p7.certs.array.length > 0) {
const certPem = p7.certs.array[0];
console.log('证书 PEM 前200字符:', certPem.substring(0, 200));
const x509 = new rs.X509();
x509.readCertPEM(certPem);
console.log('\n证书信息:');
console.log('主题:', x509.getSubjectString());
console.log('颁发者:', x509.getIssuerString());
console.log('序列号:', x509.getSerialNumberHex());
// 尝试获取公钥
console.log('\n尝试获取公钥:');
try {
const pubKey = x509.getPublicKey();
console.log('getPublicKey() 结果:', pubKey);
if (pubKey) {
console.log('公钥类型:', typeof pubKey);
console.log('公钥 keys:', Object.keys(pubKey));
}
} catch (e) {
console.error('getPublicKey() 错误:', e.message);
}
// 尝试直接从 ASN.1 解析
console.log('\n尝试从 ASN.1 解析公钥:');
try {
const hex = rs.pemtohex(certPem);
// 证书结构: SEQUENCE -> tbsCertificate -> subjectPublicKeyInfo
const hTBS = rs.ASN1HEX.getTLVbyList(hex, 0, [0]);
const hSPKI = rs.ASN1HEX.getTLVbyList(hTBS, 0, [6]); // subjectPublicKeyInfo 通常在第6个位置
if (hSPKI) {
console.log('subjectPublicKeyInfo hex:', hSPKI.substring(0, 100) + '...');
// 提取公钥值
const hAlgorithm = rs.ASN1HEX.getTLVbyList(hSPKI, 0, [0]);
const hPublicKey = rs.ASN1HEX.getTLVbyList(hSPKI, 0, [1]);
if (hAlgorithm) {
console.log('算法 OID:', rs.ASN1HEX.getOIDbyList(hAlgorithm, 0, [0]));
}
if (hPublicKey) {
console.log('公钥值 hex:', hPublicKey.substring(4) + '...'); // 跳过 tag 和 length
// 尝试创建 PEM
const pubKeyPem = rs.KJUR.asn1.ASN1Util.getPEMStringFromHex(hSPKI, 'PUBLIC KEY');
console.log('公钥 PEM:', pubKeyPem.substring(0, 200) + '...');
}
}
} catch (e) {
console.error('ASN.1 解析错误:', e.message);
}
}
+95
View File
@@ -0,0 +1,95 @@
const rs = require('jsrsasign');
// 用户提供的 SM2 PKCS7 签名数据
const sm2P7Base64 = 'MIIDKgYKKoEcz1UGAQQCAqCCAxowggMWAgEBMQ4wDAYIKoEcz1UBgxEFADAMBgoqgRzPVQYBBAIBoIICNzCCAjMwggHWoAMCAQICBgD1tfRoKTAMBggqgRzPVQGDdQUAMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBMB4XDTI2MDQwMjA4MDEzOVoXDTM2MDMzMDA4MDEzOVowTTELMAkGA1UEBhMCQ04xDzANBgNVBAoMBlBCQ0NSQzEtMCsGA1UEAwwkOTRmMWY5MzgtZjBiZC00NzYyLTkyOGUtZjY4YTdjY2UxNDFkMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu3dU5Vn20VjVL9xGCCRPB/EpATY6HW24+zIZv46Cfv0R2PGM+ZJs/dYarw7qJCnpIbe5T0PKmyyJITNVrVnnMaOBrDCBqTAfBgNVHSMEGDAWgBRUqfVQHMpD+o+YdrPK1JlRJL757zAJBgNVHRMEAjAAME8GA1UdHwRIMEYwRKBCoECkPjA8MQ4wDAYDVQQDDAVjcmw3MTEMMAoGA1UECwwDY3JsMQ8wDQYDVQQKDAZQQkNDUkMxCzAJBgNVBAYTAkNOMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU5bG6U0lYzWtkPJE74M/3HdafKKcwDAYIKoEcz1UBg3UFAANJADBGAiEAuLTE36/Eobcui23bFiPvOU4MOT0O0r9SyyHZ1JuvLwMCIQCejuSk03jyUAkbh1SaPx5n7WlDjI6tx9v9RJ7iw5rlojGBtzCBtAIBATBKMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBAgYA9bX0aCkwDAYIKoEcz1UBgxEFADANBgkqgRzPVQGCLQEFAARGMEQCIALTgWMD7io1JHfzInBwzDQpe9bonchuLFSED//JLDtnAiBmEUtr+ssAQIzFG4wY0CAj9QZxhENyCU2UYL1fhaysUA==';
const parser = new rs.KJUR.asn1.cms.CMSParser();
const p7 = parser.getCMSSignedData(rs.b64tohex(sm2P7Base64));
console.log('=== 调试 SM2 公钥提取 ===');
if (p7.certs && p7.certs.array && p7.certs.array.length > 0) {
const certPem = p7.certs.array[0];
// 将 PEM 转换为 hex
const certHex = rs.pemtohex(certPem);
console.log('证书 hex 长度:', certHex.length);
console.log('证书 hex 前100字符:', certHex.substring(0, 100));
// 证书结构: SEQUENCE { tbsCertificate, signatureAlgorithm, signatureValue }
// tbsCertificate 结构: SEQUENCE { version, serialNumber, signature, issuer, validity, subject, subjectPublicKeyInfo, ... }
try {
// 获取 tbsCertificate
const tbsCert = rs.ASN1HEX.getTLVbyList(certHex, 0, [0]);
console.log('\ntbsCertificate 长度:', tbsCert.length);
// 获取 subjectPublicKeyInfo (在 tbsCertificate 中的索引通常是 6)
// 但我们需要先确定正确的索引
let spkiIndex = 6;
let spki = null;
// 尝试不同的索引
for (let i = 3; i <= 8; i++) {
try {
const tlv = rs.ASN1HEX.getTLVbyList(tbsCert, 0, [i]);
if (tlv && tlv.startsWith('30')) { // SEQUENCE
// 检查是否是 subjectPublicKeyInfo
// subjectPublicKeyInfo 应该包含 algorithm 和 subjectPublicKey
const inner = rs.ASN1HEX.getTLVbyList(tlv, 0, [0]);
if (inner && inner.startsWith('30')) {
console.log(`索引 ${i} 可能是 subjectPublicKeyInfo`);
spki = tlv;
spkiIndex = i;
break;
}
}
} catch (e) {
// 忽略错误
}
}
if (spki) {
console.log('\nsubjectPublicKeyInfo hex:', spki.substring(0, 100));
// 提取算法 OID
try {
const algorithmSeq = rs.ASN1HEX.getTLVbyList(spki, 0, [0]);
console.log('algorithmSeq:', algorithmSeq);
// 提取 OID
const oidTlv = rs.ASN1HEX.getTLVbyList(algorithmSeq, 0, [0]);
console.log('OID TLV:', oidTlv);
// 解析 OID
if (oidTlv && oidTlv.startsWith('06')) {
const oidHex = oidTlv.substring(4);
console.log('OID hex:', oidHex);
}
} catch (e) {
console.error('提取算法 OID 错误:', e.message);
}
// 提取公钥值 (BIT STRING)
try {
const publicKeyBitString = rs.ASN1HEX.getTLVbyList(spki, 0, [1]);
console.log('\n公钥 BIT STRING:', publicKeyBitString.substring(0, 100));
// BIT STRING 的内容从第4个字符开始 (跳过 tag 03, length, 和 unused bits 00)
const publicKeyContent = publicKeyBitString.substring(4);
console.log('公钥内容:', publicKeyContent.substring(0, 100));
// 对于 SM2,公钥通常是 65 字节 (0x04 + 32字节 X + 32字节 Y)
console.log('公钥内容长度:', publicKeyContent.length / 2, '字节');
// 创建公钥 hex
console.log('\n公钥 hex (完整):', publicKeyContent);
} catch (e) {
console.error('提取公钥值错误:', e.message);
}
}
} catch (e) {
console.error('ASN.1 解析错误:', e.message);
console.error(e.stack);
}
}
+45
View File
@@ -0,0 +1,45 @@
const sm2 = require('sm-crypto').sm2;
const sm3 = require('sm-crypto').sm3;
console.log('=== 调试 SM2 公钥格式 ===');
// 测试数据
const testData = 'QDJNHHVB_FWQ';
const hash = sm3(testData);
console.log('测试数据:', testData);
console.log('SM3 哈希:', hash);
// 用户提供的签名(ASN.1 DER 格式)
const userSignature = '3044022002d3816303ee2a352477f3227070cc34297bd6e89dc86e2c54840fffc92c3b67022066114b6bfacb00408cc51b8c18d02023f50671844372094d9460bd5f85acac50';
// 解析签名
const rStart = 8;
const r = userSignature.substring(rStart, rStart + 64);
const sStart = rStart + 64 + 4;
const s = userSignature.substring(sStart, sStart + 64);
const rawSignature = r + s;
console.log('\n原始签名格式:', rawSignature);
// 用户公钥
const userPublicKey = '04bb7754e559f6d158d52fdc4608244f07f12901363a1d6db8fb3219bf8e827efd11d8f18cf9926cfdd61aaf0eea2429e921b7b94f43ca9b2c89213355ad59e731';
console.log('\n完整公钥:', userPublicKey);
// 去掉 04 前缀
const pubKeyWithoutPrefix = userPublicKey.substring(2);
console.log('去掉前缀的公钥:', pubKeyWithoutPrefix);
// 尝试验证
console.log('\n验证结果:');
try {
const result1 = sm2.doVerifySignature(hash, rawSignature, userPublicKey);
console.log(' 使用完整公钥:', result1);
} catch (e) {
console.error(' 完整公钥错误:', e.message);
}
try {
const result2 = sm2.doVerifySignature(hash, rawSignature, pubKeyWithoutPrefix);
console.log(' 使用去掉前缀的公钥:', result2);
} catch (e) {
console.error(' 去掉前缀错误:', e.message);
}
+43
View File
@@ -0,0 +1,43 @@
const sm2 = require('sm-crypto').sm2;
console.log('=== 测试公钥格式 ===');
// 测试数据
const data = 'QDJNHHVB_FWQ';
const signature = '02D3816303EE2A352477F3227070CC34297BD6E89DC86E2C54840FFFC92C3B6766114B6BFACB00408CC51B8C18D02023F50671844372094D9460BD5F85ACAC50';
// 完整公钥(带 04 前缀)
const fullPublicKey = '04bb7754e559f6d158d52fdc4608244f07f12901363a1d6db8fb3219bf8e827efd11d8f18cf9926cfdd61aaf0eea2429e921b7b94f43ca9b2c89213355ad59e731';
// 去掉 04 前缀的公钥
const publicKeyWithoutPrefix = fullPublicKey.substring(2);
console.log('完整公钥:', fullPublicKey);
console.log('去掉前缀的公钥:', publicKeyWithoutPrefix);
// 测试完整公钥
console.log('\n使用完整公钥:');
try {
const result1 = sm2.doVerifySignature(data, signature, fullPublicKey);
console.log(' 验证结果:', result1);
} catch (e) {
console.error(' 错误:', e.message);
}
// 测试去掉前缀的公钥
console.log('\n使用去掉前缀的公钥:');
try {
const result2 = sm2.doVerifySignature(data, signature, publicKeyWithoutPrefix);
console.log(' 验证结果:', result2);
} catch (e) {
console.error(' 错误:', e.message);
}
// 测试带哈希选项
console.log('\n使用去掉前缀的公钥 + 哈希:');
try {
const result3 = sm2.doVerifySignature(data, signature, publicKeyWithoutPrefix, true);
console.log(' 验证结果:', result3);
} catch (e) {
console.error(' 错误:', e.message);
}
+67
View File
@@ -0,0 +1,67 @@
const sm2 = require('sm-crypto').sm2;
const sm3 = require('sm-crypto').sm3;
console.log('=== 调试 SM2 签名格式 ===');
// 生成测试数据
const testData = 'QDJNHHVB_FWQ';
const hash = sm3(testData);
console.log('测试数据:', testData);
console.log('SM3 哈希:', hash);
// 生成密钥对
const keypair = sm2.generateKeyPairHex();
console.log('\n生成的密钥对:');
console.log(' 私钥:', keypair.privateKey);
console.log(' 公钥:', keypair.publicKey);
// 生成签名
const sig = sm2.doSignature(hash, keypair.privateKey);
console.log('\nsm-crypto 生成的签名:', sig);
console.log('签名长度:', sig.length);
// 验证签名
const verifyResult = sm2.doVerifySignature(hash, sig, keypair.publicKey);
console.log('验证结果:', verifyResult);
// 对比用户提供的签名
const userSignature = '3044022002d3816303ee2a352477f3227070cc34297bd6e89dc86e2c54840fffc92c3b67022066114b6bfacb00408cc51b8c18d02023f50671844372094d9460bd5f85acac50';
console.log('\n用户提供的签名:', userSignature);
console.log('用户签名长度:', userSignature.length);
// 分析签名格式
console.log('\n签名格式分析:');
console.log('sm-crypto 签名格式:', sig.length === 128 ? '原始 r|s 格式' : '其他格式');
console.log('用户签名格式:', userSignature.startsWith('30') ? 'ASN.1 DER 格式' : '其他格式');
// 尝试转换用户签名格式
if (userSignature.startsWith('30')) {
console.log('\n尝试将 ASN.1 DER 格式转换为原始格式:');
try {
// 解析 ASN.1 DER 格式
// 格式: 30 len 02 len r 02 len s
const rStart = 6; // 30 44 02 20
const rLen = parseInt(userSignature.substring(4, 6), 16) * 2;
const r = userSignature.substring(rStart, rStart + rLen);
const sStart = rStart + rLen + 4; // 02 20
const sLen = parseInt(userSignature.substring(rStart + rLen + 2, rStart + rLen + 4), 16) * 2;
const s = userSignature.substring(sStart, sStart + sLen);
console.log(' 提取的 r:', r);
console.log(' 提取的 s:', s);
// 组合成原始格式
const rawSignature = r + s;
console.log(' 转换后的原始格式:', rawSignature);
console.log(' 原始格式长度:', rawSignature.length);
// 尝试验证
const userPublicKey = '04bb7754e559f6d158d52fdc4608244f07f12901363a1d6db8fb3219bf8e827efd11d8f18cf9926cfdd61aaf0eea2429e921b7b94f43ca9b2c89213355ad59e731';
const result = sm2.doVerifySignature(hash, rawSignature, userPublicKey);
console.log(' 验证结果:', result);
} catch (e) {
console.error(' 转换错误:', e.message);
}
}
+50
View File
@@ -0,0 +1,50 @@
const sm2 = require('sm-crypto').sm2;
const sm3 = require('sm-crypto').sm3;
console.log('=== 调试 SM2 签名格式(正确解析)===');
// 测试数据
const testData = 'QDJNHHVB_FWQ';
const hash = sm3(testData);
console.log('测试数据:', testData);
console.log('SM3 哈希:', hash);
// 用户提供的签名(ASN.1 DER 格式)
const userSignature = '3044022002d3816303ee2a352477f3227070cc34297bd6e89dc86e2c54840fffc92c3b67022066114b6bfacb00408cc51b8c18d02023f50671844372094d9460bd5f85acac50';
console.log('\n用户提供的签名:', userSignature);
// 正确解析 ASN.1 DER 格式
console.log('\n正确解析 ASN.1 DER 格式:');
try {
// 30 44 02 20 [r] 02 20 [s]
// 30 = 序列
// 44 = 长度 68 字节
// 02 = 整数
// 20 = 长度 32 字节
// 提取 r 值
const rStart = 8; // 30440220
const r = userSignature.substring(rStart, rStart + 64); // 32字节 = 64 hex 字符
console.log(' r 值:', r);
// 提取 s 值
const sStart = rStart + 64 + 4; // 0220
const s = userSignature.substring(sStart, sStart + 64); // 32字节 = 64 hex 字符
console.log(' s 值:', s);
// 组合成原始格式 (r + s)
const rawSignature = r + s;
console.log(' 转换后的原始格式:', rawSignature);
console.log(' 原始格式长度:', rawSignature.length);
// 用户公钥
const userPublicKey = '04bb7754e559f6d158d52fdc4608244f07f12901363a1d6db8fb3219bf8e827efd11d8f18cf9926cfdd61aaf0eea2429e921b7b94f43ca9b2c89213355ad59e731';
console.log('\n用户公钥:', userPublicKey);
// 尝试验证
const result = sm2.doVerifySignature(hash, rawSignature, userPublicKey);
console.log('\n验证结果:', result);
} catch (e) {
console.error('解析错误:', e.message);
}
+80
View File
@@ -0,0 +1,80 @@
const rs = require('jsrsasign');
const sm2 = require('sm-crypto').sm2;
const sm3 = require('sm-crypto').sm3;
// 用户提供的 SM2 PKCS7 签名数据
const sm2P7Base64 = 'MIIDKgYKKoEcz1UGAQQCAqCCAxowggMWAgEBMQ4wDAYIKoEcz1UBgxEFADAMBgoqgRzPVQYBBAIBoIICNzCCAjMwggHWoAMCAQICBgD1tfRoKTAMBggqgRzPVQGDdQUAMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBMB4XDTI2MDQwMjA4MDEzOVoXDTM2MDMzMDA4MDEzOVowTTELMAkGA1UEBhMCQ04xDzANBgNVBAoMBlBCQ0NSQzEtMCsGA1UEAwwkOTRmMWY5MzgtZjBiZC00NzYyLTkyOGUtZjY4YTdjY2UxNDFkMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu3dU5Vn20VjVL9xGCCRPB/EpATY6HW24+zIZv46Cfv0R2PGM+ZJs/dYarw7qJCnpIbe5T0PKmyyJITNVrVnnMaOBrDCBqTAfBgNVHSMEGDAWgBRUqfVQHMpD+o+YdrPK1JlRJL757zAJBgNVHRMEAjAAME8GA1UdHwRIMEYwRKBCoECkPjA8MQ4wDAYDVQQDDAVjcmw3MTEMMAoGA1UECwwDY3JsMQ8wDQYDVQQKDAZQQkNDUkMxCzAJBgNVBAYTAkNOMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU5bG6U0lYzWtkPJE74M/3HdafKKcwDAYIKoEcz1UBg3UFAANJADBGAiEAuLTE36/Eobcui23bFiPvOU4MOT0O0r9SyyHZ1JuvLwMCIQCejuSk03jyUAkbh1SaPx5n7WlDjI6tx9v9RJ7iw5rlojGBtzCBtAIBATBKMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBAgYA9bX0aCkwDAYIKoEcz1UBgxEFADANBgkqgRzPVQGCLQEFAARGMEQCIALTgWMD7io1JHfzInBwzDQpe9bonchuLFSED//JLDtnAiBmEUtr+ssAQIzFG4wY0CAj9QZxhENyCU2UYL1fhaysUA==';
// 测试用的原始数据
const mockOriginalData = 'QDJNHHVB_FWQ';
console.log('=== 调试 SM2 PKCS7 验证 ===');
console.log('原始数据:', mockOriginalData);
const parser = new rs.KJUR.asn1.cms.CMSParser();
const p7 = parser.getCMSSignedData(rs.b64tohex(sm2P7Base64));
console.log('\nPKCS7 信息:');
console.log('版本:', p7.version);
console.log('哈希算法:', p7.hashalgs);
console.log('签名者数量:', p7.sinfos ? p7.sinfos.length : 0);
if (p7.sinfos && p7.sinfos.length > 0) {
const signer = p7.sinfos[0];
console.log('\n签名者信息:');
console.log(' 签名算法:', signer.sigalg);
console.log(' 签名值 (hex):', signer.sighex);
console.log(' 签名值长度:', signer.sighex ? signer.sighex.length / 2 : 0, '字节');
}
// 提取原始数据(econtent)
if (p7.econtent && p7.econtent.hex) {
const extractedData = Buffer.from(p7.econtent.hex, 'hex').toString();
console.log('\n从 PKCS7 提取的原始数据:', extractedData);
console.log('与提供的原始数据匹配:', extractedData === mockOriginalData);
}
// 提取公钥
if (p7.certs && p7.certs.array && p7.certs.array.length > 0) {
const certPem = p7.certs.array[0];
const certHex = rs.pemtohex(certPem);
const tbsCert = rs.ASN1HEX.getTLVbyList(certHex, 0, [0]);
const spki = rs.ASN1HEX.getTLVbyList(tbsCert, 0, [6]);
const publicKeyBitString = rs.ASN1HEX.getTLVbyList(spki, 0, [1]);
const publicKeyHex = publicKeyBitString.substring(6);
console.log('\n公钥信息:');
console.log(' 公钥 (hex):', publicKeyHex.substring(0, 50) + '...');
console.log(' 公钥长度:', publicKeyHex.length / 2, '字节');
// 尝试验证签名
if (p7.sinfos && p7.sinfos.length > 0) {
const signer = p7.sinfos[0];
// 计算 SM3 哈希
const hash = sm3(mockOriginalData);
console.log('\nSM3 哈希:', hash);
// 尝试验证
console.log('\n尝试使用 sm-crypto 验证:');
try {
// sm-crypto 的 doVerify 需要 hash 和公钥
// 注意:sm-crypto 的 API 可能需要不同的格式
const result = sm2.doVerify(hash, signer.sighex, publicKeyHex);
console.log('验证结果:', result);
} catch (e) {
console.error('验证错误:', e.message);
// 尝试其他格式
console.log('\n尝试其他格式...');
try {
// 可能需要去掉 04 前缀
const pubKeyWithoutPrefix = publicKeyHex.substring(2);
const result = sm2.doVerify(hash, signer.sighex, pubKeyWithoutPrefix);
console.log('验证结果 (去掉前缀):', result);
} catch (e2) {
console.error('验证错误 (去掉前缀):', e2.message);
}
}
}
}
+113
View File
@@ -0,0 +1,113 @@
const rs = require('jsrsasign');
const sm2 = require('sm-crypto').sm2;
const sm3 = require('sm-crypto').sm3;
// 用户提供的 SM2 PKCS7 签名数据
const sm2P7Base64 = 'MIIDKgYKKoEcz1UGAQQCAqCCAxowggMWAgEBMQ4wDAYIKoEcz1UBgxEFADAMBgoqgRzPVQYBBAIBoIICNzCCAjMwggHWoAMCAQICBgD1tfRoKTAMBggqgRzPVQGDdQUAMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBMB4XDTI2MDQwMjA4MDEzOVoXDTM2MDMzMDA4MDEzOVowTTELMAkGA1UEBhMCQ04xDzANBgNVBAoMBlBCQ0NSQzEtMCsGA1UEAwwkOTRmMWY5MzgtZjBiZC00NzYyLTkyOGUtZjY4YTdjY2UxNDFkMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu3dU5Vn20VjVL9xGCCRPB/EpATY6HW24+zIZv46Cfv0R2PGM+ZJs/dYarw7qJCnpIbe5T0PKmyyJITNVrVnnMaOBrDCBqTAfBgNVHSMEGDAWgBRUqfVQHMpD+o+YdrPK1JlRJL757zAJBgNVHRMEAjAAME8GA1UdHwRIMEYwRKBCoECkPjA8MQ4wDAYDVQQDDAVjcmw3MTEMMAoGA1UECwwDY3JsMQ8wDQYDVQQKDAZQQkNDUkMxCzAJBgNVBAYTAkNOMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU5bG6U0lYzWtkPJE74M/3HdafKKcwDAYIKoEcz1UBg3UFAANJADBGAiEAuLTE36/Eobcui23bFiPvOU4MOT0O0r9SyyHZ1JuvLwMCIQCejuSk03jyUAkbh1SaPx5n7WlDjI6tx9v9RJ7iw5rlojGBtzCBtAIBATBKMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBAgYA9bX0aCkwDAYIKoEcz1UBgxEFADANBgkqgRzPVQGCLQEFAARGMEQCIALTgWMD7io1JHfzInBwzDQpe9bonchuLFSED//JLDtnAiBmEUtr+ssAQIzFG4wY0CAj9QZxhENyCU2UYL1fhaysUA==';
// 测试用的原始数据
const mockOriginalData = 'QDJNHHVB_FWQ';
console.log('=== 详细调试 SM2 PKCS7 验证 ===');
console.log('原始数据:', mockOriginalData);
console.log('原始数据长度:', mockOriginalData.length);
console.log('原始数据 UTF-8 编码:', Buffer.from(mockOriginalData, 'utf8').toString('hex'));
const parser = new rs.KJUR.asn1.cms.CMSParser();
const p7 = parser.getCMSSignedData(rs.b64tohex(sm2P7Base64));
console.log('\nPKCS7 信息:');
console.log('版本:', p7.version);
console.log('哈希算法:', p7.hashalgs);
console.log('签名者数量:', p7.sinfos ? p7.sinfos.length : 0);
if (p7.sinfos && p7.sinfos.length > 0) {
const signer = p7.sinfos[0];
console.log('\n签名者信息:');
console.log(' 签名算法:', signer.sigalg);
console.log(' 签名值 (hex):', signer.sighex);
console.log(' 签名值长度:', signer.sighex ? signer.sighex.length / 2 : 0, '字节');
// 解析签名值(ASN.1 DER 格式)
console.log('\n解析签名值:');
try {
const sigAsn1 = rs.ASN1HEX.getTLVbyList(signer.sighex, 0, []);
console.log(' 签名 ASN.1 结构:', sigAsn1);
// 提取 r 和 s 值
const r = rs.ASN1HEX.getTLVbyList(sigAsn1, 0, [0]);
const s = rs.ASN1HEX.getTLVbyList(sigAsn1, 0, [1]);
console.log(' r 值:', r.substring(4)); // 跳过 tag 和 length
console.log(' s 值:', s.substring(4));
} catch (e) {
console.error(' 解析签名值错误:', e.message);
}
}
// 提取公钥
if (p7.certs && p7.certs.array && p7.certs.array.length > 0) {
const certPem = p7.certs.array[0];
const certHex = rs.pemtohex(certPem);
const tbsCert = rs.ASN1HEX.getTLVbyList(certHex, 0, [0]);
const spki = rs.ASN1HEX.getTLVbyList(tbsCert, 0, [6]);
const publicKeyBitString = rs.ASN1HEX.getTLVbyList(spki, 0, [1]);
const publicKeyHex = publicKeyBitString.substring(6);
console.log('\n公钥信息:');
console.log(' 公钥 (hex):', publicKeyHex);
console.log(' 公钥长度:', publicKeyHex.length / 2, '字节');
console.log(' 公钥格式:', publicKeyHex.startsWith('04') ? '未压缩格式' : '压缩格式');
// 尝试验证签名
if (p7.sinfos && p7.sinfos.length > 0) {
const signer = p7.sinfos[0];
// 计算 SM3 哈希
console.log('\n计算 SM3 哈希:');
const hash = sm3(mockOriginalData);
console.log(' 原始数据哈希:', hash);
// 测试不同的公钥格式
console.log('\n测试不同的公钥格式:');
// 1. 使用完整公钥(带 04 前缀)
try {
const result1 = sm2.doVerifySignature(hash, signer.sighex, publicKeyHex);
console.log(' 验证结果(完整公钥):', result1);
} catch (e) {
console.error(' 验证错误(完整公钥):', e.message);
}
// 2. 使用不带 04 前缀的公钥
try {
const pubKeyWithoutPrefix = publicKeyHex.substring(2);
const result2 = sm2.doVerifySignature(hash, signer.sighex, pubKeyWithoutPrefix);
console.log(' 验证结果(不带前缀):', result2);
} catch (e) {
console.error(' 验证错误(不带前缀):', e.message);
}
// 3. 尝试使用签名值的不同格式
console.log('\n测试不同的签名值格式:');
try {
// 直接使用 sighex
const result3 = sm2.doVerifySignature(hash, signer.sighex, publicKeyHex);
console.log(' 验证结果(直接使用 sighex):', result3);
} catch (e) {
console.error(' 验证错误(直接使用 sighex):', e.message);
}
// 4. 测试不同的编码
console.log('\n测试不同的编码:');
try {
// 使用 Buffer 转换
const hashBuffer = Buffer.from(mockOriginalData, 'utf8');
const hash2 = sm3(hashBuffer);
console.log(' Buffer 哈希:', hash2);
const result4 = sm2.doVerifySignature(hash2, signer.sighex, publicKeyHex);
console.log(' 验证结果(Buffer 编码):', result4);
} catch (e) {
console.error(' 验证错误(Buffer 编码):', e.message);
}
}
}
+35
View File
@@ -0,0 +1,35 @@
const sm2 = require('sm-crypto').sm2;
const sm3 = require('sm-crypto').sm3;
console.log('=== 检查 sm-crypto 版本 ===');
console.log('sm-crypto 版本:', require('sm-crypto/package.json').version);
// 测试公钥格式
console.log('\n=== 测试公钥格式 ===');
// 生成一个有效的 SM2 密钥对
const keypair = sm2.generateKeyPairHex();
console.log('生成的公钥:', keypair.publicKey);
console.log('生成的公钥长度:', keypair.publicKey.length);
// 用户提供的公钥
const userPublicKey = 'bb7754e559f6d158d52fdc4608244f07f12901363a1d6db8fb3219bf8e827efd11d8f18cf9926cfdd61aaf0eea2429e921b7b94f43ca9b2c89213355ad59e731';
console.log('\n用户提供的公钥:', userPublicKey);
console.log('用户提供的公钥长度:', userPublicKey.length);
// 测试用户公钥
console.log('\n=== 测试用户公钥 ===');
try {
const data = 'test';
const sig = sm2.doSignature(data, keypair.privateKey);
// 尝试使用生成的公钥验证(应该成功)
const verify1 = sm2.doVerifySignature(data, sig, keypair.publicKey);
console.log('使用生成的公钥验证:', verify1);
// 尝试使用用户公钥验证(应该失败)
const verify2 = sm2.doVerifySignature(data, sig, userPublicKey);
console.log('使用用户公钥验证:', verify2);
} catch (e) {
console.error('错误:', e.message);
}
+69
View File
@@ -0,0 +1,69 @@
const sm2 = require('sm-crypto').sm2;
const sm3 = require('sm-crypto').sm3;
console.log('=== 验证用户提供的具体数据 ===');
// 用户提供的数据
const dataHex = '51444A4E484856425F465751';
const signatureHex = '02D3816303EE2A352477F3227070CC34297BD6E89DC86E2C54840FFFC92C3B6766114B6BFA CB00408CC51B8C18D02023F50671844372094D9460BD5F85ACAC50'.replace(/\s/g, '');
const userId = '1234567812345678';
const hashHex = '0F76D50E476E24507187885DF1BC197B48F368F3C03DDF0A5C0C067D2EEF63EE';
// 从用户的 PKCS7 签名中提取的公钥
const publicKeyHex = '04bb7754e559f6d158d52fdc4608244f07f12901363a1d6db8fb3219bf8e827efd11d8f18cf9926cfdd61aaf0eea2429e921b7b94f43ca9b2c89213355ad59e731';
// 转换数据
const data = Buffer.from(dataHex, 'hex').toString('utf8');
console.log('数据:', data);
console.log('数据长度:', data.length);
console.log('签名:', signatureHex);
console.log('签名长度:', signatureHex.length / 2, '字节');
console.log('公钥:', publicKeyHex);
// 测试不同的验证选项
console.log('\n测试不同的验证选项:');
// 尝试 1: 使用原始数据 + DER + SM3 + userId
console.log('\n1. 使用原始数据 + DER + SM3 + userId:');
try {
const result1 = sm2.doVerifySignature(data, signatureHex, publicKeyHex, { der: true, hash: true, userId });
console.log(' 验证结果:', result1);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 2: 使用哈希 + DER
console.log('\n2. 使用哈希 + DER:');
try {
const result2 = sm2.doVerifySignature(hashHex, signatureHex, publicKeyHex, { der: true });
console.log(' 验证结果:', result2);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 3: 使用原始数据 + DER
console.log('\n3. 使用原始数据 + DER:');
try {
const result3 = sm2.doVerifySignature(data, signatureHex, publicKeyHex, { der: true });
console.log(' 验证结果:', result3);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 4: 直接使用哈希
console.log('\n4. 直接使用哈希:');
try {
const result4 = sm2.doVerifySignature(hashHex, signatureHex, publicKeyHex);
console.log(' 验证结果:', result4);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 5: 直接使用原始数据
console.log('\n5. 直接使用原始数据:');
try {
const result5 = sm2.doVerifySignature(data, signatureHex, publicKeyHex);
console.log(' 验证结果:', result5);
} catch (e) {
console.error(' 错误:', e.message);
}
+80
View File
@@ -0,0 +1,80 @@
const sm2 = require('sm-crypto').sm2;
const sm3 = require('sm-crypto').sm3;
console.log('=== 验证用户提供的 SM2 数据 ===');
// 用户提供的数据
const dataHex = '51444A4E484856425F465751';
const signatureHex = '02D3816303EE2A352477F3227070CC34297BD6E89DC86E2C54840FFFC92C3B6766114B6BFA CB00408CC51B8C18D02023F50671844372094D9460BD5F85ACAC50'.replace(/\s/g, '');
const userId = '1234567812345678';
const hashHex = '0F76D50E476E24507187885DF1BC197B48F368F3C03DDF0A5C0C067D2EEF63EE';
// 从用户的 PKCS7 签名中提取的公钥
const publicKeyHex = '04bb7754e559f6d158d52fdc4608244f07f12901363a1d6db8fb3219bf8e827efd11d8f18cf9926cfdd61aaf0eea2429e921b7b94f43ca9b2c89213355ad59e731';
// 转换数据
const data = Buffer.from(dataHex, 'hex').toString('utf8');
console.log('数据:', data);
console.log('数据长度:', data.length);
console.log('签名长度:', signatureHex.length / 2, '字节');
// 去掉公钥的 04 前缀
const pubKeyWithoutPrefix = publicKeyHex.substring(2);
console.log('公钥(去掉前缀):', pubKeyWithoutPrefix);
// 测试不同的验证选项
console.log('\n测试不同的验证选项:');
// 尝试 1: 使用哈希 + userId
console.log('\n1. 使用哈希 + userId:');
try {
const result1 = sm2.doVerifySignature(hashHex, signatureHex, pubKeyWithoutPrefix, { hash: true, userId });
console.log(' 验证结果:', result1);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 2: 使用原始数据 + userId
console.log('\n2. 使用原始数据 + userId:');
try {
const result2 = sm2.doVerifySignature(data, signatureHex, pubKeyWithoutPrefix, { hash: true, userId });
console.log(' 验证结果:', result2);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 3: 使用原始数据 + userId + 公钥
console.log('\n3. 使用原始数据 + userId + 公钥:');
try {
const result3 = sm2.doVerifySignature(data, signatureHex, pubKeyWithoutPrefix, { hash: true, userId, publicKey: pubKeyWithoutPrefix });
console.log(' 验证结果:', result3);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 4: 直接使用原始数据
console.log('\n4. 直接使用原始数据:');
try {
const result4 = sm2.doVerifySignature(data, signatureHex, pubKeyWithoutPrefix);
console.log(' 验证结果:', result4);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 5: 使用 DER 格式
console.log('\n5. 使用 DER 格式:');
try {
const result5 = sm2.doVerifySignature(data, signatureHex, pubKeyWithoutPrefix, { der: true });
console.log(' 验证结果:', result5);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 6: 使用 DER + 哈希 + userId
console.log('\n6. 使用 DER + 哈希 + userId:');
try {
const result6 = sm2.doVerifySignature(data, signatureHex, pubKeyWithoutPrefix, { der: true, hash: true, userId });
console.log(' 验证结果:', result6);
} catch (e) {
console.error(' 错误:', e.message);
}
+91
View File
@@ -0,0 +1,91 @@
const sm2 = require('sm-crypto').sm2;
const sm3 = require('sm-crypto').sm3;
console.log('=== 验证用户提供的 SM2 数据 ===');
// 用户提供的数据
const dataHex = '51444A4E484856425F465751';
const signatureHex = '02D3816303EE2A352477F3227070CC34297BD6E89DC86E2C54840FFFC92C3B6766114B6BFA CB00408CC51B8C18D02023F50671844372094D9460BD5F85ACAC50'.replace(/\s/g, '');
const userId = '1234567812345678';
const hashHex = '0F76D50E476E24507187885DF1BC197B48F368F3C03DDF0A5C0C067D2EEF63EE';
// 从用户的 PKCS7 签名中提取的公钥(去掉 04 前缀)
const publicKeyHex = 'bb7754e559f6d158d52fdc4608244f07f12901363a1d6db8fb3219bf8e827efd11d8f18cf9926cfdd61aaf0eea2429e921b7b94f43ca9b2c89213355ad59e731';
// 转换数据
const data = Buffer.from(dataHex, 'hex').toString('utf8');
console.log('数据:', data);
console.log('数据长度:', data.length);
console.log('签名:', signatureHex);
console.log('签名长度:', signatureHex.length / 2, '字节');
console.log('公钥:', publicKeyHex);
console.log('userId:', userId);
// 计算哈希
const calculatedHash = sm3(data);
console.log('\n计算的哈希:', calculatedHash);
console.log('用户提供的哈希:', hashHex);
console.log('哈希是否匹配:', calculatedHash === hashHex);
// 测试不同的验证选项
console.log('\n测试不同的验证选项:');
// 尝试 1: 直接验证(原始 r|s 格式)
console.log('\n1. 直接验证(原始 r|s 格式):');
try {
const result1 = sm2.doVerifySignature(data, signatureHex, publicKeyHex);
console.log(' 验证结果:', result1);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 2: 带 SM3 哈希
console.log('\n2. 带 SM3 哈希:');
try {
const result2 = sm2.doVerifySignature(data, signatureHex, publicKeyHex, { hash: true });
console.log(' 验证结果:', result2);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 3: 带 SM3 哈希 + userId
console.log('\n3. 带 SM3 哈希 + userId:');
try {
const result3 = sm2.doVerifySignature(data, signatureHex, publicKeyHex, { hash: true, userId });
console.log(' 验证结果:', result3);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 4: 使用哈希值
console.log('\n4. 使用哈希值:');
try {
const result4 = sm2.doVerifySignature(hashHex, signatureHex, publicKeyHex, { hash: true });
console.log(' 验证结果:', result4);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 5: 使用哈希值 + userId
console.log('\n5. 使用哈希值 + userId:');
try {
const result5 = sm2.doVerifySignature(hashHex, signatureHex, publicKeyHex, { hash: true, userId });
console.log(' 验证结果:', result5);
} catch (e) {
console.error(' 错误:', e.message);
}
// 尝试 6: 转换为 DER 格式
console.log('\n6. 转换为 DER 格式:');
try {
// 构造 DER 格式: 30 44 02 20 [r] 02 20 [s]
const r = signatureHex.substring(0, 64);
const s = signatureHex.substring(64);
const derSignature = '30440220' + r + '0220' + s;
console.log(' DER 签名:', derSignature);
const result6 = sm2.doVerifySignature(data, derSignature, publicKeyHex, { der: true, hash: true, userId });
console.log(' 验证结果:', result6);
} catch (e) {
console.error(' 错误:', e.message);
}
+79
View File
@@ -0,0 +1,79 @@
const forge = require('node-forge');
const mockP7Sign = `-----BEGIN PKCS7-----
MIIDNwYJKoZIhvcNAQcCoIIDKDCCAyQCAQExDzANBglghkgBZQMEAgEFADA8Bgkq
hkiG9w0BBwGgLwQtVGhpcyBpcyB0aGUgdGVzdCBjb250ZW50IGZvciBQS0NTNyBz
aWduYXR1cmUuoIIB5jCCAeIwggFLoAMCAQICAQEwDQYJKoZIhvcNAQELBQAwNzEZ
MBcGA1UEAxMQdGVzdC5leGFtcGxlLmNvbTEaMBgGA1UEChMRVGVzdCBPcmdhbml6
YXRpb24wHhcNMjYwNDA0MDAxMDIyWhcNMjcwNDA0MDAxMDIyWjA3MRkwFwYDVQQD
ExB0ZXN0LmV4YW1wbGUuY29tMRowGAYDVQQKExFUZXN0IE9yZ2FuaXphdGlvbjCB
nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1e+05xb8T3Lo2v9MZQen5HYffpnc
yY9mnT/ygvfxZNM9TeYkq4fACiuS99FONnlHFQdlXEPJluIk1NBFUoLpehAbzOSY
Fu0WEjBOVJIg9FIWm7TUojU8yvPsfPGFYTEGEOGAse8AfHw4pl+w5SvxQUF8f2jB
XsRY0MrDyb07xC8CAwEAATANBgkqhkiG9w0BAQsFAAOBgQAOEttx/p3xosIm/8sM
kIaxR2c/fbYoMbBsX+geaiRu+xNOItbtQpGOhW6Ql981c3EwIjyd34KavplofZIr
Slj4jHQFySZaJkegHY+LCAQz9GEg2TMke3MsPJ7nSZS7MUqB2AQt1tLuiXUuRh3V
jF4yiqi6lGVmvPJf+y1GXk47KjGB5TCB4gIBATA8MDcxGTAXBgNVBAMTEHRlc3Qu
ZXhhbXBsZS5jb20xGjAYBgNVBAoTEVRlc3QgT3JnYW5pemF0aW9uAgEBMA0GCWCG
SAFlAwQCAQUAMA0GCSqGSIb3DQEBAQUABIGAMxSC81vzjSiZcJTcSicgCG9879mu
p/bikqdHZPyF8UgABjikXXoVJ9IClFRd5/3wRf8ez+hwnxKR8j1vAsDI7U6dXJtr
wp0aO7Ua6ecKhI5V0F8nUg5CjEK9FR8vNg0Il8U5AXDuan0iYKvguhWkiuJlvQlV
X9ncPP4VRERPw8U=
-----END PKCS7-----`;
const originalData = 'This is the test content for PKCS7 signature.';
try {
const p7 = forge.pkcs7.messageFromPem(mockP7Sign);
console.log('=== PKCS7 信息 ===');
console.log('type:', p7.type);
console.log('content:', p7.content ? p7.content.getBytes() : 'null');
console.log('certificates count:', p7.certificates ? p7.certificates.length : 0);
if (p7.certificates && p7.certificates.length > 0) {
const cert = p7.certificates[0];
console.log('cert subject:', cert.subject.attributes.map(a => `${a.name}=${a.value}`).join(', '));
}
console.log('\n=== 尝试验证 ===');
try {
// 尝试验证
const result = p7.verify();
console.log('verify() result:', result);
} catch (e) {
console.log('verify() error:', e.message);
}
// 手动验证
console.log('\n=== 手动验证 ===');
const md = forge.md.sha256.create();
md.update(originalData, 'utf8');
const digest = md.digest().getBytes();
console.log('calculated digest (hex):', forge.util.bytesToHex(digest));
// 获取签名
if (p7.rawCapture && p7.rawCapture.signature) {
console.log('signature length:', p7.rawCapture.signature.length);
console.log('signature (hex):', forge.util.bytesToHex(p7.rawCapture.signature).substring(0, 100) + '...');
}
// 获取公钥
if (p7.certificates && p7.certificates.length > 0) {
const publicKey = p7.certificates[0].publicKey;
if (publicKey) {
console.log('publicKey exists:', true);
// 尝试用公钥验证
try {
const verified = publicKey.verify(digest, p7.rawCapture.signature);
console.log('publicKey.verify() result:', verified);
} catch (e) {
console.log('publicKey.verify() error:', e.message);
}
}
}
} catch (error) {
console.error('Error:', error.message);
console.error(error.stack);
}
+21
View File
@@ -0,0 +1,21 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const { buildScript } = require("./restore-run");
const inspectPath = path.join(__dirname, "testcase-inspect.json");
const expectedPath = path.join(__dirname, "testcase-run.sh");
const inspectData = JSON.parse(fs.readFileSync(inspectPath, "utf8"));
const info = Array.isArray(inspectData) ? inspectData[0] : inspectData;
const actual = buildScript(info);
const expected = fs.readFileSync(expectedPath, "utf8").replace(/\r\n/g, "\n");
assert.strictEqual(actual, expected);
assert.ok(actual.includes("name=freshrss"));
assert.ok(actual.includes("img=freshrss/freshrss:latest"));
assert.ok(actual.includes('-v "$(pwd)/data:/var/www/FreshRSS/data"'));
assert.ok(actual.includes('-v "$(pwd)/extensions:/var/www/FreshRSS/extensions"'));
assert.strictEqual((actual.match(/-p 8082:80\/tcp/g) || []).length, 1);
console.log("restore-run testcase passed");
+272
View File
@@ -0,0 +1,272 @@
[
{
"Id": "71ba6f6111c86b9cc72fd144717357a40a3c472794bae99cc17b7fd887648338",
"Created": "2026-06-09T08:40:16.386743045Z",
"Path": "./Docker/entrypoint.sh",
"Args": [
"/bin/bash",
"-o",
"pipefail",
"-c",
"([ -z \"$CRON_MIN\" ] || cron) && \t. /etc/apache2/envvars && \texec apache2 -D FOREGROUND $([ -n \"$OIDC_ENABLED\" ] && [ \"$OIDC_ENABLED\" -ne 0 ] && echo \"-D OIDC_ENABLED\")"
],
"State": {
"Status": "running",
"Running": true,
"Paused": false,
"Restarting": false,
"OOMKilled": false,
"Dead": false,
"Pid": 2178,
"ExitCode": 0,
"Error": "",
"StartedAt": "2026-06-11T03:36:17.818299057Z",
"FinishedAt": "2026-06-11T03:36:11.0220238Z"
},
"Image": "sha256:fc6e4cf934a2d62c5da897c9fd011ef29dc3c83948ef3581d2a526fc90789622",
"ResolvConfPath": "/var/lib/docker/containers/71ba6f6111c86b9cc72fd144717357a40a3c472794bae99cc17b7fd887648338/resolv.conf",
"HostnamePath": "/var/lib/docker/containers/71ba6f6111c86b9cc72fd144717357a40a3c472794bae99cc17b7fd887648338/hostname",
"HostsPath": "/var/lib/docker/containers/71ba6f6111c86b9cc72fd144717357a40a3c472794bae99cc17b7fd887648338/hosts",
"LogPath": "/var/lib/docker/containers/71ba6f6111c86b9cc72fd144717357a40a3c472794bae99cc17b7fd887648338/71ba6f6111c86b9cc72fd144717357a40a3c472794bae99cc17b7fd887648338-json.log",
"Name": "/freshrss",
"RestartCount": 0,
"Driver": "overlay2",
"Platform": "linux",
"MountLabel": "",
"ProcessLabel": "",
"AppArmorProfile": "docker-default",
"ExecIDs": null,
"HostConfig": {
"Binds": [
"/app/freshrss/extensions:/var/www/FreshRSS/extensions",
"/app/freshrss/data:/var/www/FreshRSS/data"
],
"ContainerIDFile": "",
"LogConfig": {
"Type": "json-file",
"Config": {
"max-file": "3",
"max-size": "10m"
}
},
"NetworkMode": "bridge",
"PortBindings": {
"80/tcp": [
{
"HostIp": "",
"HostPort": "8082"
}
]
},
"RestartPolicy": {
"Name": "unless-stopped",
"MaximumRetryCount": 0
},
"AutoRemove": false,
"VolumeDriver": "",
"VolumesFrom": null,
"ConsoleSize": [
30,
118
],
"CapAdd": null,
"CapDrop": null,
"CgroupnsMode": "private",
"Dns": [],
"DnsOptions": [],
"DnsSearch": [],
"ExtraHosts": null,
"GroupAdd": null,
"IpcMode": "private",
"Cgroup": "",
"Links": null,
"OomScoreAdj": 0,
"PidMode": "",
"Privileged": false,
"PublishAllPorts": false,
"ReadonlyRootfs": false,
"SecurityOpt": null,
"UTSMode": "",
"UsernsMode": "",
"ShmSize": 67108864,
"Runtime": "runc",
"Isolation": "",
"CpuShares": 0,
"Memory": 0,
"NanoCpus": 0,
"CgroupParent": "",
"BlkioWeight": 0,
"BlkioWeightDevice": [],
"BlkioDeviceReadBps": [],
"BlkioDeviceWriteBps": [],
"BlkioDeviceReadIOps": [],
"BlkioDeviceWriteIOps": [],
"CpuPeriod": 0,
"CpuQuota": 0,
"CpuRealtimePeriod": 0,
"CpuRealtimeRuntime": 0,
"CpusetCpus": "",
"CpusetMems": "",
"Devices": [],
"DeviceCgroupRules": null,
"DeviceRequests": null,
"MemoryReservation": 0,
"MemorySwap": 0,
"MemorySwappiness": null,
"OomKillDisable": null,
"PidsLimit": null,
"Ulimits": [],
"CpuCount": 0,
"CpuPercent": 0,
"IOMaximumIOps": 0,
"IOMaximumBandwidth": 0,
"MaskedPaths": [
"/proc/asound",
"/proc/acpi",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/proc/scsi",
"/sys/firmware",
"/sys/devices/virtual/powercap"
],
"ReadonlyPaths": [
"/proc/bus",
"/proc/fs",
"/proc/irq",
"/proc/sys",
"/proc/sysrq-trigger"
]
},
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/d9852f484fa657d12db8ed58e4f61fa9d937a63b2c02326bc859aa03f3c6a6f9-init/diff:/var/lib/docker/overlay2/e16b5110bbde48cd9070a8bea52ca1b49a6c7a6ae17abc23bc2f2152aa851dbb/diff:/var/lib/docker/overlay2/6d104177f8014ee6b291525597ac93c48cce1d6e3f45f8390330dbee8f536d89/diff:/var/lib/docker/overlay2/640758ad6ad59ac4f5825afc463879a4da93264dcbe96cfd06653d494689b1c4/diff:/var/lib/docker/overlay2/51e3461fd7fed56b0595c7c83e8c0edd6a67ae72ef9a59d2a29d6bb808847204/diff:/var/lib/docker/overlay2/b7de361f0da3dea53c4bcc25a9bbde40efaaf7933147da3b95ed8e7d4d718ad4/diff:/var/lib/docker/overlay2/1ee479d5e4ed919312b4792bec9fd9a2425ee199b4b7ff911572f272b2ba2449/diff:/var/lib/docker/overlay2/4d45f37889ffe243f232f86582a4c3b8125b13b397cacfc0193a79af39158190/diff",
"MergedDir": "/var/lib/docker/overlay2/d9852f484fa657d12db8ed58e4f61fa9d937a63b2c02326bc859aa03f3c6a6f9/merged",
"UpperDir": "/var/lib/docker/overlay2/d9852f484fa657d12db8ed58e4f61fa9d937a63b2c02326bc859aa03f3c6a6f9/diff",
"WorkDir": "/var/lib/docker/overlay2/d9852f484fa657d12db8ed58e4f61fa9d937a63b2c02326bc859aa03f3c6a6f9/work"
},
"Name": "overlay2"
},
"Mounts": [
{
"Type": "bind",
"Source": "/app/freshrss/data",
"Destination": "/var/www/FreshRSS/data",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
},
{
"Type": "bind",
"Source": "/app/freshrss/extensions",
"Destination": "/var/www/FreshRSS/extensions",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
],
"Config": {
"Hostname": "71ba6f6111c8",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"ExposedPorts": {
"80/tcp": {}
},
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"TZ=Asia/Shanghai",
"CRON_MIN=*/30",
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"COPY_LOG_TO_SYSLOG=On",
"COPY_SYSLOG_TO_STDERR=On",
"DATA_PATH=",
"FRESHRSS_ENV=",
"LISTEN=",
"OIDC_ENABLED=",
"TRUSTED_PROXY="
],
"Cmd": [
"/bin/bash",
"-o",
"pipefail",
"-c",
"([ -z \"$CRON_MIN\" ] || cron) && \t. /etc/apache2/envvars && \texec apache2 -D FOREGROUND $([ -n \"$OIDC_ENABLED\" ] && [ \"$OIDC_ENABLED\" -ne 0 ] && echo \"-D OIDC_ENABLED\")"
],
"Image": "freshrss/freshrss:latest",
"Volumes": null,
"WorkingDir": "/var/www/FreshRSS",
"Entrypoint": [
"./Docker/entrypoint.sh"
],
"OnBuild": null,
"Labels": {
"org.opencontainers.image.created": "2026-05-20T17:58:29.110Z",
"org.opencontainers.image.description": "A free, self-hostable news aggregator…",
"org.opencontainers.image.documentation": "https://freshrss.github.io/FreshRSS/",
"org.opencontainers.image.licenses": "AGPL-3.0",
"org.opencontainers.image.revision": "b2c50115baa36c217e939ee3ea8ecfae52f91abd",
"org.opencontainers.image.source": "https://github.com/FreshRSS/FreshRSS",
"org.opencontainers.image.title": "FreshRSS",
"org.opencontainers.image.url": "https://freshrss.org/",
"org.opencontainers.image.vendor": "FreshRSS",
"org.opencontainers.image.version": "1.29.1"
}
},
"NetworkSettings": {
"Bridge": "",
"SandboxID": "78f7f8cccc0810fcdadb3d0e7bff0fe16156a46ab4b0feb7a4ebd6e80e5e330d",
"SandboxKey": "/var/run/docker/netns/78f7f8cccc08",
"Ports": {
"80/tcp": [
{
"HostIp": "0.0.0.0",
"HostPort": "8082"
},
{
"HostIp": "::",
"HostPort": "8082"
}
]
},
"HairpinMode": false,
"LinkLocalIPv6Address": "",
"LinkLocalIPv6PrefixLen": 0,
"SecondaryIPAddresses": null,
"SecondaryIPv6Addresses": null,
"EndpointID": "14c4ae2de08b7c68dfe8a757933088247f4c4ffd8c3d1236ff919da0422a655c",
"Gateway": "172.17.0.1",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"IPAddress": "172.17.0.3",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"MacAddress": "02:42:ac:11:00:03",
"Networks": {
"bridge": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"MacAddress": "02:42:ac:11:00:03",
"DriverOpts": null,
"NetworkID": "2e5dc7af2729566328476edc3fe4594e9f6b39adabbdcda5b7cc26fcbe073224",
"EndpointID": "14c4ae2de08b7c68dfe8a757933088247f4c4ffd8c3d1236ff919da0422a655c",
"Gateway": "172.17.0.1",
"IPAddress": "172.17.0.3",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"DNSNames": null
}
}
}
}
]
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
name=freshrss
img=freshrss/freshrss:latest
docker run -d \
--name "$name" \
--restart unless-stopped \
-w /var/www/FreshRSS \
-e TZ=Asia/Shanghai \
-e 'CRON_MIN=*/30' \
-e PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
-e COPY_LOG_TO_SYSLOG=On \
-e COPY_SYSLOG_TO_STDERR=On \
-e DATA_PATH= \
-e FRESHRSS_ENV= \
-e LISTEN= \
-e OIDC_ENABLED= \
-e TRUSTED_PROXY= \
-p 8082:80/tcp \
-v "$(pwd)/data:/var/www/FreshRSS/data" \
-v "$(pwd)/extensions:/var/www/FreshRSS/extensions" \
--entrypoint ./Docker/entrypoint.sh \
"$img" /bin/bash -o pipefail -c '([ -z "$CRON_MIN" ] || cron) && . /etc/apache2/envvars && exec apache2 -D FOREGROUND $([ -n "$OIDC_ENABLED" ] && [ "$OIDC_ENABLED" -ne 0 ] && echo "-D OIDC_ENABLED")'
+31
View File
@@ -0,0 +1,31 @@
const sm2 = require('sm-crypto').sm2;
const sm3 = require('sm-crypto').sm3;
const forge = require('node-forge');
console.log('=== 生成 SM2 测试数据 ===');
// 生成 SM2 密钥对
const keypair = sm2.generateKeyPairHex();
console.log('私钥:', keypair.privateKey);
console.log('公钥:', keypair.publicKey);
// 测试数据
const originalData = 'This is test data for SM2 signature.';
console.log('\n原始数据:', originalData);
// 计算 SM3 哈希
const hash = sm3(originalData);
console.log('SM3 哈希:', hash);
// SM2 签名
const sig = sm2.doSignature(hash, keypair.privateKey);
console.log('SM2 签名:', sig);
// 验证签名
const verifyResult = sm2.doVerify(hash, sig, keypair.publicKey);
console.log('签名验证结果:', verifyResult);
console.log('\n=== 注意 ===');
console.log('由于 node-forge 不支持 SM2 算法,无法生成标准的 SM2 PKCS7 签名。');
console.log('SM2 PKCS7 签名需要特殊的 OID 和格式,目前仅支持 RSA PKCS7。');
console.log('建议:测试2可以使用 RSA 的 PKCS7 数据,或者跳过 SM2 测试。');
+57
View File
@@ -0,0 +1,57 @@
const forge = require('node-forge');
console.log('=== 生成测试用的 PKCS7 签名 ===');
try {
// 1. 生成 RSA 密钥对
console.log('生成 RSA 密钥对...');
const keys = forge.pki.rsa.generateKeyPair(1024);
const privateKey = keys.privateKey;
const publicKey = keys.publicKey;
// 2. 创建自签名证书
console.log('创建自签名证书...');
const cert = forge.pki.createCertificate();
cert.publicKey = publicKey;
cert.serialNumber = '01';
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
const attrs = [
{ name: 'commonName', value: 'test.example.com' },
{ name: 'organizationName', value: 'Test Organization' }
];
cert.setSubject(attrs);
cert.setIssuer(attrs);
cert.sign(privateKey, forge.md.sha256.create());
// 3. 创建 PKCS7 签名
console.log('创建 PKCS7 签名...');
const p7 = forge.pkcs7.createSignedData();
p7.content = forge.util.createBuffer('This is the test content for PKCS7 signature.');
p7.addCertificate(cert);
p7.addSigner({
key: privateKey,
certificate: cert,
digestAlgorithm: forge.pki.oids.sha256
});
p7.sign();
// 4. 转换为 PEM 格式
const pem = forge.pkcs7.messageToPem(p7);
console.log('\n=== 生成的 PKCS7 签名 PEM 格式 ===');
console.log(pem);
console.log('\n=== 提取 base64 部分 ===');
const base64 = pem.replace(/-----BEGIN PKCS7-----\n?/, '')
.replace(/-----END PKCS7-----\n?/, '')
.replace(/\n/g, '');
console.log(base64);
console.log('\n=== 完成 ===');
} catch (error) {
console.error('生成失败:', error);
console.error(error.stack);
}
+4
View File
@@ -0,0 +1,4 @@
const net = require(__dirname + "/../src/net")
net.killport(6667);
+13
View File
@@ -0,0 +1,13 @@
require("../src/init")
const Remote = require("../src/remote")
async function main(){
const remote = new Remote();
await remote.init()
console.log(await remote.check())
}
main()
+260
View File
@@ -0,0 +1,260 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import fs from "fs";
import os from "os";
import path from "path";
const RUN_MANUAL = process.env.KIT_REMOTE_MANU_TEST === "1";
const manualDescribe = RUN_MANUAL ? describe : describe.skip;
const manualTest = RUN_MANUAL ? test : test.skip;
let Remote;
if (RUN_MANUAL) {
require("../src/init");
Remote = require("../src/remote");
}
async function createRemote() {
const remote = new Remote();
await remote.init();
return remote;
}
function writeFixtureProject(tempDir, version, content, options = {}) {
const projectId = options.id || "myapp";
const group = options.group === undefined ? "/modules" : options.group;
const projectDir = path.resolve(tempDir, `fixture-${projectId}-${version || "auto"}-${Math.random().toString(16).slice(2)}`);
fs.mkdirSync(projectDir, { recursive: true });
const filePath = path.resolve(projectDir, "myapp.txt");
fs.writeFileSync(filePath, content, "utf-8");
fs.writeFileSync(path.resolve(projectDir, "meta.json"), JSON.stringify({
id: projectId,
group,
version,
desc: "test app",
official_url: "https://example.test/myapp",
dist: [
{
path: filePath,
os: "any",
platform: "any",
filename: "myapp.txt",
type: "data"
}
]
}, null, 4), "utf-8");
return projectDir;
}
function normalizeRemotePath(value) {
if (!value || value === "/") {
return "/";
}
return path.posix.normalize("/" + String(value).replace(/\\/g, "/")).replace(/\/+$/g, "") || "/";
}
function remoteProjectPath(group, projectId) {
const normalizedGroup = group === undefined ? "/modules" : normalizeRemotePath(group);
return normalizeRemotePath(path.posix.join(normalizedGroup, projectId));
}
async function deleteRemotePath(remote, remotePath) {
try {
if (remote.client && remote.client.deleteFile && await remote.client.exists(remotePath)) {
await remote.client.deleteFile(remotePath);
}
} catch (e) {
console.warn(`cleanup remote failed ${remotePath}: ${e.message}`);
}
}
let tempDir;
let localStore;
let remote;
let runId;
let counter;
let projectIds;
let remoteRoots;
beforeEach(async () => {
if (!RUN_MANUAL) {
return;
}
tempDir = fs.mkdtempSync(path.resolve(os.tmpdir(), "kit-remote-manu-test-"));
localStore = $sConfig.getLocalStorePath();
remote = await createRemote();
runId = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
counter = 0;
projectIds = [];
remoteRoots = [];
});
afterEach(async () => {
if (!RUN_MANUAL) {
return;
}
for (const remoteRoot of remoteRoots.reverse()) {
await deleteRemotePath(remote, remoteRoot);
}
for (const projectId of projectIds) {
fs.rmSync(path.resolve(localStore, "modules", projectId), { recursive: true, force: true });
}
fs.rmSync(tempDir, { recursive: true, force: true });
});
function nextProjectId() {
const projectId = `myapp-${runId}-${++counter}`.toLowerCase();
projectIds.push(projectId);
return projectId;
}
function trackRemote(group, projectId) {
const root = remoteProjectPath(group, projectId);
remoteRoots.push(root);
return root;
}
manualDescribe("remote module public API manual webdav", () => {
manualTest("1-1 exposes documented public methods", async () => {
expect(typeof remote.check).toBe("function");
expect(typeof remote.publish).toBe("function");
expect(typeof remote.install).toBe("function");
expect(typeof remote.list).toBe("function");
expect(typeof remote.use).toBe("function");
expect(typeof remote.rollback).toBe("function");
expect(typeof remote.remove).toBe("function");
});
manualTest("1-2 check validates an available webdav client", async () => {
expect(await remote.check()).toBe(true);
});
manualTest("1-3 publish stores files under default group /modules/id", async () => {
const projectId = nextProjectId();
trackRemote("/modules", projectId);
const projectDir = writeFixtureProject(tempDir, "1.0", "hello v1", { id: projectId });
await remote.publish(projectDir);
const metadata = JSON.parse(await remote.client.getFileContents(`/modules/${projectId}/metadata.json`, { format: "text" }));
const version = JSON.parse(await remote.client.getFileContents(`/modules/${projectId}/1.0/version.json`, { format: "text" }));
const artifact = await remote.client.getFileContents(`/modules/${projectId}/1.0/any/any/myapp.txt`, { format: "text" });
expect(metadata.project_id).toBe(projectId);
expect(metadata.group).toBe("/modules");
expect(metadata.version).toBe("1.0");
expect(metadata.versions).toBeUndefined();
expect(metadata.latest_version).toBeUndefined();
expect(version.artifacts[0].path).toBe("any/any/myapp.txt");
expect(artifact).toBe("hello v1");
}, 30000);
manualTest("1-4 publish supports custom nested group", async () => {
const projectId = nextProjectId();
const group = `/kit-manual-test/${runId}/docker/net`;
trackRemote(group, projectId);
await remote.publish(writeFixtureProject(tempDir, "2.0", "docker net", { id: projectId, group }));
const metadata = JSON.parse(await remote.client.getFileContents(`${group}/${projectId}/metadata.json`, { format: "text" }));
const artifact = await remote.client.getFileContents(`${group}/${projectId}/2.0/any/any/myapp.txt`, { format: "text" });
expect(metadata.group).toBe(group);
expect(metadata.version).toBe("2.0");
expect(artifact).toBe("docker net");
}, 30000);
manualTest("1-5 empty group stores project at repository root", async () => {
const projectId = nextProjectId();
trackRemote("", projectId);
await remote.publish(writeFixtureProject(tempDir, "1.0", "root group", { id: projectId, group: "" }));
const metadata = JSON.parse(await remote.client.getFileContents(`/${projectId}/metadata.json`, { format: "text" }));
expect(metadata.group).toBe("/");
expect(await remote.client.getFileContents(`/${projectId}/1.0/any/any/myapp.txt`, { format: "text" })).toBe("root group");
}, 30000);
manualTest("1-6 explicit version fails when remote version already exists", async () => {
const projectId = nextProjectId();
trackRemote("/modules", projectId);
await remote.publish(writeFixtureProject(tempDir, "1.0", "hello v1", { id: projectId }));
await expect(remote.publish(writeFixtureProject(tempDir, "1.0", "hello again", { id: projectId }))).rejects.toThrow("远程版本已存在");
}, 30000);
manualTest("1-7 blank version increments remote version tail or starts at 1.0", async () => {
const projectId = nextProjectId();
trackRemote("/modules", projectId);
await remote.publish(writeFixtureProject(tempDir, "", "first auto", { id: projectId }));
let metadata = JSON.parse(await remote.client.getFileContents(`/modules/${projectId}/metadata.json`, { format: "text" }));
expect(metadata.version).toBe("1.0");
await remote.publish(writeFixtureProject(tempDir, "", "second auto", { id: projectId }));
metadata = JSON.parse(await remote.client.getFileContents(`/modules/${projectId}/metadata.json`, { format: "text" }));
expect(metadata.version).toBe("1.1");
expect(await remote.client.getFileContents(`/modules/${projectId}/1.1/any/any/myapp.txt`, { format: "text" })).toBe("second auto");
}, 30000);
manualTest("1-8 install downloads latest version from default group", async () => {
const projectId = nextProjectId();
trackRemote("/modules", projectId);
await remote.publish(writeFixtureProject(tempDir, "1.0", "hello v1", { id: projectId }));
const installed = await remote.install(projectId);
const moduleRoot = path.resolve(localStore, "modules", projectId);
const installJson = JSON.parse(fs.readFileSync(path.resolve(moduleRoot, ".meta", "install.json"), "utf-8"));
const installedFile = fs.readFileSync(path.resolve(moduleRoot, "versions", "1.0", "any", "any", "myapp.txt"), "utf-8");
expect(installed).toBe("1.0");
expect(installedFile).toBe("hello v1");
expect(installJson.current_version).toBe("1.0");
expect(installJson.installed_versions).toContain("1.0");
expect(fs.existsSync(path.resolve(moduleRoot, "current"))).toBe(true);
}, 30000);
manualTest("1-9 install can target a custom group path", async () => {
const projectId = nextProjectId();
const group = `/kit-manual-test/${runId}/docker/net`;
trackRemote(group, projectId);
await remote.publish(writeFixtureProject(tempDir, "1.0", "docker install", { id: projectId, group }));
const installed = await remote.install(`${group}/${projectId}`);
const installedFile = fs.readFileSync(path.resolve(localStore, "modules", projectId, "versions", "1.0", "any", "any", "myapp.txt"), "utf-8");
expect(installed).toBe("1.0");
expect(installedFile).toBe("docker install");
}, 30000);
manualTest("1-10 list use rollback and remove manage installed versions", async () => {
const projectId = nextProjectId();
trackRemote("/modules", projectId);
await remote.publish(writeFixtureProject(tempDir, "1.0", "hello v1", { id: projectId }));
await remote.install(projectId);
await remote.publish(writeFixtureProject(tempDir, "2.0", "hello v2", { id: projectId }));
await remote.install(projectId);
expect(await remote.list(projectId)).toEqual(["1.0", "2.0"]);
await remote.rollback(projectId);
let installJson = JSON.parse(fs.readFileSync(path.resolve(localStore, "modules", projectId, ".meta", "install.json"), "utf-8"));
expect(installJson.current_version).toBe("1.0");
await remote.use(`${projectId}@2.0`);
installJson = JSON.parse(fs.readFileSync(path.resolve(localStore, "modules", projectId, ".meta", "install.json"), "utf-8"));
expect(installJson.current_version).toBe("2.0");
await remote.remove(`${projectId}@1.0`);
installJson = JSON.parse(fs.readFileSync(path.resolve(localStore, "modules", projectId, ".meta", "install.json"), "utf-8"));
expect(installJson.installed_versions).toEqual(["2.0"]);
expect(fs.existsSync(path.resolve(localStore, "modules", projectId, "versions", "1.0"))).toBe(false);
}, 30000);
});
if (!RUN_MANUAL) {
describe("remote manual webdav tests", () => {
test.skip("set KIT_REMOTE_MANU_TEST=1 and run bun test test/remote_manu_test.js", () => {});
});
}
+278
View File
@@ -0,0 +1,278 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import fs from "fs";
import os from "os";
import path from "path";
import { Readable, Writable } from "stream";
import Remote from "../src/remote";
class MemoryWebdavClient {
constructor() {
this.files = new Map();
this.dirs = new Set(["/"]);
}
normalize(remotePath) {
const normalized = path.posix.normalize("/" + String(remotePath || "").replace(/\\/g, "/"));
return normalized === "/" ? "/" : normalized.replace(/\/$/, "");
}
async exists(remotePath) {
const key = this.normalize(remotePath);
return this.dirs.has(key) || this.files.has(key);
}
async createDirectory(remotePath) {
const key = this.normalize(remotePath);
const parts = key.split("/").filter(Boolean);
let current = "";
for (const part of parts) {
current += "/" + part;
this.dirs.add(current);
}
}
async getDirectoryContents(remotePath) {
const dir = this.normalize(remotePath);
const prefix = dir === "/" ? "/" : dir + "/";
const names = new Map();
for (const item of [...this.dirs, ...this.files.keys()]) {
if (item === dir || !item.startsWith(prefix)) {
continue;
}
const rest = item.slice(prefix.length);
const name = rest.split("/")[0];
names.set(name, prefix + name);
}
return [...names].map(([basename, filename]) => ({ basename, filename, type: this.dirs.has(this.normalize(filename)) ? "directory" : "file" }));
}
async putFileContents(remotePath, content) {
const key = this.normalize(remotePath);
await this.createDirectory(path.posix.dirname(key));
this.files.set(key, Buffer.isBuffer(content) ? content : Buffer.from(String(content)));
}
async getFileContents(remotePath, options = {}) {
const key = this.normalize(remotePath);
if (!this.files.has(key)) {
throw new Error(`missing ${key}`);
}
const data = this.files.get(key);
return options.format === "text" ? data.toString("utf-8") : data;
}
createWriteStream(remotePath) {
const key = this.normalize(remotePath);
const chunks = [];
const stream = new Writable({
write(chunk, encoding, callback) {
chunks.push(Buffer.from(chunk));
callback();
}
});
stream.on("finish", () => {
this.createDirectory(path.posix.dirname(key));
this.files.set(key, Buffer.concat(chunks));
});
return stream;
}
createReadStream(remotePath) {
const key = this.normalize(remotePath);
if (!this.files.has(key)) {
throw new Error(`missing ${key}`);
}
return Readable.from(this.files.get(key));
}
async stat(remotePath) {
const key = this.normalize(remotePath);
const data = this.files.get(key);
if (!data) {
throw new Error(`missing ${key}`);
}
return { size: data.length };
}
}
function writeFixtureProject(tempDir, version, content, group = "/modules") {
const projectDir = path.resolve(tempDir, `fixture-${version || "auto"}-${Math.random().toString(16).slice(2)}`);
fs.mkdirSync(projectDir, { recursive: true });
const filePath = path.resolve(projectDir, "myapp.txt");
fs.writeFileSync(filePath, content, "utf-8");
fs.writeFileSync(path.resolve(projectDir, "meta.json"), JSON.stringify({
id: "myapp",
group,
version,
desc: "test app",
official_url: "https://example.test/myapp",
dist: [
{
path: filePath,
os: "any",
platform: "any",
filename: "myapp.txt",
type: "data"
}
]
}, null, 4), "utf-8");
return projectDir;
}
function createRemote() {
const remote = new Remote();
remote.client = new MemoryWebdavClient();
return remote;
}
let tempDir;
let localStore;
let oldLogger;
let oldConfig;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.resolve(os.tmpdir(), "kit-remote-test-"));
localStore = path.resolve(tempDir, "local-store");
oldLogger = globalThis.$logger;
oldConfig = globalThis.$sConfig;
globalThis.$logger = { info() {}, warn() {}, error() {}, debug() {} };
globalThis.$sConfig = { getLocalStorePath: () => localStore };
});
afterEach(() => {
globalThis.$logger = oldLogger;
globalThis.$sConfig = oldConfig;
fs.rmSync(tempDir, { recursive: true, force: true });
});
describe("remote module public API", () => {
test("exposes documented public methods", () => {
const remote = createRemote();
expect(typeof remote.check).toBe("function");
expect(typeof remote.publish).toBe("function");
expect(typeof remote.install).toBe("function");
expect(typeof remote.list).toBe("function");
expect(typeof remote.use).toBe("function");
expect(typeof remote.rollback).toBe("function");
expect(typeof remote.remove).toBe("function");
});
test("check validates an available webdav client", async () => {
const remote = createRemote();
expect(await remote.check()).toBe(true);
});
test("publish stores files under default group /modules/id", async () => {
const remote = createRemote();
const projectDir = writeFixtureProject(tempDir, "1.0", "hello v1");
await remote.publish(projectDir);
const metadata = JSON.parse(await remote.client.getFileContents("/modules/myapp/metadata.json", { format: "text" }));
const version = JSON.parse(await remote.client.getFileContents("/modules/myapp/1.0/version.json", { format: "text" }));
const artifact = await remote.client.getFileContents("/modules/myapp/1.0/any/any/myapp.txt", { format: "text" });
expect(metadata.project_id).toBe("myapp");
expect(metadata.group).toBe("/modules");
expect(metadata.version).toBe("1.0");
expect(metadata.versions).toBeUndefined();
expect(metadata.latest_version).toBeUndefined();
expect(version.artifacts[0].path).toBe("any/any/myapp.txt");
expect(artifact).toBe("hello v1");
});
test("publish supports custom nested group", async () => {
const remote = createRemote();
await remote.publish(writeFixtureProject(tempDir, "2.0", "docker net", "/docker/net"));
const metadata = JSON.parse(await remote.client.getFileContents("/docker/net/myapp/metadata.json", { format: "text" }));
const artifact = await remote.client.getFileContents("/docker/net/myapp/2.0/any/any/myapp.txt", { format: "text" });
expect(metadata.group).toBe("/docker/net");
expect(metadata.version).toBe("2.0");
expect(artifact).toBe("docker net");
});
test("empty group stores project at repository root", async () => {
const remote = createRemote();
await remote.publish(writeFixtureProject(tempDir, "1.0", "root group", ""));
const metadata = JSON.parse(await remote.client.getFileContents("/myapp/metadata.json", { format: "text" }));
expect(metadata.group).toBe("/");
expect(await remote.client.getFileContents("/myapp/1.0/any/any/myapp.txt", { format: "text" })).toBe("root group");
});
test("explicit version fails when remote version already exists", async () => {
const remote = createRemote();
await remote.publish(writeFixtureProject(tempDir, "1.0", "hello v1"));
await expect(remote.publish(writeFixtureProject(tempDir, "1.0", "hello again"))).rejects.toThrow("远程版本已存在");
});
test("blank version increments remote version tail or starts at 1.0", async () => {
const remote = createRemote();
await remote.publish(writeFixtureProject(tempDir, "", "first auto"));
let metadata = JSON.parse(await remote.client.getFileContents("/modules/myapp/metadata.json", { format: "text" }));
expect(metadata.version).toBe("1.0");
await remote.publish(writeFixtureProject(tempDir, "", "second auto"));
metadata = JSON.parse(await remote.client.getFileContents("/modules/myapp/metadata.json", { format: "text" }));
expect(metadata.version).toBe("1.1");
expect(await remote.client.getFileContents("/modules/myapp/1.1/any/any/myapp.txt", { format: "text" })).toBe("second auto");
});
test("install downloads latest version from default group", async () => {
const remote = createRemote();
await remote.publish(writeFixtureProject(tempDir, "1.0", "hello v1"));
const installed = await remote.install("myapp");
const moduleRoot = path.resolve(localStore, "modules", "myapp");
const installJson = JSON.parse(fs.readFileSync(path.resolve(moduleRoot, ".meta", "install.json"), "utf-8"));
const installedFile = fs.readFileSync(path.resolve(moduleRoot, "versions", "1.0", "any", "any", "myapp.txt"), "utf-8");
expect(installed).toBe("1.0");
expect(installedFile).toBe("hello v1");
expect(installJson.current_version).toBe("1.0");
expect(installJson.installed_versions).toContain("1.0");
expect(fs.existsSync(path.resolve(moduleRoot, "current"))).toBe(true);
});
test("install can target a custom group path", async () => {
const remote = createRemote();
await remote.publish(writeFixtureProject(tempDir, "1.0", "docker install", "/docker/net"));
const installed = await remote.install("/docker/net/myapp");
const installedFile = fs.readFileSync(path.resolve(localStore, "modules", "myapp", "versions", "1.0", "any", "any", "myapp.txt"), "utf-8");
expect(installed).toBe("1.0");
expect(installedFile).toBe("docker install");
});
test("list use rollback and remove manage installed versions", async () => {
const remote = createRemote();
await remote.publish(writeFixtureProject(tempDir, "1.0", "hello v1"));
await remote.install("myapp");
await remote.publish(writeFixtureProject(tempDir, "2.0", "hello v2"));
await remote.install("myapp");
expect(await remote.list("myapp")).toEqual(["1.0", "2.0"]);
await remote.rollback("myapp");
let installJson = JSON.parse(fs.readFileSync(path.resolve(localStore, "modules", "myapp", ".meta", "install.json"), "utf-8"));
expect(installJson.current_version).toBe("1.0");
await remote.use("myapp@2.0");
installJson = JSON.parse(fs.readFileSync(path.resolve(localStore, "modules", "myapp", ".meta", "install.json"), "utf-8"));
expect(installJson.current_version).toBe("2.0");
await remote.remove("myapp@1.0");
installJson = JSON.parse(fs.readFileSync(path.resolve(localStore, "modules", "myapp", ".meta", "install.json"), "utf-8"));
expect(installJson.installed_versions).toEqual(["2.0"]);
expect(fs.existsSync(path.resolve(localStore, "modules", "myapp", "versions", "1.0"))).toBe(false);
});
});
+42
View File
@@ -0,0 +1,42 @@
const rs = require('jsrsasign');
// 用户提供的 SM2 PKCS7 签名数据(没有头尾)
const sm2P7Base64 = 'MIIDKgYKKoEcz1UGAQQCAqCCAxowggMWAgEBMQ4wDAYIKoEcz1UBgxEFADAMBgoqgRzPVQYBBAIBoIICNzCCAjMwggHWoAMCAQICBgD1tfRoKTAMBggqgRzPVQGDdQUAMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBMB4XDTI2MDQwMjA4MDEzOVoXDTM2MDMzMDA4MDEzOVowTTELMAkGA1UEBhMCQ04xDzANBgNVBAoMBlBCQ0NSQzEtMCsGA1UEAwwkOTRmMWY5MzgtZjBiZC00NzYyLTkyOGUtZjY4YTdjY2UxNDFkMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu3dU5Vn20VjVL9xGCCRPB/EpATY6HW24+zIZv46Cfv0R2PGM+ZJs/dYarw7qJCnpIbe5T0PKmyyJITNVrVnnMaOBrDCBqTAfBgNVHSMEGDAWgBRUqfVQHMpD+o+YdrPK1JlRJL757zAJBgNVHRMEAjAAME8GA1UdHwRIMEYwRKBCoECkPjA8MQ4wDAYDVQQDDAVjcmw3MTEMMAoGA1UECwwDY3JsMQ8wDQYDVQQKDAZQQkNDUkMxCzAJBgNVBAYTAkNOMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU5bG6U0lYzWtkPJE74M/3HdafKKcwDAYIKoEcz1UBg3UFAANJADBGAiEAuLTE36/Eobcui23bFiPvOU4MOT0O0r9SyyHZ1JuvLwMCIQCejuSk03jyUAkbh1SaPx5n7WlDjI6tx9v9RJ7iw5rlojGBtzCBtAIBATBKMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBAgYA9bX0aCkwDAYIKoEcz1UBgxEFADANBgkqgRzPVQGCLQEFAARGMEQCIALTgWMD7io1JHfzInBwzDQpe9bonchuLFSED//JLDtnAiBmEUtr+ssAQIzFG4wY0CAj9QZxhENyCU2UYL1fhaysUA==';
// 转换为 hex
const sm2P7Hex = rs.hextorstr(rs.b64tohex(sm2P7Base64));
console.log('=== 使用 jsrsasign 测试 SM2 PKCS7 ===');
console.log('数据长度:', sm2P7Base64.length);
try {
// 使用 CMSParser 解析
const parser = new rs.KJUR.asn1.cms.CMSParser();
const result = parser.getCMSSignedData(rs.b64tohex(sm2P7Base64));
console.log('\n解析成功!');
console.log('版本:', result.version);
console.log('哈希算法:', result.hashalgs);
console.log('内容类型:', result.econtent?.type);
console.log('证书数量:', result.certs?.length);
console.log('签名者数量:', result.sinfos?.length);
if (result.sinfos && result.sinfos.length > 0) {
const signer = result.sinfos[0];
console.log('\n签名者信息:');
console.log(' 版本:', signer.version);
console.log(' 哈希算法:', signer.hashalg);
console.log(' 签名算法:', signer.sigalg);
console.log(' 签名值 (hex):', signer.sighex?.substring(0, 50) + '...');
}
if (result.certs && result.certs.length > 0) {
console.log('\n证书信息:');
const cert = result.certs[0];
console.log(' 证书 PEM:', cert.substring(0, 100) + '...');
}
} catch (error) {
console.error('\n解析失败:', error.message);
console.error(error.stack);
}
+26
View File
@@ -0,0 +1,26 @@
const forge = require('node-forge');
// 用户提供的 SM2 PKCS7 签名数据(没有头尾)
const sm2P7Base64 = 'MIIDKgYKKoEcz1UGAQQCAqCCAxowggMWAgEBMQ4wDAYIKoEcz1UBgxEFADAMBgoqgRzPVQYBBAIBoIICNzCCAjMwggHWoAMCAQICBgD1tfRoKTAMBggqgRzPVQGDdQUAMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBMB4XDTI2MDQwMjA4MDEzOVoXDTM2MDMzMDA4MDEzOVowTTELMAkGA1UEBhMCQ04xDzANBgNVBAoMBlBCQ0NSQzEtMCsGA1UEAwwkOTRmMWY5MzgtZjBiZC00NzYyLTkyOGUtZjY4YTdjY2UxNDFkMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu3dU5Vn20VjVL9xGCCRPB/EpATY6HW24+zIZv46Cfv0R2PGM+ZJs/dYarw7qJCnpIbe5T0PKmyyJITNVrVnnMaOBrDCBqTAfBgNVHSMEGDAWgBRUqfVQHMpD+o+YdrPK1JlRJL757zAJBgNVHRMEAjAAME8GA1UdHwRIMEYwRKBCoECkPjA8MQ4wDAYDVQQDDAVjcmw3MTEMMAoGA1UECwwDY3JsMQ8wDQYDVQQKDAZQQkNDUkMxCzAJBgNVBAYTAkNOMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU5bG6U0lYzWtkPJE74M/3HdafKKcwDAYIKoEcz1UBg3UFAANJADBGAiEAuLTE36/Eobcui23bFiPvOU4MOT0O0r9SyyHZ1JuvLwMCIQCejuSk03jyUAkbh1SaPx5n7WlDjI6tx9v9RJ7iw5rlojGBtzCBtAIBATBKMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBAgYA9bX0aCkwDAYIKoEcz1UBgxEFADANBgkqgRzPVQGCLQEFAARGMEQCIALTgWMD7io1JHfzInBwzDQpe9bonchuLFSED//JLDtnAiBmEUtr+ssAQIzFG4wY0CAj9QZxhENyCU2UYL1fhaysUA==';
// 包装成 PEM 格式
const pemData = `-----BEGIN PKCS7-----\n${sm2P7Base64}\n-----END PKCS7-----`;
console.log('=== 测试 SM2 PKCS7 数据 ===');
console.log('数据长度:', sm2P7Base64.length);
console.log('PEM 格式前200字符:', pemData.substring(0, 200));
try {
const p7 = forge.pkcs7.messageFromPem(pemData);
console.log('\n解析成功!');
console.log('类型:', p7.type);
console.log('证书数量:', p7.certificates ? p7.certificates.length : 0);
if (p7.certificates && p7.certificates.length > 0) {
const cert = p7.certificates[0];
console.log('证书主题:', cert.subject ? cert.subject.attributes.map(a => `${a.name}=${a.value}`).join(', ') : 'N/A');
console.log('证书颁发者:', cert.issuer ? cert.issuer.attributes.map(a => `${a.name}=${a.value}`).join(', ') : 'N/A');
}
} catch (error) {
console.error('\n解析失败:', error.message);
}
+32
View File
@@ -0,0 +1,32 @@
const crypto = require('../src/crypto.js');
// 用户提供的 SM2 PKCS7 签名数据
const sm2P7Base64 = 'MIIDKgYKKoEcz1UGAQQCAqCCAxowggMWAgEBMQ4wDAYIKoEcz1UBgxEFADAMBgoqgRzPVQYBBAIBoIICNzCCAjMwggHWoAMCAQICBgD1tfRoKTAMBggqgRzPVQGDdQUAMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBMB4XDTI2MDQwMjA4MDEzOVoXDTM2MDMzMDA4MDEzOVowTTELMAkGA1UEBhMCQ04xDzANBgNVBAoMBlBCQ0NSQzEtMCsGA1UEAwwkOTRmMWY5MzgtZjBiZC00NzYyLTkyOGUtZjY4YTdjY2UxNDFkMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEu3dU5Vn20VjVL9xGCCRPB/EpATY6HW24+zIZv46Cfv0R2PGM+ZJs/dYarw7qJCnpIbe5T0PKmyyJITNVrVnnMaOBrDCBqTAfBgNVHSMEGDAWgBRUqfVQHMpD+o+YdrPK1JlRJL757zAJBgNVHRMEAjAAME8GA1UdHwRIMEYwRKBCoECkPjA8MQ4wDAYDVQQDDAVjcmw3MTEMMAoGA1UECwwDY3JsMQ8wDQYDVQQKDAZQQkNDUkMxCzAJBgNVBAYTAkNOMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU5bG6U0lYzWtkPJE74M/3HdafKKcwDAYIKoEcz1UBg3UFAANJADBGAiEAuLTE36/Eobcui23bFiPvOU4MOT0O0r9SyyHZ1JuvLwMCIQCejuSk03jyUAkbh1SaPx5n7WlDjI6tx9v9RJ7iw5rlojGBtzCBtAIBATBKMEAxCzAJBgNVBAYTAkNOMQ8wDQYDVQQKDAZQQkNDUkMxIDAeBgNVBAMMF1BCQ0NSQyBTTTIgT3BlcmF0aW9uIENBAgYA9bX0aCkwDAYIKoEcz1UBgxEFADANBgkqgRzPVQGCLQEFAARGMEQCIALTgWMD7io1JHfzInBwzDQpe9bonchuLFSED//JLDtnAiBmEUtr+ssAQIzFG4wY0CAj9QZxhENyCU2UYL1fhaysUA==';
const rs = require('jsrsasign');
console.log('=== 使用真实原始数据验证 SM2 PKCS7 ===');
// 从 PKCS7 中提取原始数据
const parser = new rs.KJUR.asn1.cms.CMSParser();
const p7 = parser.getCMSSignedData(rs.b64tohex(sm2P7Base64));
let realOriginalData = null;
if (p7.econtent && p7.econtent.hex) {
realOriginalData = Buffer.from(p7.econtent.hex, 'hex').toString();
console.log('从 PKCS7 提取的原始数据:', realOriginalData);
}
// 使用正确的原始数据进行验证
if (realOriginalData) {
console.log('\n使用提取的原始数据进行验证:');
const result = crypto.verifyPKCS7(sm2P7Base64, realOriginalData);
console.log('验证结果:', result);
} else {
console.log('无法从 PKCS7 中提取原始数据');
}
// 也测试一下不提供原始数据的情况(attach 模式)
console.log('\n使用 attach 模式(不提供原始数据):');
const result2 = crypto.verifyPKCS7(sm2P7Base64);
console.log('验证结果:', result2);
+147
View File
@@ -0,0 +1,147 @@
const fs = require("fs");
const { Transform } = require('stream');
const { pipeline } = require('node:stream/promises');
const paths = require('path');
function convertBytes(bytes) {
if (bytes >= 1024 * 1024 * 1024) {
const gigabytes = bytes / (1024 * 1024 * 1024);
return `${gigabytes.toFixed(2)}GB`;
} else if (bytes >= 1024 * 1024) {
const megabytes = bytes / (1024 * 1024);
return `${megabytes.toFixed(2)}MB`;
} else if (bytes >= 1024 ) {
const megabytes = bytes / (1024 );
return `${megabytes.toFixed(2)}KB`;
} else {
return `${bytes} B`;
}
}
// 创建一个用于统计字节数的Transform流
class ByteCounter extends Transform {
constructor(totalSize) {
super();
this.bytesCount = 0;
this.totalSize = totalSize;
this.timestamp = (new Date()).getTime()
this.recordBytesCount = 0;
this.speed = 0;
this.intervalId = setInterval(()=>{
let now = (new Date()).getTime()
let timecost = now - this.timestamp;
this.speed = ((this.bytesCount - this.recordBytesCount ) * 1000 / timecost).toFixed(2)
this.timestamp = now;
this.recordBytesCount = this.bytesCount;
}, 2000);
}
_transform(chunk, encoding, callback) {
this.bytesCount += chunk.length;
let process = (this.bytesCount * 100 / this.totalSize).toFixed(2)
console.log(`进度 ${process}%\t速度${ convertBytes(this.speed) }/s\t${convertBytes(this.bytesCount)} / ${convertBytes(this.totalSize)}`);
callback(null, chunk);
}
_flush(callback) {
if (this.intervalId) {
clearInterval(this.intervalId);
}
callback();
}
}
async function wUpload(client , from, to){
const totalSize = fs.statSync(from).size;
const rs = fs.createReadStream( from );
let wSize = 0;
let rSize = 0;
rs.on("data", (r)=>{
rSize += r.length;
console.log("r", rSize * 100 / totalSize)
})
const ws = client.createWriteStream(to)
ws.on("data", (r)=>{
wSize += r.length;
console.log("w", wSize * 100 / totalSize)
})
ws.on("finish", ()=>{
console.log("finish")
})
return pipeline( rs, ws );
}
/**
* 文件不存在会抛出异常
* @param client
* @param file
* @param to
* @returns {Promise<void>}
*/
async function wDownload(client, file, to){
// 确认文件存在以及大小
let stat = await client.stat(file);
let totalSize = stat.size;
if ( ! to ) {
to = paths.basename(file);
}
const byteCounter = new ByteCounter(totalSize);
await pipeline(
client.createReadStream(file),
byteCounter,
fs.createWriteStream(to)
);
await pipeline(rs , ws );
}
async function main(){
const webdav = await import("webdav");
// const client = webdav.createClient(
// "http://baishe.fullstack.club:5244/dav",
// {
// username: "cheney",
// password: "722V587"
// }
// );
const client = webdav.createClient(
"http://openlist.honor3.com/dav",
{
username: "sunyard",
password: "sunyard"
}
);
// Get directory contents
const directoryItems = await client.getDirectoryContents("/");
console.log(directoryItems)
// 下载测试
// await wDownload(client, "/kit")
// let rs = fs.createReadStream("D:\\fullstack\\TDevOps\\kit\\dist\\windows\\kit.exe")
// rs.on("data", (r)=>{
// console.log("r", r.length)
// })
// let stat = await client.stat("/kitw.exe");
// console.log("stat", stat)
// let ws = client.createWriteStream(
// "/kit.exe"
// );
// ws.on("data", (r)=>{
// console.log("w", r.length)
// })
// console.log("end")
// await wUpload( client, "D:\\fullstack\\TDevOps\\Readme.md", "/test/case1/data/20250313111818/Readme.md2" )
// await wUpload( client, "../dist/windows/kit.exe", "/modules/kit/0.1.20240227094429/Windows/X86_64/kit.exe" )
console.log("over")
}
//kit3 config remoteAuth sunyard:sunyard
//kit3 config remoteServer http://openlist.honor3.com/dav
main()
+26 -20
View File
@@ -1,21 +1,27 @@
{
"id" : "kit", // 项目 ID
"name" : "kit 辅助工具", // 项目显示名称
"desc" : "手边的开发运维小工具", // 项目描述
"type" : "app", // 类型参考类型表;
"vcs" : "folder", // 版本管理方式
"version": "0.1", // 当前版本号,只包含两位,其他部分自动生成
// "platform" : "cross", // cross=跨平台;[X86_64, ARM64]
// "os" : "cross", // cross=跨操作系统;[Windows, Linux, Alpine]
"dist": [{
platform : "X86_64",
os : "Windows",
path: "kit/dist/windows/kit.exe", // 本地存储路径
"filename" : "kit.exe"
}, {
platform : "X86_64",
os : "Linux",
path: "kit/dist/linux/kit", // 本地存储路径
"filename" : "kit"
}]
{
"id" : "kit", // 项目 ID
"name" : "kit 辅助工具", // 项目显示名称
"desc" : "手边的开发运维小工具", // 项目描述
"type" : "app", // 类型参考类型表;
"vcs" : "folder", // 版本管理方式
"version": "0.1", // 当前版本号,只包含两位,其他部分自动生成
// "platform" : "cross", // cross=跨平台;[X86_64, ARM64]
// "os" : "cross", // cross=跨操作系统;[Windows, Linux, Alpine]
"versions": [{
"version" : "0.1",
"match" : {
"x64|win32" : {
"path": "data/windows/kit.exe",
"filename" : "kit.exe"
},
"x64|linux" : {
"path": "data/20250314003525/kit",
"filename" : "kit"
},
"x64|linux|kylin V10" : {
"path": "data/kylin/kit.zip",
"filename" : "kit"
}
}
}]
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "my-website",
"name": "kit-website",
"version": "0.0.0",
"private": true,
"scripts": {