chore: import verified syly2 maintenance baseline without embedded credentials

This commit is contained in:
Ethan
2026-10-29 22:22:49 +08:00
commit da8224efd8
5265 changed files with 1199963 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
> 历史记录:以下说明对应 2026-10-29 原始 UAT 构建及部署,当前仓库的构建入口和密钥配置请看根目录 README。
# syly2 UAT 编译工具
目标主机:`ubuntu@192.168.68.223`。这些工具用于隔离的源码编译,不包含数据库初始化或应用发布步骤。
## 本次目录
- 工具链:`/home/ubuntu/.local/opt/syly2-build`
- Maven 专用仓库:`/home/ubuntu/.local/share/syly2-build/m2`
- 构建记录:`/home/ubuntu/builds/syly2/20261029-01`
- 编译副本:上述构建目录下的 `source`
- 构建日志:上述构建目录下的 `logs`
- 本地传输包、原始文件哈希清单及回收证据:项目 `.local/uat-build/20261029-01`
## 工具职责
1. `prepare-toolchain.py`:安装固定版本 Corretto 8.512.09.1 与 Maven 3.9.11,验证官方 SHA-256/SHA-512 校验值;只写入当前用户的专用目录,不修改系统 Java 或 shell 配置。
2. `install-private-libs.py RUN_DIRECTORY`:将交接包中的 6 个旧 JAR 注册到专用 Maven 仓库,记录原文件哈希。JODConverter 使用原项目声明的坐标和交接包中的 JAR,其原嵌入坐标不同,未据此升级或替换组件。
3. `apply-compatibility.py RUN_DIRECTORY`:校验原始文件哈希,备份并修改编译副本中的三个文件:POM 的 Windows bootclasspath 分号改为 `${path.separator}`,两个统计 Controller 各删除一条未使用的 JDK 内部类 import。保留原换行符,并输出完整 `compatibility.patch`。`pom-linux.patch` 单独展示第一项差异。
4. `run-build.sh RUN_DIRECTORY PHASE`:在专用 JDK/仓库下执行 `clean package`,限制 JVM 内存、降低 CPU 调度优先级并记录退出码;显式跳过测试编译及执行。
5. `verify-build.py RUN_DIRECTORY PHASE`:校验构建退出码、WAR CRC、关键条目和 Java 字节码版本,核对源码差异,保存产物与哈希,并比较构建前后的共享 UAT 容器状态。需要构建目录中已有 `containers-before.json` 基线记录。
本次只打包外层工程的 `pom.xml`、`src`、`jar`,不带历史 `target` 或内层重复工程。传输包与展开后的 5,302 个文件均核验了 SHA-256。
## 重新编译当前 UAT 副本
```sh
ssh ubuntu@192.168.68.223 'bash /home/ubuntu/builds/syly2/20261029-01/scripts/run-build.sh /home/ubuntu/builds/syly2/20261029-01 repeat'
```
`repeat` 对应日志文件名;需要保留每次日志时请使用不同的阶段名。
仅使用编译工具链时:
```sh
source /home/ubuntu/.local/opt/syly2-build/env.sh
java -version
mvn -version
```
## 在新的构建目录复现
先建立当前用户所有、权限为 700 的独立目录,创建 `source`、`logs`、`artifacts`、`scripts` 子目录,复制并核验源码包、原始 `source-manifest.json` 与这些工具。然后执行:
```sh
python3 scripts/prepare-toolchain.py
python3 scripts/install-private-libs.py /absolute/path/to/build-run
python3 scripts/apply-compatibility.py /absolute/path/to/build-run
bash scripts/run-build.sh /absolute/path/to/build-run linux-build
```
源码包包含原有配置,构建目录、传输包和产物均应按受限资料管理。编译通过不代表应用启动、数据库恢复、JSP 运行或省级接口联调通过。
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""Apply only the three verified compilation fixes to an isolated source copy."""
import difflib
import hashlib
import json
import os
from pathlib import Path
import re
import sys
os.umask(0o077)
run = Path(sys.argv[1]).resolve()
source = run / 'source'
manifest = json.loads((run / 'source-manifest.json').read_text())
changes = {
'pom.xml': (
b'${java.home}/lib/rt.jar;${java.home}/lib/jce.jar',
b'${java.home}/lib/rt.jar${path.separator}${java.home}/lib/jce.jar',
),
'src/main/java/com/zzc/web/htoal/controller/HotelQuarterStatisticController.java': (
b'import com.sun.org.apache.regexp.internal.recompile;\r\n', b'',
),
'src/main/java/com/zzc/web/travel/controller/InlandStatisticPageController.java': (
b'import com.sun.org.apache.regexp.internal.recompile;\r\n', b'',
),
}
patches = []
for relative, (before, after) in changes.items():
path = source / relative
data = path.read_bytes()
backup = run / 'artifacts/original-build-files' / relative
if hashlib.sha256(data).hexdigest() == manifest[relative]:
assert data.count(before) == 1, relative
if relative.endswith('.java'):
assert len(re.findall(rb'\brecompile\b', data)) == 1, relative
backup.parent.mkdir(parents=True, exist_ok=True)
backup.write_bytes(data)
path.write_bytes(data.replace(before, after, 1))
elif relative == 'pom.xml' and (run / 'artifacts/pom-original.xml').exists():
original = (run / 'artifacts/pom-original.xml').read_bytes()
assert hashlib.sha256(original).hexdigest() == manifest[relative]
assert data == original.replace(before, after, 1)
backup.parent.mkdir(parents=True, exist_ok=True)
backup.write_bytes(original)
else:
assert backup.exists(), 'Unexpected source change: ' + relative
assert hashlib.sha256(backup.read_bytes()).hexdigest() == manifest[relative]
assert data == backup.read_bytes().replace(before, after, 1)
patches.extend(difflib.unified_diff(
backup.read_text().splitlines(keepends=True), path.read_text().splitlines(keepends=True),
fromfile='a/' + relative, tofile='b/' + relative,
))
print('Compatibility fix verified:', relative)
(run / 'artifacts/compatibility.patch').write_text(''.join(patches))
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""Register the six supplied legacy JARs in a dedicated Maven repository."""
import hashlib
import json
import os
from pathlib import Path
import subprocess
import sys
import xml.etree.ElementTree as ET
os.umask(0o077)
run = Path(sys.argv[1]).resolve()
source = run / 'source'
repo = Path.home() / '.local/share/syly2-build/m2'
base = Path.home() / '.local/opt/syly2-build'
env = dict(os.environ, JAVA_HOME=str(base / 'corretto-8.512.09.1'))
env['PATH'] = env['JAVA_HOME'] + '/bin:' + env['PATH']
mvn = base / 'apache-maven-3.9.11/bin/mvn'
artifacts = [
('com.oracle', 'ojdbc14', '10.2.0.4.0', 'jar/ojdbc14/ojdbc14/10.2.0.4.0/ojdbc14-10.2.0.4.0.jar'),
*[('com.ckfinder', name, '2.4', f'jar/ckfinder/ckfinder/{name}/2.4/{name}-2.4.jar') for name in [
'CKFinder-updateByAlexander', 'CKFinderPlugin-FileEditor',
'CKFinderPlugin-ImageResize', 'CKFinderPlugin-Watermark',
]],
('com.artofsolving', 'jodconverter', '3.0-beta-4', 'src/main/webapp/WEB-INF/lib/jodconverter-3.0-beta-4.jar'),
]
evidence = []
poms = run / 'artifacts/local-poms'
poms.mkdir(exist_ok=True)
for group, artifact, version, relative in artifacts:
jar = source / relative
original_pom = jar.with_suffix('.pom')
if original_pom.is_file():
pom = original_pom
else:
project = ET.Element('project', xmlns='http://maven.apache.org/POM/4.0.0')
for tag, text in [('modelVersion', '4.0.0'), ('groupId', group), ('artifactId', artifact), ('version', version), ('packaging', 'jar')]:
ET.SubElement(project, tag).text = text
pom = poms / f'{artifact}-{version}.pom'
ET.ElementTree(project).write(pom, encoding='utf-8', xml_declaration=True)
log = run / 'logs' / f'install-{artifact}.log'
with log.open('w') as output:
result = subprocess.run([
str(mvn), '-B', '-ntp', f'-Dmaven.repo.local={repo}',
'org.apache.maven.plugins:maven-install-plugin:3.1.4:install-file',
f'-Dfile={jar}', f'-DpomFile={pom}',
f'-DgroupId={group}', f'-DartifactId={artifact}', f'-Dversion={version}', '-Dpackaging=jar',
], cwd=run, env=env, stdout=output, stderr=subprocess.STDOUT, timeout=300)
if result.returncode:
raise RuntimeError(f'Installation failed: {artifact}; see {log}')
evidence.append({'coordinates': f'{group}:{artifact}:{version}', 'source': relative, 'sha256': hashlib.sha256(jar.read_bytes()).hexdigest()})
print('Installed', evidence[-1]['coordinates'], flush=True)
(run / 'artifacts/private-libs.json').write_text(json.dumps(evidence, indent=2) + '\n')
+11
View File
@@ -0,0 +1,11 @@
--- a/pom.xml
+++ b/pom.xml
@@ -812,7 +812,7 @@
</compilerArgs>
<compilerArguments>
<verbose/>
- <bootclasspath>${java.home}/lib/rt.jar;${java.home}/lib/jce.jar</bootclasspath>
+ <bootclasspath>${java.home}/lib/rt.jar${path.separator}${java.home}/lib/jce.jar</bootclasspath>
</compilerArguments>
<fork>true</fork>
</configuration>
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Install the pinned syly2 build toolchain in the current UAT user's home."""
import hashlib
import json
import os
from pathlib import Path
import subprocess
import tarfile
import urllib.request
os.umask(0o077)
base = Path.home() / '.local/opt/syly2-build'
base.mkdir(parents=True, exist_ok=True)
downloads = base / 'downloads'
downloads.mkdir(exist_ok=True)
packages = [
{
'name': 'corretto-8.512.09.1',
'url': 'https://corretto.aws/downloads/resources/8.512.09.1/amazon-corretto-8.512.09.1-linux-x64.tar.gz',
'algorithm': 'sha256',
'checksum': '2f2df938886e61c80010ed5af0ea6504ca685280a3b807c5c59ed3ca4fb95be4',
'checksum_source': 'https://corretto.aws/downloads/latest_sha256/amazon-corretto-8-x64-linux-jdk.tar.gz',
'executable': 'bin/java',
},
{
'name': 'apache-maven-3.9.11',
'url': 'https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.tar.gz',
'algorithm': 'sha512',
'checksum': 'bcfe4fe305c962ace56ac7b5fc7a08b87d5abd8b7e89027ab251069faebee516b0ded8961445d6d91ec1985dfe30f8153268843c89aa392733d1a3ec956c9978',
'checksum_source': 'https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.tar.gz.sha512',
'executable': 'bin/mvn',
},
]
for package in packages:
archive = downloads / (package['name'] + '.tar.gz')
if not archive.exists():
print('Downloading', package['name'], flush=True)
partial = archive.with_suffix('.part')
with urllib.request.urlopen(package['url'], timeout=90) as response, partial.open('wb') as output:
while chunk := response.read(1024 * 1024):
output.write(chunk)
partial.rename(archive)
digest = hashlib.new(package['algorithm'], archive.read_bytes()).hexdigest()
if digest != package['checksum']:
raise RuntimeError('Checksum mismatch: ' + package['name'])
dest = base / package['name']
if not dest.exists():
staging = base / (package['name'] + '.extract')
staging.mkdir(exist_ok=False)
with tarfile.open(archive) as tar:
tar.extractall(staging, filter='data')
children = list(staging.iterdir())
if len(children) != 1 or not children[0].is_dir():
raise RuntimeError('Unexpected archive layout')
children[0].rename(dest)
staging.rmdir()
if not (dest / package['executable']).is_file():
raise RuntimeError('Incomplete installation: ' + str(dest))
print('Verified and installed', dest, flush=True)
jdk = base / packages[0]['name']
maven = base / packages[1]['name']
env_text = f'export JAVA_HOME="{jdk}"\nexport PATH="{jdk}/bin:{maven}/bin:$PATH"\nexport MAVEN_OPTS="-Xmx1536m -Djava.awt.headless=true"\n'
(base / 'env.sh').write_text(env_text)
(base / 'manifest.json').write_text(json.dumps(packages, indent=2) + '\n')
env = dict(os.environ, JAVA_HOME=str(jdk), PATH=f'{jdk}/bin:{maven}/bin:' + os.environ['PATH'])
subprocess.run([str(jdk / 'bin/java'), '-version'], env=env, check=True)
subprocess.run([str(maven / 'bin/mvn'), '-version'], env=env, check=True)
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail
umask 077
run_dir="${1:?Usage: run-build.sh RUN_DIRECTORY PHASE}"
phase="${2:?Missing build phase}"
case "$phase" in
*[!a-zA-Z0-9_-]*|'') printf 'Invalid phase\n' >&2; exit 2 ;;
esac
source "$HOME/.local/opt/syly2-build/env.sh"
mkdir -p "$run_dir/logs" "$HOME/.local/share/syly2-build/m2"
cd "$run_dir/source"
set +e
nice -n 10 timeout 1200 mvn --batch-mode --no-transfer-progress \
-Dstyle.color=never \
-Dmaven.repo.local="$HOME/.local/share/syly2-build/m2" \
-Dmaven.test.skip=true -DskipTests=true \
clean package > "$run_dir/logs/$phase.log" 2>&1
result=$?
set -e
printf '%s\n' "$result" > "$run_dir/logs/$phase.exit-code"
printf 'Build phase %s exited %s; log: %s\n' "$phase" "$result" "$run_dir/logs/$phase.log"
exit "$result"
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""Verify the WAR, record exact source changes, and compare shared UAT containers."""
import collections
import hashlib
import json
import os
from pathlib import Path
import shutil
import struct
import subprocess
import sys
import zipfile
os.umask(0o077)
run = Path(sys.argv[1]).resolve()
phase = sys.argv[2]
assert (run / 'logs' / (phase + '.exit-code')).read_text().strip() == '0'
log = (run / 'logs' / (phase + '.log')).read_text(errors='replace')
assert 'BUILD SUCCESS' in log
source = run / 'source'
war = source / 'target/syly.war'
required = [
'WEB-INF/web.xml',
'WEB-INF/classes/com/zzc/web/system/controller/core/LoginController.class',
'WEB-INF/classes/com/zzc/core/interceptors/AuthInterceptor.class',
'WEB-INF/classes/com/zzc/web/htoal/controller/HotelMonthlyController.class',
]
with zipfile.ZipFile(war) as archive:
bad = archive.testzip()
assert bad is None, bad
names = archive.namelist()
assert all(name in names for name in required)
classes = [name for name in names if name.startswith('WEB-INF/classes/') and name.endswith('.class')]
major_versions = collections.Counter(struct.unpack('>H', archive.read(name)[6:8])[0] for name in classes)
assert set(major_versions) == {52}, major_versions
libs = [name for name in names if name.startswith('WEB-INF/lib/') and name.endswith('.jar')]
manifest = json.loads((run / 'source-manifest.json').read_text())
changes = []
for relative, expected in manifest.items():
actual = hashlib.sha256((source / relative).read_bytes()).hexdigest()
if expected != actual:
changes.append({'path': relative, 'original_sha256': expected, 'build_sha256': actual})
expected_changes = {
'pom.xml',
'src/main/java/com/zzc/web/htoal/controller/HotelQuarterStatisticController.java',
'src/main/java/com/zzc/web/travel/controller/InlandStatisticPageController.java',
}
assert {change['path'] for change in changes} == expected_changes, changes
ids = subprocess.check_output(['docker', 'ps', '-aq'], text=True).split()
fmt = '{"id":{{json .Id}},"name":{{json .Name}},"startedAt":{{json .State.StartedAt}},"running":{{json .State.Running}},"restartCount":{{json .RestartCount}}}'
after = [json.loads(subprocess.check_output(['docker', 'inspect', '--format', fmt, cid], text=True)) for cid in ids]
before = json.loads((run / 'containers-before.json').read_text())
key = lambda values: {value['id']: value for value in values}
unchanged = key(before) == key(after)
(run / 'containers-after.json').write_text(json.dumps(after, indent=2) + '\n')
artifact = run / 'artifacts/syly.war'
shutil.copyfile(war, artifact)
artifact.chmod(0o600)
evidence = {
'host': '192.168.68.223', 'run': str(run), 'phase': phase, 'build_success': True,
'artifact': str(artifact), 'artifact_bytes': artifact.stat().st_size,
'artifact_sha256': hashlib.sha256(artifact.read_bytes()).hexdigest(),
'zip_crc_check': 'passed', 'required_entries_present': required,
'compiled_class_files': len(classes), 'class_major_versions': dict(major_versions), 'bundled_jars': len(libs),
'main_java_sources': len(list((source / 'src/main/java').rglob('*.java'))),
'extra_java_sources_outside_maven_root': len(list((source / 'src/main/com').rglob('*.java'))),
'source_manifest_files': len(manifest), 'changed_source_files': changes,
'tests_skipped': 'Tests are skipped' in log,
'containers_before': len(before), 'containers_after': len(after), 'existing_containers_unchanged': unchanged,
'built_at': next((line.split('Finished at:', 1)[1].strip() for line in log.splitlines() if 'Finished at:' in line), None),
'elapsed': next((line.split('Total time:', 1)[1].strip() for line in log.splitlines() if 'Total time:' in line), None),
'java_version': subprocess.check_output([str(Path.home() / '.local/opt/syly2-build/corretto-8.512.09.1/bin/java'), '-version'], stderr=subprocess.STDOUT, text=True).strip(),
}
(run / 'artifacts/verification.json').write_text(json.dumps(evidence, indent=2) + '\n')
print(json.dumps(evidence, indent=2))
if not unchanged:
print('NOTICE: Container inventory changed during the build; inspect before/after records.')
+6
View File
@@ -0,0 +1,6 @@
FROM syly2-uat-base:20261029
COPY java/ /opt/java/
COPY tomcat/ /opt/tomcat/
ENV JAVA_HOME=/opt/java CATALINA_HOME=/opt/tomcat CATALINA_BASE=/opt/tomcat PATH=/opt/java/bin:$PATH
USER 1000:1000
CMD ["/opt/tomcat/bin/catalina.sh", "run"]
+4
View File
@@ -0,0 +1,4 @@
FROM docker.jianmuhub.com/library/ubuntu@sha256:98f43fe45e1073f27722eb362248541a6c90599b5ac458de28cee309d92a7199
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends libaio1 libnuma1 libncurses5 libtinfo5 libfontconfig1 fonts-dejavu-core tzdata ca-certificates && rm -rf /var/lib/apt/lists/*
ENV TZ=Asia/Shanghai LANG=C.UTF-8
RUN groupadd -g 1000 syly && useradd -u 1000 -g 1000 -M syly
+5
View File
@@ -0,0 +1,5 @@
FROM syly2-uat-base:20261029
COPY mysql/ /opt/mysql/
ENV PATH=/opt/mysql/bin:$PATH
USER 1000:1000
ENTRYPOINT ["/opt/mysql/bin/mysqld", "--defaults-file=/etc/mysql-uat.cnf"]
+16
View File
@@ -0,0 +1,16 @@
import java.net.*;
public class EgressProbe {
public static void main(String[] args) throws Exception {
String[] hosts = {"db", "10.5.34.202", "1.1.1.1"};
int[] ports = {3306, 3306, 443};
for (int i=0;i<hosts.length;i++) {
boolean connected=false;
try (Socket socket=new Socket()) {
socket.connect(new InetSocketAddress(hosts[i],ports[i]),1500);
connected=true;
} catch(Exception expected) {}
System.out.println(hosts[i]+":"+ports[i]+"="+(connected?"connected":"blocked"));
if ((i==0)!=connected) throw new IllegalStateException("Unexpected UAT network policy");
}
}
}
+63
View File
@@ -0,0 +1,63 @@
> 历史记录:以下说明对应 2026-10-29 原始 UAT 构建及部署,当前仓库的构建入口和密钥配置请看根目录 README。
# syly2 独立 UAT 部署
目标:`ubuntu@192.168.68.223`。部署目录:`/home/ubuntu/apps/syly2-uat`。
## 组成
- 专用 Compose 项目 `syly2-uat`,数据库、Java 应用和 Nginx 各一个容器。
- MySQL **5.7.44**,与用户提供的线上版本一致;独立数据目录 `runtime/mysql-data`,不发布数据库端口。
- Corretto **8.512.09.1**、Tomcat **9.0.123**;使用已编译并校验的外层工程 WAR。
- Ubuntu 基础镜像和 Nginx 来自建木镜像仓库,固定 digest;MySQL 和 Tomcat 从厂商官方 HTTPS 地址下载。
- MySQL 官方包核验官方 MD5,再记录 SHA-256。`package-mysql.py` 从已验证的包提取必要运行文件,避免重复传输调试库、静态库和工具。运行包再核对 SHA-256。Tomcat 核验官方 SHA-512。
## 数据及边界
使用项目根目录的 `sanya_ch_full.sql`,128,389,799 字节。仅进行流式校验、传输、导入;不把备份正文写入报告。
原备份 SHA-256:`d6ec01e162f4b1c92350a0c97bd163b5deb633cdd187f96ff2aadb32e899c5d4`。
备份恢复到此项目自己的 MySQL 实例内,库名仍为 `sanya_ch`。部署脚本只允许首次恢复,拒绝覆盖已有 UAT 数据。恢复后保存所有表的精确行数,不输出用户或业务行。
创建专用 `uat_admin` 用户及测试组织、角色,保留恢复数据中的原账号及密码。部署过滤器仅允许专用测试用户登录。密码保存在权限 600 的 `secrets/credentials.json`,不写入 Compose 环境变量、日志或报告。
应用与数据库只连接 `internal` 网络。省级上报和邮件配置改为本地禁用地址,停用定时调度;浏览器 CSP 限制页面向外部地址请求。UAT 过滤器阻断免登录重置密码、修复清库、移动端绕过登录、同步、上传连接器及 Druid 入口,并对其余非公开资源检查 UAT 登录状态。
这些保护仅作用于部署副本,不能替代原系统安全修复。Web 应用文件以只读方式挂载,附件上传功能未在本次部署中开放。备份数据属于受限资料,UAT 不用于真实省级上报、短信或邮件发送。
## 首次部署顺序
脚本固定目标目录;不要在已有实例上重新运行初始化/恢复。
1. 保存原有容器 ID、启动时间、重启计数;检查端口和空间。建立权限 700 的部署目录及私有子目录。
2. 上传脚本、Compose、Nginx 配置以及 SQL 备份;在服务端再次核验 SQL SHA-256。
3. 下载官方发行包并校验。执行 `package-mysql.py`,上传生成的运行包和 manifest。
4. 构建 `Dockerfile.base`,执行 `prepare-runtime.py`,构建 `Dockerfile.mysql` 和 `Dockerfile.app`。
5. 使用 `docker compose -f compose.json run --rm --no-deps db --initialize-insecure` 初始化空数据目录;此时配置仍为 `skip_networking=ON`。
6. 只启动 `db`。执行 `database-uat.py restore`,核对版本、表数及精确行数,再执行 `database-uat.py seed` 设置新数据库密码与测试账号。
7. 将 `runtime/mysql.cnf` 中 `skip_networking=ON` 改为 `OFF`,只重建本项目 `db` 服务,启动 `app` 和 `web`。
8. 检查应用启动、登录页、验证码、登录、菜单和列表;验证阻断规则、外连隔离,并比较原有服务状态。
## 访问与运维
- 业务入口:`https://192.168.68.223:8445/syly/`。
- 本实例使用独立自签名证书,公钥证书在 `secrets/tls/server.crt`。浏览器首次访问可能提示证书不受信任。
- 直接访问上述 UAT HTTPS 地址,不需要 SSH 隧道。验证过程中使用的临时隧道及回环 HTTP 入口已移除。
```sh
cd /home/ubuntu/apps/syly2-uat
docker compose -f compose.json ps
docker compose -f compose.json logs --tail 100 app
docker compose -f compose.json up -d
```
只暂停本应用并保留恢复数据:
```sh
docker compose -f compose.json stop web app db
```
恢复运行使用上面的 `up -d`。不要全局 prune,也不要对其他 Compose 项目执行停启、清理或重建。
证据位于远程 `evidence/`;本地核验结果回收到项目 `.local/uat-deploy/evidence/`。密码另存私有交接文件。启动与页面验证不代表生产验收、全量报表正确性、历史附件完整性或外部接口联调通过。
+46
View File
@@ -0,0 +1,46 @@
package uat;
import java.io.IOException;
import java.util.Locale;
import javax.servlet.*;
import javax.servlet.http.*;
/** Deployment-only boundary for the isolated restored UAT copy. */
public class UatGuardFilter implements Filter {
public void init(FilterConfig config) {}
public void destroy() {}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
String path = req.getServletPath().toLowerCase(Locale.ROOT);
res.setHeader("X-Syly-Environment", "isolated-uat");
res.setHeader("X-Content-Type-Options", "nosniff");
if (path.contains("appservice") || path.contains("repaircontroller") || path.contains("dataupdatecontroller")
|| path.contains("druid") || path.equals("/rest") || path.startsWith("/rest/") || path.contains("connector.java")
|| path.contains("filesuploadcontroller") || path.contains("/ueditor/jsp/")
|| (path.contains("usercontroller") && req.getParameterMap().containsKey("savenewpwdforuser"))) {
res.sendError(403, "Disabled in isolated UAT"); return;
}
boolean login = path.equals("/logincontroller.do") || path.equals("/logincontroller.action");
if (login && req.getParameterMap().containsKey("checkuser")
&& !"uat_admin".equals(req.getParameter("userName"))) {
res.setContentType("application/json;charset=UTF-8");
res.getWriter().write("{\"success\":false,\"msg\":\"UAT permits only the dedicated test account\"}"); return;
}
boolean publicPath = (login && (req.getParameterMap().containsKey("login") || req.getParameterMap().containsKey("checkuser")))
|| path.equals("/randcodeimage") || path.equals("/") || path.equals("")
|| path.equals("/webpage/login/login.jsp")
|| ((path.startsWith("/plug-in/") || path.startsWith("/style/") || path.startsWith("/images/") || path.startsWith("/css/") || path.startsWith("/js/"))
&& path.matches(".*\\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|swf|map)$"));
if (!publicPath) {
HttpSession session = req.getSession(false);
com.zzc.web.system.pojo.base.Client client = session == null ? null :
com.zzc.web.system.manager.ClientManager.getInstance().getClient(session.getId());
if (client == null || client.getUser() == null || !"uat_admin".equals(client.getUser().getUserName())) {
res.sendError(401, "UAT login required"); return;
}
}
chain.doFilter(request, response);
}
}
+60
View File
@@ -0,0 +1,60 @@
{
"name": "syly2-uat",
"services": {
"db": {
"image": "syly2-uat-mysql:5.7.44-20261029",
"pull_policy": "never",
"restart": "unless-stopped",
"user": "1000:1000",
"read_only": true,
"cap_drop": ["ALL"],
"security_opt": ["no-new-privileges:true"],
"mem_limit": "1g",
"cpus": 1.5,
"pids_limit": 200,
"volumes": ["./runtime/mysql-data:/var/lib/mysql", "./runtime/mysql.cnf:/etc/mysql-uat.cnf:ro", "./secrets/mysql-client.cnf:/run/secrets/mysql-client.cnf:ro"],
"tmpfs": ["/tmp:uid=1000,gid=1000,mode=1777,size=128m"],
"networks": ["backend"],
"healthcheck": {"test": ["CMD", "/opt/mysql/bin/mysqladmin", "--defaults-extra-file=/run/secrets/mysql-client.cnf", "ping", "--silent"], "interval": "10s", "timeout": "5s", "retries": 12},
"logging": {"driver":"json-file", "options":{"max-size":"10m","max-file":"3"}}
},
"app": {
"image": "syly2-uat-app:tomcat9.0.123-java8-20261029",
"pull_policy": "never",
"restart": "unless-stopped",
"user": "1000:1000",
"read_only": true,
"cap_drop": ["ALL"],
"security_opt": ["no-new-privileges:true"],
"mem_limit": "2g",
"cpus": 2,
"pids_limit": 300,
"environment": {"CATALINA_OPTS":"-Xms256m -Xmx1280m -XX:MaxMetaspaceSize=384m -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai"},
"volumes": ["./runtime/webapp:/opt/tomcat/webapps/syly:ro", "./runtime/server.xml:/opt/tomcat/conf/server.xml:ro", "./runtime/tomcat-work:/opt/tomcat/work", "./runtime/tomcat-logs:/opt/tomcat/logs"],
"tmpfs": ["/tmp:uid=1000,gid=1000,mode=1777,size=128m", "/opt/tomcat/temp:uid=1000,gid=1000,mode=1777,size=128m", "/opt/tomcat/conf/Catalina:uid=1000,gid=1000,mode=0755,size=8m"],
"networks": ["backend"],
"depends_on": {"db":{"condition":"service_healthy"}},
"logging": {"driver":"json-file", "options":{"max-size":"10m","max-file":"3"}}
},
"web": {
"image": "docker.jianmuhub.com/library/nginx@sha256:04ac43f568bd51a2e8f37fa0a185ff8f9f4d55c3aa1f1af5008f094d026f950d",
"pull_policy": "never",
"entrypoint": ["nginx", "-g", "daemon off;"],
"user": "1000:1000",
"restart": "unless-stopped",
"read_only": true,
"cap_drop": ["ALL"],
"security_opt": ["no-new-privileges:true"],
"mem_limit": "128m",
"cpus": 0.5,
"pids_limit": 50,
"ports": ["192.168.68.223:8445:8445"],
"volumes": ["./nginx.conf:/etc/nginx/nginx.conf:ro", "./secrets/tls:/etc/nginx/tls:ro"],
"tmpfs": ["/var/cache/nginx:uid=1000,gid=1000", "/var/run:uid=1000,gid=1000", "/tmp:uid=1000,gid=1000"],
"networks": ["backend", "ingress"],
"depends_on": ["app"],
"logging": {"driver":"json-file", "options":{"max-size":"5m","max-file":"2"}}
}
},
"networks": {"backend":{"internal":true}, "ingress":{}}
}
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""Restore the user-supplied dump only to the new isolated MySQL container."""
import json, os, subprocess, sys, time
from pathlib import Path
os.umask(0o077)
ROOT=Path('/home/ubuntu/apps/syly2-uat')
DC=['docker','compose','-f',str(ROOT/'compose.json')]
cid=subprocess.check_output(DC+['ps','-q','db'],text=True).strip()
assert cid
label=subprocess.check_output(['docker','inspect','--format','{{index .Config.Labels "com.docker.compose.project"}}',cid],text=True).strip()
assert label=='syly2-uat'
phase=sys.argv[1]
cmd=['docker','exec','-i',cid,'/opt/mysql/bin/mysql','--max-allowed-packet=512M','--default-character-set=utf8mb4','-uroot','-N','-B']
if phase=='inventory': cmd.insert(-4,'--defaults-extra-file=/run/secrets/mysql-client.cnf')
def query(sql):
p=subprocess.run(cmd,input=sql,text=True,capture_output=True)
if p.returncode:
(ROOT/'evidence/db-last-error.log').write_text(p.stderr)
raise RuntimeError('SQL failed; private error log saved')
return p.stdout.strip()
if phase=='restore':
assert not (ROOT/'evidence/restore-complete.json').exists(), 'Refusing repeated restore over existing UAT'
assert query("SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name='sanya_ch'")=='0'
started=time.time()
with (ROOT/'incoming/sanya_ch_full.sql').open('rb') as inp,(ROOT/'evidence/restore.log').open('wb') as log:
p=subprocess.run(cmd,stdin=inp,stdout=log,stderr=log)
if p.returncode: raise RuntimeError('Restore failed; inspect private log')
tables=query("SELECT table_name FROM information_schema.tables WHERE table_schema='sanya_ch' AND table_type='BASE TABLE' ORDER BY table_name").splitlines()
counts={t:int(query('SELECT COUNT(*) FROM sanya_ch.`'+t+'`')) for t in tables}
info={'mysql_version':query('SELECT VERSION()'),'tables':len(tables),'rows':sum(counts.values()),'row_counts':counts,'seconds':round(time.time()-started,1)}
(ROOT/'evidence/restore-complete.json').write_text(json.dumps(info,indent=2))
cols=query("SELECT column_name FROM information_schema.columns WHERE table_schema='sanya_ch' AND table_name='t_s_base_user'").splitlines()
(ROOT/'evidence/base-user-columns.json').write_text(json.dumps(cols))
print(json.dumps({k:v for k,v in info.items() if k!='row_counts'}))
elif phase=='seed':
assert (ROOT/'evidence/restore-complete.json').exists()
assert query("SELECT COUNT(*) FROM sanya_ch.t_s_base_user WHERE username='uat_admin'")=='0', 'Refusing to change an existing account'
creds=json.loads((ROOT/'secrets/credentials.json').read_text())
jdk='/home/ubuntu/.local/opt/syly2-build/corretto-8.512.09.1/bin/'
helper=ROOT/'scripts/HashUat.java'
helper.write_text('import java.io.*; import org.mindrot.jbcrypt.BCrypt; public class HashUat { public static void main(String[] a) throws Exception { String p=new BufferedReader(new InputStreamReader(System.in)).readLine(); System.out.print(BCrypt.hashpw(p,BCrypt.gensalt(12))); }}')
cp=str(ROOT/'runtime/webapp/WEB-INF/lib/*')
subprocess.run([jdk+'javac','-cp',cp,str(helper)],check=True)
hashed=subprocess.check_output([jdk+'java','-cp',str(ROOT/'scripts')+':'+cp,'HashUat'],input=creds['uat_password'],text=True)
columns=json.loads((ROOT/'evidence/base-user-columns.json').read_text())
added=[]
if 'password_update_date' not in columns:
query('ALTER TABLE sanya_ch.t_s_base_user ADD COLUMN password_update_date datetime NULL;')
added.append('t_s_base_user.password_update_date')
# All IDs belong to this synthetic UAT fixture, never to a restored account.
uid='01a186a4e1407e328d2bfe6687e68b01'
org='01a186a4e1407e328d2bfe6687e68b02'
role='01a186a4e1407e328d2bfe6687e68b03'
sql=f"""USE sanya_ch;
START TRANSACTION;
INSERT INTO t_s_depart(ID,departname,description,org_code,org_type) VALUES('{org}','UAT验证组织','仅用于隔离测试','UAT','1');
INSERT INTO t_s_base_user(ID,username,password,realname,status,activitiSync,area,login_fail_count,is_locked,password_update_date) VALUES('{uid}','uat_admin','{hashed}','UAT验证管理员',1,0,'460200',0,0,NOW());
INSERT INTO t_s_user(id,create_date,create_by,create_name) VALUES('{uid}',NOW(),'uat-deploy','UAT deployment');
INSERT INTO t_s_user_org(ID,user_id,org_id) VALUES(REPLACE(UUID(),'-',''),'{uid}','{org}');
INSERT INTO t_s_role(ID,rolecode,rolename) VALUES('{role}','uat_admin','UAT验证角色');
INSERT INTO t_s_role_user(ID,roleid,userid) VALUES(REPLACE(UUID(),'-',''),'{role}','{uid}');
INSERT INTO t_s_role_function(ID,functionid,roleid) SELECT REPLACE(UUID(),'-',''),ID,'{role}' FROM t_s_function;
COMMIT;
CREATE USER 'syly_uat'@'%' IDENTIFIED BY '{creds['mysql_app']}';
GRANT SELECT, INSERT, UPDATE, DELETE, EXECUTE, SHOW VIEW ON sanya_ch.* TO 'syly_uat'@'%';
ALTER USER 'root'@'localhost' IDENTIFIED BY '{creds['mysql_root']}';
"""
query(sql)
(ROOT/'evidence/uat-seed.json').write_text(json.dumps({'username':'uat_admin','user_id':uid,'org_id':org,'role_id':role,'added_columns':added,'existing_account_passwords_changed':False,'business_rows_changed':False},indent=2))
print('Dedicated UAT account added; original accounts preserved; DB credentials initialized.')
else:
raise ValueError(phase)
+33
View File
@@ -0,0 +1,33 @@
worker_processes 1;
pid /var/run/nginx.pid;
events { worker_connections 256; }
http {
include /etc/nginx/mime.types;
server_tokens off;
access_log off;
error_log /dev/stderr warn;
client_max_body_size 10m;
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
server {
listen 8445 ssl;
server_name _;
absolute_redirect off;
ssl_certificate /etc/nginx/tls/server.crt;
ssl_certificate_key /etc/nginx/tls/server.key;
ssl_protocols TLSv1.2 TLSv1.3;
add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; connect-src 'self'; form-action 'self'; frame-src 'self'; base-uri 'self'" always;
add_header X-Frame-Options SAMEORIGIN always;
location = / { return 302 /syly/; }
location = /syly/ { return 302 /syly/loginController.do?login; }
location /syly/ {
proxy_pass http://app:8080;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 8445;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
}
location / { return 404; }
}
}
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env python3
"""Verify the official archive and transfer only required runtime files."""
import hashlib,json,re,tarfile
from pathlib import Path
root=Path(__file__).resolve().parents[1]/'.local/uat-deploy'
archive=root/'mysql-5.7.44.tar.gz'
def hashfile(p, alg):
h=hashlib.new(alg)
with p.open('rb') as f:
for block in iter(lambda:f.read(1024*1024),b''):h.update(block)
return h.hexdigest()
expected=re.search(r'\b[0-9a-fA-F]{32}\b',(root/'mysql-5.7.44.tar.gz.md5').read_text()).group().lower()
assert hashfile(archive,'md5')==expected
out=root/'mysql-5.7.44-runtime.tar.gz'
files=[]
with tarfile.open(archive,'r|gz') as src,tarfile.open(out,'w:gz',compresslevel=6) as dst:
for m in src:
rel=m.name.split('/',1)[1] if '/' in m.name else ''
take=rel in ['bin/mysqld','bin/mysql','bin/mysqladmin','bin/mysqldump'] or rel.startswith(('lib/','share/'))
if take and '/debug/' not in rel and not rel.endswith('.a'):
assert '..' not in Path(rel).parts
dst.addfile(m,src.extractfile(m) if m.isfile() else None)
files.append({'path':rel,'bytes':m.size})
manifest={'source_url':'https://cdn.mysql.com/archives/mysql-5.7/mysql-5.7.44-linux-glibc2.12-x86_64.tar.gz','source_md5_verified':expected,'source_sha256':hashfile(archive,'sha256'),'runtime_sha256':hashfile(out,'sha256'),'runtime_bytes':out.stat().st_size,'files':files}
(root/'mysql-runtime-manifest.json').write_text(json.dumps(manifest,indent=2))
print(json.dumps({k:v for k,v in manifest.items() if k!='files'},indent=2))
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""Prepare only the syly2 UAT directory. Never prints credentials or dump rows."""
import hashlib, json, os, re, secrets, shutil, subprocess, sys, tarfile, zipfile
from pathlib import Path
os.umask(0o077)
ROOT = Path('/home/ubuntu/apps/syly2-uat')
assert not (ROOT/'evidence/restore-complete.json').exists(), 'Preparation is for a fresh deployment only; refusing to overwrite initialized UAT configuration'
JDK = Path('/home/ubuntu/.local/opt/syly2-build/corretto-8.512.09.1')
WAR = Path('/home/ubuntu/builds/syly2/20261029-01/artifacts/syly.war')
def digest(path, alg='sha256'):
h = hashlib.new(alg)
with path.open('rb') as f:
for chunk in iter(lambda: f.read(1024*1024), b''): h.update(chunk)
return h.hexdigest()
def write(path, text, mode=0o600):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text)
path.chmod(mode)
assert digest(WAR) == '7c1e4236d32f2d2106df738d2523f08588d73eab7f70089382e3ae5e7c827e76'
dump = ROOT/'incoming/sanya_ch_full.sql'
assert digest(dump) == 'd6ec01e162f4b1c92350a0c97bd163b5deb633cdd187f96ff2aadb32e899c5d4'
dump.chmod(0o600)
mysql = ROOT/'downloads/mysql-5.7.44-runtime.tar.gz'
app_only = '--app-only' in sys.argv
if not app_only:
mysql_manifest=json.loads((ROOT/'downloads/mysql-runtime-manifest.json').read_text())
assert mysql_manifest['source_md5_verified']
assert digest(mysql) == mysql_manifest['runtime_sha256']
tomcat = ROOT/'downloads/apache-tomcat-9.0.123.tar.gz'
assert digest(tomcat, 'sha512') == (ROOT/'downloads/apache-tomcat-9.0.123.tar.gz.sha512').read_text().split()[0]
ctx = ROOT/'image-context'
ctx.mkdir(exist_ok=True)
for archive, dest, allowed in [(mysql, ctx/'mysql', ('bin/', 'lib/', 'share/')), (tomcat, ctx/'tomcat', ('bin/', 'lib/', 'conf/'))]:
if archive == mysql and app_only: continue
if not dest.exists():
dest.mkdir()
with tarfile.open(archive, 'r|gz') as tf:
for m in tf:
name = m.name.split('/',1)[1] if '/' in m.name else ''
if name.startswith(allowed) and '/debug/' not in name and not name.endswith('-debug'):
assert '..' not in Path(name).parts
m.name = name
tf.extract(m, dest)
if not (ctx/'java').exists(): shutil.copytree(JDK, ctx/'java', symlinks=True)
for f in ['Dockerfile.mysql','Dockerfile.app']:
shutil.copy2(ROOT/'scripts'/f,ctx/f)
for p in [ctx/'tomcat/webapps',ctx/'tomcat/work',ctx/'tomcat/temp',ctx/'tomcat/logs']:
p.mkdir(exist_ok=True)
p.chmod(0o755)
# Image files must be traversable by the runtime uid.
for base, dirs, files in os.walk(ctx):
Path(base).chmod(0o755)
for file in files:
p = Path(base)/file
if not p.is_symlink(): p.chmod(0o755 if p.stat().st_mode & 0o111 else 0o644)
secret_file = ROOT/'secrets/credentials.json'
if not secret_file.exists():
creds = {k:secrets.token_urlsafe(24) for k in ['mysql_root','mysql_app','uat_password']}
creds['uat_username']='uat_admin'
write(secret_file,json.dumps(creds,indent=2))
creds=json.loads(secret_file.read_text())
write(ROOT/'secrets/mysql-client.cnf','[client]\nuser=root\npassword='+creds['mysql_root']+'\nsocket=/tmp/mysql.sock\n')
write(ROOT/'runtime/mysql.cnf','''[mysqld]
basedir=/opt/mysql
datadir=/var/lib/mysql
socket=/tmp/mysql.sock
pid-file=/tmp/mysql.pid
port=3306
bind-address=0.0.0.0
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
lower_case_table_names=1
sql_mode=NO_ENGINE_SUBSTITUTION
max_allowed_packet=512M
innodb_buffer_pool_size=384M
max_connections=60
event_scheduler=OFF
local_infile=OFF
secure_file_priv=NULL
skip_name_resolve=ON
skip_networking=ON
''')
webapp=ROOT/'runtime/webapp'
if not webapp.exists():
webapp.mkdir()
with zipfile.ZipFile(WAR) as z: z.extractall(webapp)
classes=webapp/'WEB-INF/classes'
db='''hibernate.dialect=org.hibernate.dialect.MySQLDialect
validationQuery.sqlserver=SELECT 1
jdbc.url=jdbc:mysql://db:3306/sanya_ch?useUnicode=true&characterEncoding=UTF-8&useSSL=false&connectTimeout=5000&socketTimeout=30000
jdbc.username=syly_uat
jdbc.password='''+creds['mysql_app']+'''
jdbc.dbType=mysql
hibernate.hbm2ddl.auto=none
scenicWebServiceBase=http://127.0.0.1:9
hotelWebServiceBase=http://127.0.0.1:9
hotelProviceurl=http://127.0.0.1:9
scenicProvice=http://127.0.0.1:9
hotelIp=127.0.0.1
'''
write(classes/'dbconfig.properties',db)
write(classes/'spring-mvc-timeTask.xml','''<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="schedulerFactory" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"><property name="autoStartup" value="false"/></bean>
</beans>''')
write(classes/'log4j.properties','''log4j.rootLogger=WARN,A1
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=[%p][%c]%m%n
log4j.logger.org.hibernate.type.descriptor.sql.BasicBinder=OFF
log4j.logger.org.hibernate.type.descriptor.sql.BasicExtractor=OFF
log4j.logger.org.hibernate.engine.QueryParameters=OFF
''')
p=classes/'spring-mvc-hibernate.xml';s=p.read_text();s=s.replace('<prop key="hibernate.show_sql">true</prop>','<prop key="hibernate.show_sql">false</prop>');s=re.sub(r'<property name="maxIdle" value="20"\s*/>','',s);write(p,s)
p=classes/'sysConfig.properties';s=p.read_text();s=re.sub(r'^mail\.[^=]+=.*$',lambda m: m.group().split('=')[0]+'=',s,flags=re.M);s=re.sub(r'^ck.userfiles=.*$', 'ck.userfiles=/syly/upload/',s,flags=re.M);write(p,s)
# Remove alternate embedded database credentials in the runtime copy.
for p in (classes/'zzc').glob('zzc_database.properties*'):
write(p,'# Disabled in isolated UAT; runtime uses dbconfig.properties\n')
p=webapp/'WEB-INF/web.xml';s=p.read_text()
s=s.replace('<url-pattern>*</url-pattern>','<url-pattern>/*</url-pattern>')
if 'uatGuard' not in s:
start=s.index('>',s.index('<web-app'))+1
s=s[:start]+'''\n<filter><filter-name>uatGuard</filter-name><filter-class>uat.UatGuardFilter</filter-class></filter>
<filter-mapping><filter-name>uatGuard</filter-name><url-pattern>/*</url-pattern><dispatcher>REQUEST</dispatcher></filter-mapping>\n'''+s[start:]
# Eliminate the unused Druid servlet and its embedded credentials.
s=re.sub(r'<servlet>\s*<servlet-name>druidStatView</servlet-name>.*?</servlet>','',s,flags=re.S)
s=re.sub(r'<servlet-mapping>\s*<servlet-name>druidStatView</servlet-name>.*?</servlet-mapping>','',s,flags=re.S)
write(p,s)
login=webapp/'webpage/login/login.jsp'
text=login.read_text()
if 'isolated-uat-banner' not in text:
text=text.replace('<title>','<title>[UAT] ')
text=re.sub(r'(<body[^>]*>)',r'\1<div id="isolated-uat-banner" style="position:fixed;top:0;left:0;right:0;z-index:9999;text-align:center;background:#fff3cd;color:#664d03;padding:6px">UAT 隔离测试环境 · 数据来自备份 · 外部上报已禁用</div>',text,count=1)
write(login,text)
cp=str(classes)+':'+str(webapp/'WEB-INF/lib/*')+':'+str(ctx/'tomcat/lib/servlet-api.jar')
subprocess.run([str(JDK/'bin/javac'),'-encoding','UTF-8','-cp',cp,'-d',str(classes),str(ROOT/'scripts/UatGuardFilter.java')],check=True)
write(ROOT/'runtime/server.xml','''<Server port="-1"><Service name="Catalina">
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" URIEncoding="UTF-8" maxThreads="80" />
<Engine name="Catalina" defaultHost="localhost"><Host name="localhost" appBase="webapps" unpackWARs="false" autoDeploy="false">
<Valve className="org.apache.catalina.valves.RemoteIpValve" internalProxies=".*" remoteIpHeader="x-forwarded-for" protocolHeader="x-forwarded-proto" portHeader="x-forwarded-port" hostHeader="x-forwarded-host"/>
</Host></Engine></Service></Server>''')
for p in [ROOT/'runtime/mysql-data', ROOT/'runtime/tomcat-work', ROOT/'runtime/tomcat-logs']:
p.mkdir(exist_ok=True)
tls=ROOT/'secrets/tls';tls.mkdir(exist_ok=True)
if not (tls/'server.crt').exists():
subprocess.run(['openssl','req','-x509','-newkey','rsa:3072','-nodes','-keyout',str(tls/'server.key'),'-out',str(tls/'server.crt'),'-days','365','-subj','/CN=syly2-uat','-addext','subjectAltName=IP:192.168.68.223,DNS:localhost'],check=True,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
write(ROOT/'evidence/runtime-manifest.json',json.dumps({'war_sha256':digest(WAR),'dump_sha256':digest(dump),'dump_bytes':dump.stat().st_size,'mysql_tar_sha256':None if app_only else digest(mysql),'mysql_vendor_md5_verified':not app_only,'tomcat_tar_sha512_verified':True,'tomcat':'9.0.123','java':'Corretto 8.512.09.1','mysql':'5.7.44'},indent=2))
print('Runtime prepared; hashes verified; credentials kept in private files.')
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""No business mutations; inspect only status and non-sensitive response shape."""
import json, ssl, urllib.request, urllib.error
from pathlib import Path
ROOT=Path(__file__).resolve().parents[1]
evidence=ROOT/'.local/uat-deploy/evidence'
ctx=ssl.create_default_context(cafile=str(evidence/'server.crt'))
base='https://192.168.68.223:8445/syly/'
results=[]
cases=[
('login-page','',200,'formLogin'),
('login-controller','loginController.do?login',200,'formLogin'),
('captcha','randCodeImage',200,None),
('anonymous-business','hotelController.do?datagrid',401,None),
('repair-disabled','repairController.do?uat_probe',403,None),
('password-reset-disabled','userController.do?savenewpwdforuser',403,None),
('mobile-disabled','appservice.do?uat_probe',403,None),
('sync-disabled','hoteldataupdatecontroller.do?uat_probe',403,None),
('druid-disabled','webpage/system/druid/index.html',403,None),
('upload-disabled','plug-in/ckfinder/core/connector/java/connector.java',403,None),
('rest-disabled','rest/uat_probe',403,None),
('backup-account-login-disabled','loginController.do?checkuser&userName=admin',200,'UAT permits only'),
]
for name,path,expected,marker in cases:
req=urllib.request.Request(base+path,headers={'User-Agent':'syly2-uat-deployment-verification'})
try: res=urllib.request.urlopen(req,context=ctx,timeout=45)
except urllib.error.HTTPError as e: res=e
body=res.read(1024*1024)
ok=res.status==expected and (marker is None or marker.encode() in body)
if name=='captcha': ok=ok and body.startswith(b'\xff\xd8')
results.append({'check':name,'status':res.status,'expected_status':expected,'passed':ok,'bytes':len(body)})
(evidence/'http-checks.json').write_text(json.dumps(results,indent=2))
print(json.dumps(results,indent=2))
assert all(x['passed'] for x in results)
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
import json,subprocess
from pathlib import Path
root=Path('/home/ubuntu/apps/syly2-uat')
fmt='{"id":{{json .Id}},"name":{{json .Name}},"startedAt":{{json .State.StartedAt}},"running":{{json .State.Running}},"restartCount":{{json .RestartCount}}}'
before=[json.loads(x) for x in (root/'evidence/containers-before.jsonl').read_text().splitlines()]
after=[]
for old in before:
after.append(json.loads(subprocess.check_output(['docker','inspect','--format',fmt,old['id']],text=True)))
images={}
runtime=[]
for service in ['db','app','web']:
cid=subprocess.check_output(['docker','compose','-f',str(root/'compose.json'),'ps','-q',service],text=True).strip()
assert cid, service
state=json.loads(subprocess.check_output(['docker','inspect','--format',fmt,cid],text=True))
state['service']=service
state['image']=subprocess.check_output(['docker','inspect','--format','{{.Image}}',cid],text=True).strip()
state['network_names']=json.loads(subprocess.check_output(['docker','inspect','--format','{{json .NetworkSettings.Networks}}',cid],text=True)).keys()
state['network_names']=list(state['network_names'])
runtime.append(state)
info={'original_container_count':len(before),'original_containers_unchanged':before==after,'runtime':runtime}
(root/'evidence/containers-after.json').write_text(json.dumps(after,indent=2))
(root/'evidence/runtime-verification.json').write_text(json.dumps(info,indent=2))
print(json.dumps(info,indent=2))
assert before==after
assert all(x['running'] for x in runtime)
assert all(x['network_names']==['syly2-uat_backend'] for x in runtime if x['service'] in ['app','db'])