问题修复
This commit is contained in:
@@ -668,6 +668,7 @@ async def _render_script(domain: str, server_name: str, os: str, db: AsyncSessio
|
|||||||
cert_dir=d.cert_dir,
|
cert_dir=d.cert_dir,
|
||||||
check_cmd=d.check_cmd,
|
check_cmd=d.check_cmd,
|
||||||
reload_cmd=d.reload_cmd,
|
reload_cmd=d.reload_cmd,
|
||||||
|
acme_config_id=d.acme_config_id or "",
|
||||||
)
|
)
|
||||||
return script, server, d
|
return script, server, d
|
||||||
|
|
||||||
|
|||||||
+27
-31
@@ -28,19 +28,30 @@ async def verify_token(
|
|||||||
raise HTTPException(status_code=401, detail="Invalid authorization header")
|
raise HTTPException(status_code=401, detail="Invalid authorization header")
|
||||||
token = authorization[7:]
|
token = authorization[7:]
|
||||||
result = await db.execute(select(Server).where(Server.token == token))
|
result = await db.execute(select(Server).where(Server.token == token))
|
||||||
server = result.scalar_one_or_none()
|
server = result.scalars().first()
|
||||||
if not server:
|
if not server:
|
||||||
raise HTTPException(status_code=401, detail="Invalid token")
|
raise HTTPException(status_code=401, detail="Invalid token")
|
||||||
return server
|
return server
|
||||||
|
|
||||||
|
|
||||||
@router.get("/version")
|
async def _find_domain(db: AsyncSession, domain: str, server_id: int | None = None, acme_config_id: int | None = None) -> Domain:
|
||||||
async def get_version(domain: str, db: AsyncSession = Depends(get_db)):
|
"""根据条件查找域名记录,acme_config_id 精确匹配"""
|
||||||
"""返回当前证书版本号(无需认证,方便客户端轻量检查)"""
|
conditions = [Domain.domain == domain]
|
||||||
result = await db.execute(select(Domain).where(Domain.domain == domain))
|
if server_id is not None:
|
||||||
d = result.scalar_one_or_none()
|
conditions.append(Domain.server_id == server_id)
|
||||||
|
if acme_config_id is not None:
|
||||||
|
conditions.append(Domain.acme_config_id == acme_config_id)
|
||||||
|
result = await db.execute(select(Domain).where(*conditions))
|
||||||
|
d = result.scalars().first()
|
||||||
if not d:
|
if not d:
|
||||||
raise HTTPException(status_code=404, detail="Domain not found")
|
raise HTTPException(status_code=404, detail="Domain not found")
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/version")
|
||||||
|
async def get_version(domain: str, acme_config_id: int | None = None, db: AsyncSession = Depends(get_db)):
|
||||||
|
"""返回当前证书版本号(无需认证,方便客户端轻量检查)"""
|
||||||
|
d = await _find_domain(db, domain, acme_config_id=acme_config_id)
|
||||||
return PlainTextResponse(d.version)
|
return PlainTextResponse(d.version)
|
||||||
|
|
||||||
|
|
||||||
@@ -52,16 +63,12 @@ def _cert_store_dir(domain: str) -> str:
|
|||||||
@router.get("/cert/fullchain")
|
@router.get("/cert/fullchain")
|
||||||
async def get_fullchain(
|
async def get_fullchain(
|
||||||
domain: str,
|
domain: str,
|
||||||
|
acme_config_id: int | None = None,
|
||||||
server: Server = Depends(verify_token),
|
server: Server = Depends(verify_token),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""下载 fullchain.pem"""
|
"""下载 fullchain.pem"""
|
||||||
result = await db.execute(
|
await _find_domain(db, domain, server_id=server.id, acme_config_id=acme_config_id)
|
||||||
select(Domain).where(Domain.domain == domain, Domain.server_id == server.id)
|
|
||||||
)
|
|
||||||
d = result.scalar_one_or_none()
|
|
||||||
if not d:
|
|
||||||
raise HTTPException(status_code=404, detail="Domain not found for this server")
|
|
||||||
cert_path = Path(settings.cert_dir) / _cert_store_dir(domain) / "fullchain.pem"
|
cert_path = Path(settings.cert_dir) / _cert_store_dir(domain) / "fullchain.pem"
|
||||||
if not cert_path.exists():
|
if not cert_path.exists():
|
||||||
raise HTTPException(status_code=404, detail="Certificate file not found")
|
raise HTTPException(status_code=404, detail="Certificate file not found")
|
||||||
@@ -71,16 +78,12 @@ async def get_fullchain(
|
|||||||
@router.get("/cert/private")
|
@router.get("/cert/private")
|
||||||
async def get_private_key(
|
async def get_private_key(
|
||||||
domain: str,
|
domain: str,
|
||||||
|
acme_config_id: int | None = None,
|
||||||
server: Server = Depends(verify_token),
|
server: Server = Depends(verify_token),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""下载 private.key"""
|
"""下载 private.key"""
|
||||||
result = await db.execute(
|
await _find_domain(db, domain, server_id=server.id, acme_config_id=acme_config_id)
|
||||||
select(Domain).where(Domain.domain == domain, Domain.server_id == server.id)
|
|
||||||
)
|
|
||||||
d = result.scalar_one_or_none()
|
|
||||||
if not d:
|
|
||||||
raise HTTPException(status_code=404, detail="Domain not found for this server")
|
|
||||||
key_path = Path(settings.cert_dir) / _cert_store_dir(domain) / "private.key"
|
key_path = Path(settings.cert_dir) / _cert_store_dir(domain) / "private.key"
|
||||||
if not key_path.exists():
|
if not key_path.exists():
|
||||||
raise HTTPException(status_code=404, detail="Private key file not found")
|
raise HTTPException(status_code=404, detail="Private key file not found")
|
||||||
@@ -91,22 +94,18 @@ async def get_private_key(
|
|||||||
async def generate_script(
|
async def generate_script(
|
||||||
domain: str,
|
domain: str,
|
||||||
server_name: str,
|
server_name: str,
|
||||||
|
acme_config_id: int | None = None,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""根据 server_name 生成对应的部署脚本"""
|
"""根据 server_name 生成对应的部署脚本"""
|
||||||
# 查找服务器
|
# 查找服务器
|
||||||
result = await db.execute(select(Server).where(Server.name == server_name))
|
result = await db.execute(select(Server).where(Server.name == server_name))
|
||||||
server = result.scalar_one_or_none()
|
server = result.scalars().first()
|
||||||
if not server:
|
if not server:
|
||||||
raise HTTPException(status_code=404, detail="Server not found")
|
raise HTTPException(status_code=404, detail="Server not found")
|
||||||
|
|
||||||
# 查找域名配置
|
# 查找域名配置
|
||||||
result = await db.execute(
|
d = await _find_domain(db, domain, server_id=server.id, acme_config_id=acme_config_id)
|
||||||
select(Domain).where(Domain.domain == domain, Domain.server_id == server.id)
|
|
||||||
)
|
|
||||||
d = result.scalar_one_or_none()
|
|
||||||
if not d:
|
|
||||||
raise HTTPException(status_code=404, detail="Domain not found for this server")
|
|
||||||
|
|
||||||
# 选择模板
|
# 选择模板
|
||||||
if server.platform == "windows":
|
if server.platform == "windows":
|
||||||
@@ -122,6 +121,7 @@ async def generate_script(
|
|||||||
cert_dir=d.cert_dir,
|
cert_dir=d.cert_dir,
|
||||||
check_cmd=d.check_cmd,
|
check_cmd=d.check_cmd,
|
||||||
reload_cmd=d.reload_cmd,
|
reload_cmd=d.reload_cmd,
|
||||||
|
acme_config_id=d.acme_config_id or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
media_type = "text/plain" if server.platform == "windows" else "application/x-sh"
|
media_type = "text/plain" if server.platform == "windows" else "application/x-sh"
|
||||||
@@ -134,16 +134,12 @@ async def report_deploy(
|
|||||||
status: str,
|
status: str,
|
||||||
message: str = "",
|
message: str = "",
|
||||||
hostname: str = "",
|
hostname: str = "",
|
||||||
|
acme_config_id: int | None = None,
|
||||||
server: Server = Depends(verify_token),
|
server: Server = Depends(verify_token),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""客户端上报部署结果"""
|
"""客户端上报部署结果"""
|
||||||
result = await db.execute(
|
d = await _find_domain(db, domain, server_id=server.id, acme_config_id=acme_config_id)
|
||||||
select(Domain).where(Domain.domain == domain, Domain.server_id == server.id)
|
|
||||||
)
|
|
||||||
d = result.scalar_one_or_none()
|
|
||||||
if not d:
|
|
||||||
raise HTTPException(status_code=404, detail="Domain not found for this server")
|
|
||||||
|
|
||||||
log = DeployLog(domain_id=d.id, server_id=server.id, hostname=hostname, status=status, message=message)
|
log = DeployLog(domain_id=d.id, server_id=server.id, hostname=hostname, status=status, message=message)
|
||||||
db.add(log)
|
db.add(log)
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ $Token = "{{ token }}"
|
|||||||
$CertDir = "{{ cert_dir }}"
|
$CertDir = "{{ cert_dir }}"
|
||||||
$CheckCmd = "{{ check_cmd }}"
|
$CheckCmd = "{{ check_cmd }}"
|
||||||
$ReloadCmd = "{{ reload_cmd }}"
|
$ReloadCmd = "{{ reload_cmd }}"
|
||||||
|
$ConfigId = "{{ acme_config_id }}"
|
||||||
# ===========================
|
# ===========================
|
||||||
|
|
||||||
$EncodedDomain = [System.Uri]::EscapeDataString($Domain)
|
$EncodedDomain = [System.Uri]::EscapeDataString($Domain)
|
||||||
|
$ConfigParam = if ($ConfigId) { "&acme_config_id=$ConfigId" } else { "" }
|
||||||
$TmpDir = "$env:TEMP\cert-sync-$($Domain -replace '[*\.]','_')"
|
$TmpDir = "$env:TEMP\cert-sync-$($Domain -replace '[*\.]','_')"
|
||||||
$VersionFile = "$CertDir\.version"
|
$VersionFile = "$CertDir\.version"
|
||||||
|
|
||||||
@@ -20,13 +22,13 @@ $headers = @{ Authorization = "Bearer $Token" }
|
|||||||
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
|
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
|
||||||
|
|
||||||
# 1. 检查版本
|
# 1. 检查版本
|
||||||
$remote = (Invoke-WebRequest -Uri "$BaseUrl/api/version?domain=$EncodedDomain" -Headers $headers -UseBasicParsing -SkipCertificateCheck).Content.Trim()
|
$remote = (Invoke-WebRequest -Uri "$BaseUrl/api/version?domain=$EncodedDomain$ConfigParam" -Headers $headers -UseBasicParsing -SkipCertificateCheck).Content.Trim()
|
||||||
$local = if (Test-Path $VersionFile) { (Get-Content $VersionFile).Trim() } else { "0" }
|
$local = if (Test-Path $VersionFile) { (Get-Content $VersionFile).Trim() } else { "0" }
|
||||||
if ($remote -eq $local) { exit 0 }
|
if ($remote -eq $local) { exit 0 }
|
||||||
|
|
||||||
# 2. 下载证书
|
# 2. 下载证书
|
||||||
Invoke-WebRequest -Uri "$BaseUrl/api/cert/fullchain?domain=$EncodedDomain" -Headers $headers -OutFile "$TmpDir\fullchain.pem" -UseBasicParsing -SkipCertificateCheck
|
Invoke-WebRequest -Uri "$BaseUrl/api/cert/fullchain?domain=$EncodedDomain$ConfigParam" -Headers $headers -OutFile "$TmpDir\fullchain.pem" -UseBasicParsing -SkipCertificateCheck
|
||||||
Invoke-WebRequest -Uri "$BaseUrl/api/cert/private?domain=$EncodedDomain" -Headers $headers -OutFile "$TmpDir\private.key" -UseBasicParsing -SkipCertificateCheck
|
Invoke-WebRequest -Uri "$BaseUrl/api/cert/private?domain=$EncodedDomain$ConfigParam" -Headers $headers -OutFile "$TmpDir\private.key" -UseBasicParsing -SkipCertificateCheck
|
||||||
|
|
||||||
# 3. 备份旧证书
|
# 3. 备份旧证书
|
||||||
Copy-Item "$CertDir\fullchain.pem" "$CertDir\fullchain.pem.bak" -ErrorAction SilentlyContinue
|
Copy-Item "$CertDir\fullchain.pem" "$CertDir\fullchain.pem.bak" -ErrorAction SilentlyContinue
|
||||||
@@ -43,11 +45,11 @@ try {
|
|||||||
$remote | Out-File -NoNewline -Encoding ascii $VersionFile
|
$remote | Out-File -NoNewline -Encoding ascii $VersionFile
|
||||||
Invoke-Expression $ReloadCmd
|
Invoke-Expression $ReloadCmd
|
||||||
Write-Host "[$(Get-Date -Format o)] updated: $Domain -> $remote"
|
Write-Host "[$(Get-Date -Format o)] updated: $Domain -> $remote"
|
||||||
try { Invoke-WebRequest -Uri "$BaseUrl/api/report?domain=$EncodedDomain&status=success&hostname=$HostName&message=deployed+v$remote" -Headers $headers -Method POST -UseBasicParsing -SkipCertificateCheck | Out-Null } catch {}
|
try { Invoke-WebRequest -Uri "$BaseUrl/api/report?domain=$EncodedDomain&status=success&hostname=$HostName&message=deployed+v$remote$ConfigParam" -Headers $headers -Method POST -UseBasicParsing -SkipCertificateCheck | Out-Null } catch {}
|
||||||
} catch {
|
} catch {
|
||||||
Move-Item "$CertDir\fullchain.pem.bak" "$CertDir\fullchain.pem" -Force -ErrorAction SilentlyContinue
|
Move-Item "$CertDir\fullchain.pem.bak" "$CertDir\fullchain.pem" -Force -ErrorAction SilentlyContinue
|
||||||
Move-Item "$CertDir\private.key.bak" "$CertDir\private.key" -Force -ErrorAction SilentlyContinue
|
Move-Item "$CertDir\private.key.bak" "$CertDir\private.key" -Force -ErrorAction SilentlyContinue
|
||||||
Write-Error "[$(Get-Date -Format o)] FAILED: $Domain, rolled back"
|
Write-Error "[$(Get-Date -Format o)] FAILED: $Domain, rolled back"
|
||||||
try { Invoke-WebRequest -Uri "$BaseUrl/api/report?domain=$EncodedDomain&status=failed&hostname=$HostName&message=check+failed+rolled+back" -Headers $headers -Method POST -UseBasicParsing -SkipCertificateCheck | Out-Null } catch {}
|
try { Invoke-WebRequest -Uri "$BaseUrl/api/report?domain=$EncodedDomain&status=failed&hostname=$HostName&message=check+failed+rolled+back$ConfigParam" -Headers $headers -Method POST -UseBasicParsing -SkipCertificateCheck | Out-Null } catch {}
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,15 @@ TOKEN="{{ token }}"
|
|||||||
CERT_DIR="{{ cert_dir }}"
|
CERT_DIR="{{ cert_dir }}"
|
||||||
CHECK_CMD="{{ check_cmd }}"
|
CHECK_CMD="{{ check_cmd }}"
|
||||||
RELOAD_CMD="{{ reload_cmd }}"
|
RELOAD_CMD="{{ reload_cmd }}"
|
||||||
|
CONFIG_ID="{{ acme_config_id }}"
|
||||||
# ===========================
|
# ===========================
|
||||||
|
|
||||||
VERSION_URL="${BASE_URL}/api/version?domain=${DOMAIN}"
|
CONFIG_PARAM=""
|
||||||
FULLCHAIN_URL="${BASE_URL}/api/cert/fullchain?domain=${DOMAIN}"
|
[[ -n "${CONFIG_ID}" ]] && CONFIG_PARAM="&acme_config_id=${CONFIG_ID}"
|
||||||
PRIVATE_URL="${BASE_URL}/api/cert/private?domain=${DOMAIN}"
|
|
||||||
|
VERSION_URL="${BASE_URL}/api/version?domain=${DOMAIN}${CONFIG_PARAM}"
|
||||||
|
FULLCHAIN_URL="${BASE_URL}/api/cert/fullchain?domain=${DOMAIN}${CONFIG_PARAM}"
|
||||||
|
PRIVATE_URL="${BASE_URL}/api/cert/private?domain=${DOMAIN}${CONFIG_PARAM}"
|
||||||
TMP_DIR="/tmp/cert-sync-${DOMAIN//\*/_}"
|
TMP_DIR="/tmp/cert-sync-${DOMAIN//\*/_}"
|
||||||
VERSION_FILE="${CERT_DIR}/.version"
|
VERSION_FILE="${CERT_DIR}/.version"
|
||||||
|
|
||||||
@@ -47,11 +51,11 @@ if ${CHECK_CMD}; then
|
|||||||
echo "${REMOTE}" > "${VERSION_FILE}"
|
echo "${REMOTE}" > "${VERSION_FILE}"
|
||||||
${RELOAD_CMD}
|
${RELOAD_CMD}
|
||||||
echo "[$(date -Is)] updated: ${DOMAIN} -> ${REMOTE}"
|
echo "[$(date -Is)] updated: ${DOMAIN} -> ${REMOTE}"
|
||||||
curl "${curl_opts[@]}" "${auth[@]}" -X POST "${BASE_URL}/api/report?domain=${DOMAIN}&status=success&hostname=${HOSTNAME}&message=deployed+v${REMOTE}" 2>/dev/null || true
|
curl "${curl_opts[@]}" "${auth[@]}" -X POST "${BASE_URL}/api/report?domain=${DOMAIN}&status=success&hostname=${HOSTNAME}&message=deployed+v${REMOTE}${CONFIG_PARAM}" 2>/dev/null || true
|
||||||
else
|
else
|
||||||
mv -f "${CERT_DIR}/fullchain.pem.bak" "${CERT_DIR}/fullchain.pem" 2>/dev/null || true
|
mv -f "${CERT_DIR}/fullchain.pem.bak" "${CERT_DIR}/fullchain.pem" 2>/dev/null || true
|
||||||
mv -f "${CERT_DIR}/private.key.bak" "${CERT_DIR}/private.key" 2>/dev/null || true
|
mv -f "${CERT_DIR}/private.key.bak" "${CERT_DIR}/private.key" 2>/dev/null || true
|
||||||
echo "[$(date -Is)] FAILED: ${DOMAIN}, rolled back" >&2
|
echo "[$(date -Is)] FAILED: ${DOMAIN}, rolled back" >&2
|
||||||
curl "${curl_opts[@]}" "${auth[@]}" -X POST "${BASE_URL}/api/report?domain=${DOMAIN}&status=failed&hostname=${HOSTNAME}&message=check+failed+rolled+back" 2>/dev/null || true
|
curl "${curl_opts[@]}" "${auth[@]}" -X POST "${BASE_URL}/api/report?domain=${DOMAIN}&status=failed&hostname=${HOSTNAME}&message=check+failed+rolled+back${CONFIG_PARAM}" 2>/dev/null || true
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -51,8 +51,4 @@ export const getLogs = (status) => api.get('/logs', { params: { status } })
|
|||||||
export const issueCert = (domainId) => api.post(`/acme/issue/${domainId}`)
|
export const issueCert = (domainId) => api.post(`/acme/issue/${domainId}`)
|
||||||
export const renewCert = (domainId) => api.post(`/acme/renew/${domainId}`)
|
export const renewCert = (domainId) => api.post(`/acme/renew/${domainId}`)
|
||||||
|
|
||||||
// Script (通过管理接口获取脚本内容)
|
|
||||||
export const getScript = (domain, serverName) =>
|
|
||||||
axios.get('/api/script', { params: { domain, server_name: serverName } })
|
|
||||||
|
|
||||||
export default api
|
export default api
|
||||||
|
|||||||
Reference in New Issue
Block a user