Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74585a5f07 | ||
|
|
b19716aef9 | ||
|
|
f9912ade7e |
@@ -46,24 +46,27 @@ npm run build
|
||||
代码仓库和 Release 已公开,Linux 服务器不需要 Gitea Token。安装器读取
|
||||
`https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest`,生产机不执行 `git pull` 或现场编译。
|
||||
安装脚本内置固定的发布公钥指纹,下载的应用包、清单、SBOM 和更新脚本仍须通过 RSA 签名与 SHA-256 校验。
|
||||
Gitea API 只用于确认 Release tag 和资产名称;下载地址由受信 API 域名、仓库路径和 tag 重新构造,不采用
|
||||
Gitea 响应中可能错误指向内网地址的 `browser_download_url`。
|
||||
本版只支持由运维人员预先准备的外部 MySQL 8.4.x;安装器不会安装 MySQL、创建数据库容器或修改现有
|
||||
PostgreSQL 18。PostgreSQL 兼容开发已冻结,不属于本次 Preview.10 发布范围。
|
||||
PostgreSQL 18。PostgreSQL 兼容开发已冻结,不属于本次 Preview.12 发布范围。
|
||||
|
||||
执行命令的机器需预装 `bash`、`sudo`、`curl`、`mktemp` 和 `sha256sum`,并能访问目标 Gitea;`jq`、Java
|
||||
和数据库客户端由安装器补齐。应用固定安装到 `/opt/kaidi`、`/var/lib/kaidi`、
|
||||
执行命令的机器需预装 `bash`、`sudo`、`curl`、`mktemp` 和 `sha256sum`,并能访问目标 Gitea;`jq` 和 Java
|
||||
由安装器补齐;Java 17 优先使用服务器已有运行时,没有合适版本时才从官方 Azul 下载。数据库服务和数据库客户端均不会被安装。默认首次安装向导直接使用 JDBC 连接浏览器中填写的
|
||||
外部数据库,不需要服务器安装 MySQL CLI。应用固定安装到 `/opt/kaidi`、`/var/lib/kaidi`、
|
||||
`/var/lib/kaidi-update` 和 `/etc/kaidi`。安装器不安装、不启动也不修改 Nginx、Caddy、宝塔或其他反向代理。
|
||||
|
||||
### 直接 curl 安装
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.10/install.sh | sudo bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.13/install.sh | sudo bash
|
||||
```
|
||||
|
||||
这条命令会提示填写 Java 应用端口,直接回车使用 `18080`;随后安装最新签名 Release,并默认进入 `/setup`
|
||||
安装向导。Java 默认只监听 `127.0.0.1:所选端口`,前端页面、API 和健康检查均由同一端口提供。无人值守安装可直接指定:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.10/install.sh \
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.13/install.sh \
|
||||
| sudo env KAIDI_APP_PORT=19090 bash
|
||||
```
|
||||
|
||||
@@ -71,8 +74,8 @@ curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-previe
|
||||
时才设置 `KAIDI_SERVER_ADDRESS=0.0.0.0`,通常应保持默认回环绑定并由本机反向代理访问。需要在执行前独立校验安装脚本时使用:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.10/install.sh -o /tmp/kaidi-install.sh
|
||||
printf '%s %s\n' cc9ae48fc3e36fbeb13b0660b0d91e59bd099a1386d49df699f214285c1afc0a /tmp/kaidi-install.sh | sha256sum -c -
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.13/install.sh -o /tmp/kaidi-install.sh
|
||||
printf '%s %s\n' 95a5887809edaf1e4ed6201224c4d2d122388c0289a83e2c960a0641d9671664 /tmp/kaidi-install.sh | sha256sum -c -
|
||||
sudo bash /tmp/kaidi-install.sh
|
||||
rm -f /tmp/kaidi-install.sh
|
||||
```
|
||||
@@ -83,7 +86,7 @@ rm -f /tmp/kaidi-install.sh
|
||||
包装器会在 `sudo` 前校验 `deploy/install.sh` 的固定 SHA-256,再按同一公钥信任链安装最新签名 Release。
|
||||
|
||||
```bash
|
||||
git clone --branch v1.0.0-preview.10 --depth 1 https://git.awaioi.com/ERP-Team/kaidi.git kaidi-preview
|
||||
git clone --branch v1.0.0-preview.13 --depth 1 https://git.awaioi.com/ERP-Team/kaidi.git kaidi-preview
|
||||
cd kaidi-preview
|
||||
./deploy/install-from-git.sh
|
||||
```
|
||||
@@ -95,7 +98,7 @@ cd kaidi-preview
|
||||
|
||||
- 仅在 Linux + systemd 环境执行;首版 32 位支持基线为带 systemd 的 Debian/Ubuntu x86 32 位 Linux。
|
||||
- 不安装或创建数据库;在 `/setup` 中连接运维人员预先准备的外部 MySQL 8.4.x。
|
||||
- 识别 `x86_64`、`aarch64`、`armv7` 或 32 位 `i386/i486/i586/i686`,校验 SHA-256 后安装对应的 Azul Java 17 JRE。
|
||||
- 识别 `x86_64`、`aarch64`、`armv7` 或 32 位 `i386/i486/i586/i686`;已有 Java 17 直接复用,否则校验 SHA-256 后下载对应的官方 Azul Java 17 JRE。
|
||||
- 使用安装器内置的 SHA-256 指纹校验 Release 公钥,再用该公钥验证发布清单 RSA 签名。
|
||||
- 首次安装默认启用 `/setup` 向导,不在命令行保存数据库密码;向导只接受 MySQL 8.4.x,并在提交前验证 DDL/DML 权限。
|
||||
- 安装签名 Release 到 `/opt/kaidi/releases/<version>`,以 `/opt/kaidi/current` 原子切换当前版本。
|
||||
@@ -138,10 +141,10 @@ sudo cat /root/kaidi-first-login.txt
|
||||
32 位服务端镜像,因此 32 位主机需要预先连接一台 MySQL 8.4 数据库,之后仍然只执行一个安装命令:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.10/install.sh | sudo bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.13/install.sh | sudo bash
|
||||
```
|
||||
|
||||
32 位主机不能运行安装器自动创建的 MySQL 容器,因此在打开向导前,需要预先创建 `kaidi_finance`,并授予安装账号该库的
|
||||
无论主机架构如何,安装器都不会安装 MySQL、数据库客户端或创建数据库容器。在打开向导前,需要预先创建 `kaidi_finance`,并授予安装账号该库的
|
||||
DDL、DML 权限。向导会连接数据库、核验 MySQL 8.4.x 版本并用临时表验证权限,全部通过后 Flyway 才会建表。
|
||||
数据库管理员可在 MySQL 8.4 中按实际应用服务器地址执行以下基线 SQL:
|
||||
|
||||
@@ -158,7 +161,7 @@ GRANT ALL PRIVILEGES ON kaidi_finance.* TO 'kaidi'@'KAIDI_SERVER_IP';
|
||||
管理员数据和运维人员新增的环境变量:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.10/install.sh \
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.13/install.sh \
|
||||
| sudo env KAIDI_REINSTALL=true KAIDI_SETUP_WIZARD=false bash
|
||||
```
|
||||
|
||||
@@ -168,14 +171,14 @@ curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-previe
|
||||
如果安装器已完成但向导尚未提交,可执行下面的命令重新生成一次性安装码;该恢复路径只接受仍处于向导模式且未锁定的安装,正式模式不会被覆盖。
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.10/install.sh \
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.13/install.sh \
|
||||
| sudo env KAIDI_REINSTALL=true bash
|
||||
```
|
||||
|
||||
### 停用并移除程序
|
||||
|
||||
以下命令移除应用程序和服务,但保留 `/var/lib/kaidi`、`/var/lib/kaidi-update`、`/etc/kaidi` 以及数据库,
|
||||
便于审计、备份或重新安装。确认数据备份前不要删除这些保留目录或 MySQL 数据卷。
|
||||
便于审计、备份或重新安装。确认数据备份前不要删除这些保留目录或外部 MySQL 数据。
|
||||
反向代理由运维人员独立管理,停用程序不会修改其配置。
|
||||
|
||||
```bash
|
||||
@@ -204,8 +207,8 @@ act_runner 提供 `ubuntu-24.04` 标签,并在 tag 发布时执行后端、前
|
||||
Preview 属性由 SemVer 版本名表达。之后推送 tag 即会构建、测试、签名并发布:
|
||||
|
||||
```bash
|
||||
git tag v1.0.0-preview.10
|
||||
git push origin v1.0.0-preview.10
|
||||
git tag v1.0.0-preview.13
|
||||
git push origin v1.0.0-preview.13
|
||||
```
|
||||
|
||||
在线更新使用独立的 TDesign 页面:隔离的系统管理员进入“系统治理 → 系统更新”。权限与配置页只管理用户、角色、数据范围、表单模板和参数版本,不配置系统名称或域名。
|
||||
@@ -217,7 +220,8 @@ URL、Token、脚本或命令。更新流程固定为:
|
||||
3. 页面显示 `READY/等待重启` 后才出现“立即重启”;管理员点击后提交安装。未缓存或版本不一致
|
||||
的包不能进入安装。
|
||||
4. 安装请求持久领取到 `/var/lib/kaidi-update/processing`;进程或主机中断后由 systemd 恢复未完成事务。
|
||||
5. root 更新器重新验签和验哈希,确认 `mysqldump` 成功并生成权限为 `0600` 的备份,默认保留最近 5 份。
|
||||
5. root 更新器重新验签和验哈希,确认 `mysqldump` 成功并生成权限为 `0600` 的备份,默认保留最近 5 份。安装器不安装
|
||||
`mysqldump`;启用在线更新前,运维人员须自行在应用服务器预装与外部 MySQL 兼容的 `mysqldump`。
|
||||
6. 校验更新脚本和 systemd 单元后,原子切换 updater、systemd 和应用版本,不修改反向代理。
|
||||
7. 同时检查 Java 健康端点、更新 path unit 和 Java 托管的静态首页。全部通过后页面显示 10 秒倒计时并自动
|
||||
刷新;刷新或短暂断线发生在安装中时,页面会恢复 3 秒轮询。任一检查失败则恢复并验证上一版本。
|
||||
@@ -256,7 +260,7 @@ cat /var/lib/kaidi-update/status.json
|
||||
```bash
|
||||
KAIDI_RELEASE_SIGNING_KEY=/secure/release-signing-private.pem \
|
||||
KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \
|
||||
./scripts/package-release.sh 1.0.0-preview.10
|
||||
./scripts/package-release.sh 1.0.0-preview.13
|
||||
KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \
|
||||
./scripts/verify-release.sh dist/release
|
||||
```
|
||||
|
||||
+32
-6
@@ -15,6 +15,7 @@ import com.kaidi.finance.update.api.SystemUpdateView;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
@@ -52,6 +53,9 @@ public class SystemUpdateApplicationService {
|
||||
+ "(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$");
|
||||
private static final Pattern ARTIFACT = Pattern.compile("^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}\\.tar\\.gz$");
|
||||
private static final Pattern SHA256 = Pattern.compile("^[0-9a-fA-F]{64}$");
|
||||
private static final Pattern GITEA_RELEASE_API_PATH = Pattern.compile(
|
||||
"^(.*/)?api/v1/repos/([A-Za-z0-9._-]+)/([A-Za-z0-9._-]+)/releases/(?:latest|tags/[^/?#]+)$");
|
||||
private static final Pattern GITEA_RELEASE_TAG = Pattern.compile("^v[0-9A-Za-z][0-9A-Za-z._+-]{0,127}$");
|
||||
private static final List<String> BUSY_STATES = List.of(
|
||||
"QUEUED", "DOWNLOAD_QUEUED", "INSTALL_QUEUED", "VERIFYING", "DOWNLOADING", "BACKING_UP",
|
||||
"INSTALLING", "RUNNING");
|
||||
@@ -195,20 +199,42 @@ public class SystemUpdateApplicationService {
|
||||
URI api = releaseApiUri();
|
||||
try {
|
||||
JsonNode release = objectMapper.readTree(fetchReleaseBytes(api, MAX_RELEASE_API_BYTES, "Gitea Release"));
|
||||
boolean assetPresent = false;
|
||||
for (JsonNode asset : release.path("assets")) {
|
||||
if (!assetName.equals(asset.path("name").asText())) continue;
|
||||
URI uri = validatedReleaseUri(asset.path("browser_download_url").asText(null), "Release 资源地址");
|
||||
if (!sameOrigin(api, uri)) {
|
||||
throw releaseUnavailable("Gitea Release 资源必须与 API 使用同一来源");
|
||||
if (assetName.equals(asset.path("name").asText())) {
|
||||
assetPresent = true;
|
||||
break;
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
throw releaseUnavailable("Gitea Release 缺少 " + assetName);
|
||||
if (!assetPresent) throw releaseUnavailable("Gitea Release 缺少 " + assetName);
|
||||
String tag = release.path("tag_name").asText(null);
|
||||
if (tag == null || !GITEA_RELEASE_TAG.matcher(tag).matches()) {
|
||||
throw releaseUnavailable("Gitea Release tag 格式无效");
|
||||
}
|
||||
return trustedGiteaAssetUri(api, tag, assetName);
|
||||
} catch (IOException exception) {
|
||||
throw releaseUnavailable("Gitea Release 响应读取失败");
|
||||
}
|
||||
}
|
||||
|
||||
private URI trustedGiteaAssetUri(URI api, String tag, String assetName) {
|
||||
if (api.getRawQuery() != null || api.getRawFragment() != null) {
|
||||
throw releaseUnavailable("Gitea Release API 地址格式无效");
|
||||
}
|
||||
Matcher matcher = GITEA_RELEASE_API_PATH.matcher(api.getPath());
|
||||
if (!matcher.matches()) {
|
||||
throw releaseUnavailable("Gitea Release API 地址必须指向仓库 Release 端点");
|
||||
}
|
||||
String prefix = matcher.group(1) == null ? "/" : matcher.group(1);
|
||||
String path = prefix + matcher.group(2) + "/" + matcher.group(3)
|
||||
+ "/releases/download/" + tag + "/" + assetName;
|
||||
try {
|
||||
return new URI(api.getScheme(), null, api.getHost(), api.getPort(), path, null, null);
|
||||
} catch (URISyntaxException exception) {
|
||||
throw releaseUnavailable("Gitea Release 资源地址格式无效");
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] fetchReleaseBytes(URI uri, int maximumBytes, String resourceName) {
|
||||
String token = releaseToken();
|
||||
URI current = uri;
|
||||
|
||||
+19
-7
@@ -47,18 +47,17 @@ class SystemUpdateApplicationServiceTest {
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
server.createContext("/api/v1/repos/ERP-Team/kaidi/releases/latest", exchange -> {
|
||||
assertEquals("token read-only-token", exchange.getRequestHeaders().getFirst("Authorization"));
|
||||
String assetUrl = "http://127.0.0.1:" + server.getAddress().getPort()
|
||||
+ "/assets/release-manifest.json";
|
||||
byte[] body = ("""
|
||||
{"tag_name":"v1.0.0-preview.2","assets":[
|
||||
{"name":"release-manifest.json","browser_download_url":"%s"}
|
||||
]}
|
||||
""").formatted(assetUrl).getBytes(StandardCharsets.UTF_8);
|
||||
""").formatted("http://10.0.0.8:3000/internal/release-manifest.json")
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.sendResponseHeaders(200, body.length);
|
||||
exchange.getResponseBody().write(body);
|
||||
exchange.close();
|
||||
});
|
||||
server.createContext("/assets/release-manifest.json", exchange -> {
|
||||
server.createContext("/ERP-Team/kaidi/releases/download/v1.0.0-preview.2/release-manifest.json", exchange -> {
|
||||
assertEquals("token read-only-token", exchange.getRequestHeaders().getFirst("Authorization"));
|
||||
byte[] body = """
|
||||
{"version":"1.0.0-preview.2","artifact":"kaidi-finance-1.0.0-preview.2.tar.gz",
|
||||
@@ -98,7 +97,7 @@ class SystemUpdateApplicationServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsPrivateGiteaAssetOnAnotherOriginWithoutSendingToken() throws Exception {
|
||||
void ignoresCrossOriginBrowserAssetUrlWithoutSendingTokenThere() throws Exception {
|
||||
AtomicInteger assetRequests = new AtomicInteger();
|
||||
HttpServer assetServer = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
assetServer.createContext("/release-manifest.json", exchange -> {
|
||||
@@ -112,12 +111,25 @@ class SystemUpdateApplicationServiceTest {
|
||||
String assetUrl = "http://127.0.0.1:" + assetServer.getAddress().getPort()
|
||||
+ "/release-manifest.json";
|
||||
byte[] body = ("""
|
||||
{"assets":[{"name":"release-manifest.json","browser_download_url":"%s"}]}
|
||||
{"tag_name":"v1.0.0-preview.2",
|
||||
"assets":[{"name":"release-manifest.json","browser_download_url":"%s"}]}
|
||||
""").formatted(assetUrl).getBytes(StandardCharsets.UTF_8);
|
||||
exchange.sendResponseHeaders(200, body.length);
|
||||
exchange.getResponseBody().write(body);
|
||||
exchange.close();
|
||||
});
|
||||
apiServer.createContext(
|
||||
"/ERP-Team/kaidi/releases/download/v1.0.0-preview.2/release-manifest.json", exchange -> {
|
||||
assertEquals("token read-only-token", exchange.getRequestHeaders().getFirst("Authorization"));
|
||||
byte[] body = """
|
||||
{"version":"1.0.0-preview.2","artifact":"kaidi-finance-1.0.0-preview.2.tar.gz",
|
||||
"sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"publishedAt":"2026-08-16T00:00:00Z","releaseNotes":"Gitea Preview update"}
|
||||
""".getBytes(StandardCharsets.UTF_8);
|
||||
exchange.sendResponseHeaders(200, body.length);
|
||||
exchange.getResponseBody().write(body);
|
||||
exchange.close();
|
||||
});
|
||||
assetServer.start();
|
||||
apiServer.start();
|
||||
try {
|
||||
@@ -125,7 +137,7 @@ class SystemUpdateApplicationServiceTest {
|
||||
SystemUpdateApplicationService service = serviceForGitea(apiServer, inbox,
|
||||
tempDir.resolve("cross-origin-status.json"));
|
||||
|
||||
assertThrows(BusinessException.class, service::check);
|
||||
assertEquals("1.0.0-preview.2", service.check().latestVersion());
|
||||
assertEquals(0, assetRequests.get());
|
||||
} finally {
|
||||
apiServer.stop(0);
|
||||
|
||||
@@ -5,7 +5,7 @@ umask 077
|
||||
|
||||
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
INSTALLER="$ROOT/deploy/install.sh"
|
||||
INSTALLER_SHA256=${KAIDI_INSTALLER_SHA256:-cc9ae48fc3e36fbeb13b0660b0d91e59bd099a1386d49df699f214285c1afc0a}
|
||||
INSTALLER_SHA256=${KAIDI_INSTALLER_SHA256:-95a5887809edaf1e4ed6201224c4d2d122388c0289a83e2c960a0641d9671664}
|
||||
RELEASE_API_URL=${KAIDI_RELEASE_API_URL:-https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest}
|
||||
PUBLIC_KEY_SHA256=${KAIDI_RELEASE_PUBLIC_KEY_SHA256:-807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9}
|
||||
TOKEN_FILE=${KAIDI_RELEASE_TOKEN_FILE:-}
|
||||
|
||||
+56
-17
@@ -89,13 +89,17 @@ download() {
|
||||
local url=$1 output=$2
|
||||
case "$url" in
|
||||
https://*)
|
||||
curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' \
|
||||
--tlsv1.2 "$url" -o "$output"
|
||||
if ! curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' \
|
||||
--tlsv1.2 --connect-timeout 15 --max-time 600 "$url" -o "$output"; then
|
||||
die "Download failed: $url"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
[ "${KAIDI_ALLOW_INSECURE_UPDATE:-false}" = "true" ] || die "Release URLs must use HTTPS"
|
||||
curl --fail --silent --show-error --location --proto '=http,https' \
|
||||
--proto-redir '=http,https' "$url" -o "$output"
|
||||
if ! curl --fail --silent --show-error --location --proto '=http,https' \
|
||||
--proto-redir '=http,https' --connect-timeout 15 --max-time 600 "$url" -o "$output"; then
|
||||
die "Download failed: $url"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -126,17 +130,20 @@ download_release_url() {
|
||||
}
|
||||
|
||||
release_asset_url() {
|
||||
local asset_name=$1 asset_url api_origin
|
||||
local asset_name=$1 api_origin repo_path release_tag encoded_tag encoded_asset
|
||||
if [ -n "$RELEASE_API_URL" ]; then
|
||||
asset_url=$(jq -er --arg name "$asset_name" \
|
||||
'.assets[] | select(.name == $name) | .browser_download_url
|
||||
| strings | select(startswith("https://") or startswith("http://"))' \
|
||||
"$WORK_DIR/release-api.json" | head -n 1) || die "Release asset $asset_name is missing"
|
||||
jq -e --arg name "$asset_name" 'any(.assets[]?; .name == $name)' \
|
||||
"$WORK_DIR/release-api.json" >/dev/null || die "Release asset $asset_name is missing"
|
||||
release_tag=$(jq -er '.tag_name | strings
|
||||
| select(test("^v[0-9A-Za-z][0-9A-Za-z._+-]{0,127}$"))' \
|
||||
"$WORK_DIR/release-api.json") || die "Gitea Release tag is invalid"
|
||||
api_origin=$(printf '%s' "$RELEASE_API_URL" | sed -E 's#^(https?://[^/]+).*$#\1#')
|
||||
case "$asset_url" in
|
||||
"$api_origin"/*) printf '%s\n' "$asset_url" ;;
|
||||
*) die "Release asset $asset_name must use the Gitea API host" ;;
|
||||
esac
|
||||
repo_path=$(printf '%s' "$RELEASE_API_URL" | sed -nE \
|
||||
's#^https?://[^/]+/api/v1/repos/([A-Za-z0-9._-]+/[A-Za-z0-9._-]+)/releases/(latest|tags/[^/?#]+)$#\1#p')
|
||||
[ -n "$repo_path" ] || die "KAIDI_RELEASE_API_URL must be a Gitea repository Release API endpoint"
|
||||
encoded_tag=$(jq -rn --arg value "$release_tag" '$value | @uri')
|
||||
encoded_asset=$(jq -rn --arg value "$asset_name" '$value | @uri')
|
||||
printf '%s/%s/releases/download/%s/%s\n' "$api_origin" "$repo_path" "$encoded_tag" "$encoded_asset"
|
||||
else
|
||||
printf '%s/%s\n' "${RELEASE_BASE_URL%/}" "$asset_name"
|
||||
fi
|
||||
@@ -152,11 +159,11 @@ install_packages() {
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq ca-certificates coreutils curl findutils gzip jq openssl tar util-linux default-mysql-client
|
||||
apt-get install -y -qq ca-certificates coreutils curl findutils gzip jq openssl tar util-linux
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y ca-certificates coreutils curl findutils gzip jq openssl tar util-linux mysql
|
||||
dnf install -y ca-certificates coreutils curl findutils gzip jq openssl tar util-linux
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
yum install -y ca-certificates coreutils curl findutils gzip jq openssl tar util-linux mysql
|
||||
yum install -y ca-certificates coreutils curl findutils gzip jq openssl tar util-linux
|
||||
else
|
||||
die "Supported package managers are apt, dnf, and yum"
|
||||
fi
|
||||
@@ -172,8 +179,40 @@ azul_arch() {
|
||||
esac
|
||||
}
|
||||
|
||||
system_java_home() {
|
||||
local java_bin resolved_java java_major home
|
||||
if [ -n "${KAIDI_JAVA_HOME:-}" ]; then
|
||||
home=${KAIDI_JAVA_HOME%/}
|
||||
java_bin="$home/bin/java"
|
||||
else
|
||||
java_bin=$(command -v java 2>/dev/null || true)
|
||||
fi
|
||||
[ -x "$java_bin" ] || return 1
|
||||
java_major=$(
|
||||
"$java_bin" -version 2>&1 \
|
||||
| sed -n 's/.*version "\([0-9][0-9]*\).*/\1/p' \
|
||||
| head -n 1
|
||||
)
|
||||
[[ "$java_major" =~ ^[0-9]+$ ]] && [ "$java_major" -ge 17 ] || return 1
|
||||
resolved_java=$(readlink -f "$java_bin" 2>/dev/null || printf '%s' "$java_bin")
|
||||
case "$resolved_java" in
|
||||
*/bin/java) home=${resolved_java%/bin/java} ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
[ -x "$home/bin/java" ] || return 1
|
||||
printf '%s\n' "$home"
|
||||
}
|
||||
|
||||
prepare_java() {
|
||||
local api java_metadata package_metadata package_uuid java_url java_sha256 actual_sha256
|
||||
local api java_metadata package_metadata package_uuid java_url java_sha256 actual_sha256 system_home
|
||||
if system_home=$(system_java_home); then
|
||||
JAVA_STAGED_DIR="$WORK_DIR/java.next"
|
||||
ln -s "$system_home" "$JAVA_STAGED_DIR"
|
||||
log "Using existing Java 17 runtime at $system_home"
|
||||
"$JAVA_STAGED_DIR/bin/java" -version
|
||||
return
|
||||
fi
|
||||
log "No local Java 17 runtime found; downloading the official Azul Java 17 runtime"
|
||||
api="https://api.azul.com/metadata/v1/zulu/packages/?java_version=17&os=linux&arch=$(azul_arch)&archive_type=tar.gz&java_package_type=jre&release_status=ga&availability_types=CA&latest=true"
|
||||
java_metadata="$WORK_DIR/zulu-metadata.json"
|
||||
download "$api" "$java_metadata"
|
||||
|
||||
+11
-8
@@ -124,15 +124,18 @@ download() {
|
||||
release_asset_url() {
|
||||
asset_name=$1
|
||||
if [ -n "$RELEASE_API_URL" ]; then
|
||||
asset_url=$(jq -er --arg name "$asset_name" \
|
||||
'.assets[] | select(.name == $name) | .browser_download_url
|
||||
| strings | select(startswith("https://") or startswith("http://"))' \
|
||||
"$WORK_DIR/release-api.json" | head -n 1) || return 1
|
||||
jq -e --arg name "$asset_name" 'any(.assets[]?; .name == $name)' \
|
||||
"$WORK_DIR/release-api.json" >/dev/null || return 1
|
||||
release_tag=$(jq -er '.tag_name | strings
|
||||
| select(test("^v[0-9A-Za-z][0-9A-Za-z._+-]{0,127}$"))' \
|
||||
"$WORK_DIR/release-api.json") || return 1
|
||||
api_origin=$(printf '%s' "$RELEASE_API_URL" | sed -E 's#^(https?://[^/]+).*$#\1#')
|
||||
case "$asset_url" in
|
||||
"$api_origin"/*) printf '%s\n' "$asset_url" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
repo_path=$(printf '%s' "$RELEASE_API_URL" | sed -nE \
|
||||
's#^https?://[^/]+/api/v1/repos/([A-Za-z0-9._-]+/[A-Za-z0-9._-]+)/releases/(latest|tags/[^/?#]+)$#\1#p')
|
||||
[ -n "$repo_path" ] || return 1
|
||||
encoded_tag=$(jq -rn --arg value "$release_tag" '$value | @uri')
|
||||
encoded_asset=$(jq -rn --arg value "$asset_name" '$value | @uri')
|
||||
printf '%s/%s/releases/download/%s/%s\n' "$api_origin" "$repo_path" "$encoded_tag" "$encoded_asset"
|
||||
else
|
||||
printf '%s/%s\n' "${RELEASE_BASE_URL%/}" "$asset_name"
|
||||
fi
|
||||
|
||||
@@ -99,7 +99,7 @@ cp "$ROOT/deploy/systemd/kaidi-update.path" "$STAGE/ops/kaidi-update.path"
|
||||
chmod 0755 "$STAGE/ops/update.sh"
|
||||
|
||||
ARTIFACT="kaidi-finance-$VERSION.tar.gz"
|
||||
COPYFILE_DISABLE=1 tar -czf "$OUTPUT_DIR/$ARTIFACT" -C "$STAGE" .
|
||||
COPYFILE_DISABLE=1 tar --format=ustar -czf "$OUTPUT_DIR/$ARTIFACT" -C "$STAGE" .
|
||||
SHA256=$(sha256_file "$OUTPUT_DIR/$ARTIFACT")
|
||||
cp "$ROOT/backend/target/backend-sbom.json" "$OUTPUT_DIR/backend-sbom.cdx.json"
|
||||
(cd "$ROOT/frontend" && npm sbom --omit=dev --package-lock-only \
|
||||
|
||||
@@ -23,11 +23,13 @@ fail() {
|
||||
sed -n '/^is_semver()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^write_env_file_preserving_unknown()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^azul_arch()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^system_java_home()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^sha256_file()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^prepare_java()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^download_release_url()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^release_asset_url()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^download_release_asset()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^install_packages()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
} > "$WORK/helpers.sh"
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
source "$WORK/helpers.sh"
|
||||
@@ -156,8 +158,15 @@ download() {
|
||||
esac
|
||||
}
|
||||
ARCH_FIXTURE=i686
|
||||
export KAIDI_JAVA_HOME="$WORK/missing-java-home"
|
||||
prepare_java >/dev/null 2>&1
|
||||
[ -x "$JAVA_STAGED_DIR/bin/java" ] || fail 'verified i686 Java runtime was not staged'
|
||||
rm -rf "$JAVA_STAGED_DIR"
|
||||
export KAIDI_JAVA_HOME="$WORK/fake-jre"
|
||||
prepare_java >/dev/null 2>&1
|
||||
[ -L "$JAVA_STAGED_DIR" ] || fail 'existing Java 17 runtime was not reused'
|
||||
[ "$(readlink -f "$JAVA_STAGED_DIR")" = "$(readlink -f "$WORK/fake-jre")" ] \
|
||||
|| fail 'installer linked an unexpected local Java runtime'
|
||||
|
||||
export RELEASE_API_URL=https://gitea.fixture.invalid/api/v1/repos/ERP-Team/kaidi/releases/latest
|
||||
RELEASE_TOKEN=fixture-read-only-token
|
||||
@@ -165,7 +174,7 @@ RELEASE_AUTH_HEADER_FILE=$WORK/release-auth-header
|
||||
printf 'Authorization: token %s\n' "$RELEASE_TOKEN" > "$RELEASE_AUTH_HEADER_FILE"
|
||||
chmod 0600 "$RELEASE_AUTH_HEADER_FILE"
|
||||
cat > "$WORK/release-api.json" <<'JSON'
|
||||
{"assets":[
|
||||
{"tag_name":"v1.0.0-preview.11","assets":[
|
||||
{"name":"release-manifest.json","browser_download_url":"https://gitea.fixture.invalid/assets/release-manifest.json"}
|
||||
]}
|
||||
JSON
|
||||
@@ -188,12 +197,13 @@ curl() {
|
||||
done
|
||||
[ "$header_file" = "$RELEASE_AUTH_HEADER_FILE" ] || return 90
|
||||
grep -Fqx "Authorization: token $RELEASE_TOKEN" "$header_file" || return 91
|
||||
[ "$url" = https://gitea.fixture.invalid/assets/release-manifest.json ] || return 92
|
||||
[ "$url" = https://gitea.fixture.invalid/ERP-Team/kaidi/releases/download/v1.0.0-preview.11/release-manifest.json ] \
|
||||
|| return 92
|
||||
cp "$WORK/release-manifest.fixture" "$output"
|
||||
}
|
||||
[ "$(release_asset_url release-manifest.json)" = \
|
||||
https://gitea.fixture.invalid/assets/release-manifest.json ] \
|
||||
|| fail 'installer did not resolve the private Gitea release asset'
|
||||
https://gitea.fixture.invalid/ERP-Team/kaidi/releases/download/v1.0.0-preview.11/release-manifest.json ] \
|
||||
|| fail 'installer did not construct the trusted Gitea release asset URL'
|
||||
download_release_asset release-manifest.json "$WORK/downloaded-manifest.json"
|
||||
cmp -s "$WORK/release-manifest.fixture" "$WORK/downloaded-manifest.json" \
|
||||
|| fail 'installer did not download the private Gitea release asset'
|
||||
@@ -205,9 +215,9 @@ cmp -s "$WORK/release-manifest.fixture" "$WORK/downloaded-manifest.json" \
|
||||
jq '.assets[0].browser_download_url = "https://assets.fixture.invalid/release-manifest.json"' \
|
||||
"$WORK/release-api.json" > "$WORK/release-api.cross-origin.json"
|
||||
mv "$WORK/release-api.cross-origin.json" "$WORK/release-api.json"
|
||||
if release_asset_url release-manifest.json >/dev/null 2>&1; then
|
||||
fail 'installer accepted a cross-origin Gitea release asset'
|
||||
fi
|
||||
[ "$(release_asset_url release-manifest.json)" = \
|
||||
https://gitea.fixture.invalid/ERP-Team/kaidi/releases/download/v1.0.0-preview.11/release-manifest.json ] \
|
||||
|| fail 'installer trusted the cross-origin browser download URL'
|
||||
|
||||
# shellcheck disable=SC2016 # Match literal installer source.
|
||||
grep -Fq '[ "$APP_ROOT" = /opt/kaidi ]' "$ROOT/deploy/install.sh" \
|
||||
@@ -223,6 +233,8 @@ grep -Fq 'download "$api" "$java_metadata"' "$ROOT/deploy/install.sh" \
|
||||
# shellcheck disable=SC2016 # Match literal installer source.
|
||||
grep -Fq 'java_sha256=$(jq -er' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer no longer obtains the Java runtime SHA-256'
|
||||
grep -Fq 'Using existing Java 17 runtime' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer no longer reuses a local Java 17 runtime'
|
||||
# shellcheck disable=SC2016 # Match literal installer source.
|
||||
grep -Fq '[ "$actual_sha256" = "$java_sha256" ]' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer no longer verifies the Java runtime SHA-256'
|
||||
@@ -238,6 +250,26 @@ grep -Fq 'SETUP_WIZARD=${KAIDI_SETUP_WIZARD:-true}' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer must not create a MySQL container'
|
||||
! grep -Fq 'install_docker' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer must not install Docker or MySQL automatically'
|
||||
package_log="$WORK/package-manager.log"
|
||||
for package_manager in apt-get dnf yum; do
|
||||
package_bin="$WORK/package-manager-$package_manager"
|
||||
mkdir "$package_bin"
|
||||
cat > "$package_bin/$package_manager" <<'SH'
|
||||
#!/bin/sh
|
||||
printf '%s %s\n' "${0##*/}" "$*" >> "$PACKAGE_LOG"
|
||||
SH
|
||||
chmod +x "$package_bin/$package_manager"
|
||||
: > "$package_log"
|
||||
(
|
||||
PATH="$package_bin"
|
||||
PACKAGE_LOG="$package_log"
|
||||
export PATH PACKAGE_LOG
|
||||
install_packages
|
||||
)
|
||||
if grep -Eqi '(^|[[:space:]])(default-mysql-client|mysql(-client|-server)?|mariadb(-client|-server)?)([[:space:]]|$)' "$package_log"; then
|
||||
fail "installer asked $package_manager to install a database package"
|
||||
fi
|
||||
done
|
||||
grep -Fq 'An external MySQL 8.4 database is required' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer does not require an operator-managed external MySQL database'
|
||||
grep -Fq 'KAIDI_RELEASE_TOKEN_FILE' "$ROOT/deploy/install.sh" \
|
||||
|
||||
@@ -154,10 +154,12 @@ build_release() {
|
||||
build_gitea_release_index() {
|
||||
local fixture=$1
|
||||
local origin=${2:-https://gitea.fixture.invalid}
|
||||
local artifact
|
||||
local artifact version tag
|
||||
artifact=$(jq -er '.artifact' "$fixture/release/release-manifest.json")
|
||||
jq -n --arg origin "$origin" --arg artifact "$artifact" \
|
||||
'{assets:[
|
||||
version=$(jq -er '.version' "$fixture/release/release-manifest.json")
|
||||
tag="v$version"
|
||||
jq -n --arg origin "$origin" --arg artifact "$artifact" --arg tag "$tag" \
|
||||
'{tag_name:$tag,assets:[
|
||||
{name:"release-manifest.json",browser_download_url:($origin + "/assets/release-manifest.json")},
|
||||
{name:"release-manifest.sig",browser_download_url:($origin + "/assets/release-manifest.sig")},
|
||||
{name:$artifact,browser_download_url:($origin + "/assets/" + $artifact)}
|
||||
@@ -267,7 +269,7 @@ assert_private_gitea_release_case() {
|
||||
|| fail 'authenticated private Gitea requests unexpectedly enabled redirects'
|
||||
}
|
||||
|
||||
assert_cross_origin_gitea_asset_rejected() {
|
||||
assert_cross_origin_gitea_browser_url_ignored() {
|
||||
local fixture="$WORK/cross-origin-gitea"
|
||||
local version='1.0.0-preview.2'
|
||||
mkdir -p "$fixture"
|
||||
@@ -276,18 +278,16 @@ assert_cross_origin_gitea_asset_rejected() {
|
||||
build_gitea_release_index "$fixture" https://assets.fixture.invalid
|
||||
prepare_installation "$fixture" "$version"
|
||||
|
||||
if FIXTURE_RELEASE_BASE_URL='' \
|
||||
FIXTURE_RELEASE_BASE_URL='' \
|
||||
FIXTURE_RELEASE_API_URL=https://gitea.fixture.invalid/api/v1/repos/ERP-Team/kaidi/releases/latest \
|
||||
FIXTURE_RELEASE_ORIGIN=https://gitea.fixture.invalid \
|
||||
FIXTURE_RELEASE_TOKEN=fixture-read-only-token \
|
||||
MOCK_CURL_LOG="$fixture/curl.log" \
|
||||
run_update "$fixture" success > "$fixture/update.log" 2>&1; then
|
||||
fail 'cross-origin Gitea asset unexpectedly succeeded'
|
||||
fi
|
||||
grep -q 'Release manifest asset is missing' "$fixture/update.log" \
|
||||
|| fail 'cross-origin Gitea asset rejection was not reported'
|
||||
run_update "$fixture" success
|
||||
[ "$(jq -r '.state' "$fixture/state/status.json")" = READY ] \
|
||||
|| fail 'misconfigured browser download URL prevented trusted same-origin download'
|
||||
! grep -Fq 'assets.fixture.invalid' "$fixture/curl.log" \
|
||||
|| fail 'cross-origin Gitea asset was requested'
|
||||
|| fail 'cross-origin browser download URL was requested'
|
||||
}
|
||||
|
||||
assert_success_case() {
|
||||
@@ -430,5 +430,5 @@ assert_database_failure_case
|
||||
assert_symlink_request_rejected
|
||||
assert_install_without_verified_cache_rejected
|
||||
assert_private_gitea_release_case
|
||||
assert_cross_origin_gitea_asset_rejected
|
||||
assert_cross_origin_gitea_browser_url_ignored
|
||||
printf 'Online update download, confirmation, success, rollback, failure, and unsafe-request fixtures passed\n'
|
||||
|
||||
Reference in New Issue
Block a user