Last active
January 23, 2026 07:15
-
-
Save rakuyoMo/68c3be53edbc9e5285fc2c33fb67fa49 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # 基于 DSM API 导入证书,并可选设置默认证书与服务绑定。 | |
| # 依赖 curl 和 awk 以及 sed,使用 --assign-services 时需要安装 Python3 套件。 | |
| usage() { | |
| cat <<'EOF' | |
| 用法: | |
| synology-import-cert.sh --base-url URL --username USER --password PASS --cert-desc DESC --fullchain PATH --key PATH [选项] | |
| 必填参数: | |
| --base-url DSM 地址,例如 https://192.168.1.10:5001 | |
| --username DSM 管理员账号 | |
| --password DSM 管理员密码 | |
| --cert-desc 证书描述,用于定位或创建记录 | |
| --fullchain 全链证书路径 | |
| --key 私钥路径 | |
| 可选参数: | |
| --create 证书描述不存在时自动创建 | |
| --insecure 跳过 HTTPS 证书校验(等同 curl -k) | |
| --set-default 设置为系统默认证书 | |
| --assign-services 绑定服务名称,逗号分隔,支持 all 表示全部 | |
| --device-id 2FA 已信任设备 ID | |
| --device-name 设备名称(自定义),默认为 certd | |
| -h, --help 显示帮助 | |
| 提示: | |
| 使用 --assign-services 需要先安装 Python3 套件。 | |
| EOF | |
| } | |
| # 记录参数默认值,后续会被命令行参数覆盖。 | |
| SYNO_BASE_URL="" | |
| SYNO_USERNAME="" | |
| SYNO_PASSWORD="" | |
| SYNO_CERT_DESC="" | |
| CERT_FULLCHAIN="" | |
| CERT_KEY="" | |
| SYNO_CREATE="0" | |
| SYNO_SET_DEFAULT="0" | |
| SYNO_ASSIGN_SERVICES="" | |
| SYNO_DEVICE_NAME="certd" | |
| SYNO_DEVICE_ID="" | |
| INSECURE="0" | |
| # 统一错误输出,方便在流水线日志中定位问题。 | |
| fail() { | |
| echo "错误:$1" >&2 | |
| exit 1 | |
| } | |
| # 检查必填参数是否存在。 | |
| require_arg() { | |
| [ -n "$2" ] || fail "缺少参数:$1" | |
| } | |
| # 检查参数是否带值。 | |
| require_value() { | |
| local flag="$1" | |
| local value="${2:-}" | |
| [ -n "$value" ] || fail "参数 $flag 缺少值" | |
| } | |
| # 解析命令行参数。 | |
| while [ "$#" -gt 0 ]; do | |
| case "$1" in | |
| --base-url) | |
| require_value "$1" "${2:-}" | |
| SYNO_BASE_URL="$2" | |
| shift 2 | |
| ;; | |
| --username) | |
| require_value "$1" "${2:-}" | |
| SYNO_USERNAME="$2" | |
| shift 2 | |
| ;; | |
| --password) | |
| require_value "$1" "${2:-}" | |
| SYNO_PASSWORD="$2" | |
| shift 2 | |
| ;; | |
| --cert-desc) | |
| require_value "$1" "${2:-}" | |
| SYNO_CERT_DESC="$2" | |
| shift 2 | |
| ;; | |
| --fullchain) | |
| require_value "$1" "${2:-}" | |
| CERT_FULLCHAIN="$2" | |
| shift 2 | |
| ;; | |
| --key) | |
| require_value "$1" "${2:-}" | |
| CERT_KEY="$2" | |
| shift 2 | |
| ;; | |
| --device-name) | |
| require_value "$1" "${2:-}" | |
| SYNO_DEVICE_NAME="$2" | |
| shift 2 | |
| ;; | |
| --device-id) | |
| require_value "$1" "${2:-}" | |
| SYNO_DEVICE_ID="$2" | |
| shift 2 | |
| ;; | |
| --create) | |
| SYNO_CREATE="1" | |
| shift | |
| ;; | |
| --set-default) | |
| SYNO_SET_DEFAULT="1" | |
| shift | |
| ;; | |
| --assign-services) | |
| require_value "$1" "${2:-}" | |
| SYNO_ASSIGN_SERVICES="$2" | |
| shift 2 | |
| ;; | |
| --insecure) | |
| INSECURE="1" | |
| shift | |
| ;; | |
| -h|--help) | |
| usage | |
| exit 0 | |
| ;; | |
| *) | |
| fail "未知参数:$1" | |
| ;; | |
| esac | |
| done | |
| # 校验证书文件是否存在。 | |
| require_file() { | |
| [ -f "$1" ] || fail "未找到文件:$1" | |
| } | |
| # 查找可用的 Python,用于解析 DSM 返回的 JSON。 | |
| find_python() { | |
| if command -v python3 >/dev/null 2>&1; then | |
| echo "python3" | |
| return 0 | |
| fi | |
| if command -v python >/dev/null 2>&1; then | |
| echo "python" | |
| return 0 | |
| fi | |
| return 1 | |
| } | |
| # 生成服务绑定所需的 settings JSON。 | |
| # DSM 需要完整的 service 对象,因此要从证书列表里提取结构。 | |
| build_service_settings() { | |
| local services_csv="$1" | |
| local new_cert_id="$2" | |
| local python_bin | |
| python_bin=$(find_python) || fail "未找到 python3 或 python,请先在套件中心安装 Python3 套件后再使用 --assign-services" | |
| local tmp_err | |
| tmp_err=$(mktemp) | |
| local settings | |
| settings=$( | |
| printf "%s" "$cert_list" | "$python_bin" - "$services_csv" "$new_cert_id" 2>"$tmp_err" <<'PY' | |
| import json | |
| import sys | |
| # 读取参数与证书列表。 | |
| services_arg = sys.argv[1].strip() | |
| new_id = sys.argv[2] | |
| data = json.load(sys.stdin) | |
| # 建立服务名称到证书与服务信息的映射。 | |
| service_map = {} | |
| for cert in data.get("data", {}).get("certificates", []): | |
| for service in cert.get("services", []) or []: | |
| name = service.get("display_name") or "" | |
| if not name: | |
| continue | |
| service_map[name] = { | |
| "service": service, | |
| "old_id": cert.get("id", "") | |
| } | |
| if not service_map: | |
| sys.stderr.write("未获取到服务列表,请确认 DSM 返回的证书列表包含 services 字段。\n") | |
| sys.exit(2) | |
| # 解析服务名称,支持 all 或 * 表示全部。 | |
| if services_arg.lower() in ("all", "*"): | |
| services = sorted(service_map.keys()) | |
| else: | |
| services = [s.strip() for s in services_arg.split(",") if s.strip()] | |
| if not services: | |
| sys.stderr.write("服务名称不能为空。\n") | |
| sys.exit(2) | |
| missing = [name for name in services if name not in service_map] | |
| if missing: | |
| available = sorted(service_map.keys()) | |
| sys.stderr.write("未找到服务名称:" + ", ".join(missing) + "\n") | |
| if available: | |
| sys.stderr.write("可选服务名称:" + ", ".join(available) + "\n") | |
| sys.exit(2) | |
| # 生成 DSM 需要的 settings 数组结构。 | |
| settings = [] | |
| for name in services: | |
| info = service_map[name] | |
| settings.append({"service": info["service"], "old_id": info["old_id"], "id": new_id}) | |
| sys.stdout.write(json.dumps(settings, separators=(",", ":"))) | |
| PY | |
| ) || { | |
| local err | |
| err=$(cat "$tmp_err") | |
| rm -f "$tmp_err" | |
| [ -n "$err" ] || err="生成服务绑定参数失败" | |
| fail "$err" | |
| } | |
| rm -f "$tmp_err" | |
| printf "%s" "$settings" | |
| } | |
| # 检查必填参数。 | |
| require_arg "--base-url" "$SYNO_BASE_URL" | |
| require_arg "--username" "$SYNO_USERNAME" | |
| require_arg "--password" "$SYNO_PASSWORD" | |
| require_arg "--cert-desc" "$SYNO_CERT_DESC" | |
| require_arg "--fullchain" "$CERT_FULLCHAIN" | |
| require_arg "--key" "$CERT_KEY" | |
| # 校验证书文件路径。 | |
| require_file "$CERT_FULLCHAIN" | |
| require_file "$CERT_KEY" | |
| # 如果要绑定服务,提前确认 Python 可用。 | |
| if [ -n "$SYNO_ASSIGN_SERVICES" ]; then | |
| if ! find_python >/dev/null 2>&1; then | |
| fail "未找到 python3 或 python,请先在套件中心安装 Python3 套件后再使用 --assign-services" | |
| fi | |
| fi | |
| # 组装 curl 参数,可选跳过 HTTPS 证书校验。 | |
| CURL_ARGS=(-sS) | |
| # 如需忽略证书校验,则追加 -k。 | |
| if [ "$INSECURE" = "1" ]; then | |
| CURL_ARGS+=(-k) | |
| fi | |
| # 获取 DSM API 入口路径与版本,兼容不同 DSM 版本。 | |
| auth_info=$(curl "${CURL_ARGS[@]}" "$SYNO_BASE_URL/webapi/query.cgi?api=SYNO.API.Info&version=1&method=query&query=SYNO.API.Auth") | |
| api_path=$(echo "$auth_info" | sed -n 's/.*"path" *: *"\([^"]*\)".*/\1/p') | |
| api_version=$(echo "$auth_info" | sed -n 's/.*"maxVersion" *: *\([0-9]\+\).*/\1/p') | |
| # 如果返回为空,使用常见默认值兜底。 | |
| [ -n "$api_path" ] || api_path="entry.cgi" | |
| [ -n "$api_version" ] || api_version="6" | |
| # 登录获取 sid 与 SynoToken,后续接口都依赖它们。 | |
| login_args=( | |
| "${CURL_ARGS[@]}" -G "$SYNO_BASE_URL/webapi/$api_path" | |
| --data-urlencode "api=SYNO.API.Auth" | |
| --data-urlencode "version=$api_version" | |
| --data-urlencode "method=login" | |
| --data-urlencode "format=sid" | |
| --data-urlencode "account=$SYNO_USERNAME" | |
| --data-urlencode "passwd=$SYNO_PASSWORD" | |
| --data-urlencode "enable_syno_token=yes" | |
| ) | |
| # 已信任设备登录,适合自动化场景。 | |
| if [ -n "$SYNO_DEVICE_ID" ]; then | |
| login_args+=(--data-urlencode "device_id=$SYNO_DEVICE_ID" --data-urlencode "device_name=$SYNO_DEVICE_NAME") | |
| fi | |
| login_response=$(curl "${login_args[@]}") | |
| # 简单判断是否有错误码返回。 | |
| if echo "$login_response" | grep -q '"error"'; then | |
| error_code=$(echo "$login_response" | sed -n 's/.*"code":\([0-9]\+\).*/\1/p') | |
| fail "登录失败,错误码:$error_code" | |
| fi | |
| # 解析会话标识与 Token。 | |
| sid=$(echo "$login_response" | sed -n 's/.*"sid" *: *"\([^"]*\)".*/\1/p') | |
| token=$(echo "$login_response" | sed -n 's/.*"synotoken" *: *"\([^"]*\)".*/\1/p') | |
| [ -n "$sid" ] || fail "未获取到 sid" | |
| [ -n "$token" ] || fail "未获取到 SynoToken" | |
| # 获取证书列表,用于定位证书 ID 与服务列表。 | |
| cert_list=$(curl "${CURL_ARGS[@]}" -X POST "$SYNO_BASE_URL/webapi/entry.cgi" \ | |
| -H "X-SYNO-TOKEN: $token" \ | |
| --data "api=SYNO.Core.Certificate.CRT" \ | |
| --data "method=list" \ | |
| --data "version=1" \ | |
| --data "_sid=$sid") | |
| if echo "$cert_list" | grep -q '"error"'; then | |
| fail "获取证书列表失败,请检查账号权限。" | |
| fi | |
| # 基于证书描述匹配证书 ID,避免额外 JSON 解析依赖。 | |
| escaped_desc=$(printf "%s" "$SYNO_CERT_DESC" | sed 's/\([].*^$[]\)/\\\1/g;s/"/\\\\"/g') | |
| cert_id=$(echo "$cert_list" | sed -n "s/.*\\\"desc\\\":\\\"$escaped_desc\\\",\\\"id\\\":\\\"\\([^\"]*\\)\\\".*/\\1/p") | |
| if [ -z "$cert_id" ]; then | |
| if [ "${SYNO_CREATE:-}" = "1" ]; then | |
| echo "提示:未找到证书描述,将自动创建记录:$SYNO_CERT_DESC" | |
| else | |
| fail "未找到证书描述:$SYNO_CERT_DESC,请先在 DSM 中创建同名证书记录。" | |
| fi | |
| fi | |
| # 记录当前证书是否是默认值,便于复用。 | |
| is_default=$(echo "$cert_list" | sed -n "s/.*\\\"desc\\\":\\\"$escaped_desc\\\"[^}]*\\\"is_default\\\":\\(true\\|false\\).*/\\1/p") | |
| # DSM 导入接口要求叶子证书与中间证书分开。 | |
| tmp_leaf=$(mktemp) | |
| tmp_intermediate=$(mktemp) | |
| trap 'rm -f "$tmp_leaf" "$tmp_intermediate"' EXIT | |
| : > "$tmp_leaf" | |
| : > "$tmp_intermediate" | |
| awk '/-----BEGIN CERTIFICATE-----/ {n++} { if (n == 1) print > leaf; else print > inter }' \ | |
| leaf="$tmp_leaf" inter="$tmp_intermediate" "$CERT_FULLCHAIN" | |
| # 上传证书并可选设置为默认。 | |
| upload_url="$SYNO_BASE_URL/webapi/entry.cgi?api=SYNO.Core.Certificate&method=import&version=1&SynoToken=$token&_sid=$sid" | |
| upload_args=( | |
| "${CURL_ARGS[@]}" -X POST "$upload_url" | |
| -H "X-SYNO-TOKEN: $token" | |
| -F "key=@$CERT_KEY" | |
| -F "cert=@$tmp_leaf" | |
| -F "inter_cert=@$tmp_intermediate" | |
| -F "id=$cert_id" | |
| -F "desc=$SYNO_CERT_DESC" | |
| ) | |
| # 如果显式设置默认,或原记录已是默认,则保持默认状态。 | |
| if [ "$SYNO_SET_DEFAULT" = "1" ] || [ "$is_default" = "true" ]; then | |
| upload_args+=(-F "as_default=true") | |
| fi | |
| upload_response=$(curl "${upload_args[@]}") | |
| if echo "$upload_response" | grep -q '"success":true'; then | |
| echo "证书导入完成。" | |
| new_cert_id=$(echo "$upload_response" | sed -n 's/.*"id" *: *"\([^"]*\)".*/\1/p') | |
| [ -n "$new_cert_id" ] && cert_id="$new_cert_id" | |
| else | |
| fail "证书导入失败:$upload_response" | |
| fi | |
| # 可选:再次调用接口设置系统默认证书。 | |
| if [ "$SYNO_SET_DEFAULT" = "1" ]; then | |
| [ -n "$cert_id" ] || fail "未获取到证书 ID,无法设置默认证书" | |
| default_id="\"$cert_id\"" | |
| default_desc="\"\"" | |
| set_default_response=$(curl "${CURL_ARGS[@]}" -X POST "$SYNO_BASE_URL/webapi/entry.cgi" \ | |
| -H "X-SYNO-TOKEN: $token" \ | |
| --data-urlencode "api=SYNO.Core.Certificate.CRT" \ | |
| --data-urlencode "method=set" \ | |
| --data-urlencode "version=1" \ | |
| --data-urlencode "_sid=$sid" \ | |
| --data-urlencode "id=$default_id" \ | |
| --data-urlencode "desc=$default_desc" \ | |
| --data-urlencode "as_default=true") | |
| if echo "$set_default_response" | grep -q '"success":true'; then | |
| echo "默认证书已更新。" | |
| else | |
| fail "设置默认证书失败:$set_default_response" | |
| fi | |
| fi | |
| # 可选:绑定服务证书。 | |
| if [ -n "$SYNO_ASSIGN_SERVICES" ]; then | |
| [ -n "$cert_id" ] || fail "未获取到证书 ID,无法绑定服务" | |
| settings_json=$(build_service_settings "$SYNO_ASSIGN_SERVICES" "$cert_id") | |
| assign_response=$(curl "${CURL_ARGS[@]}" -X POST "$SYNO_BASE_URL/webapi/entry.cgi" \ | |
| -H "X-SYNO-TOKEN: $token" \ | |
| --data-urlencode "api=SYNO.Core.Certificate.Service" \ | |
| --data-urlencode "method=set" \ | |
| --data-urlencode "version=1" \ | |
| --data-urlencode "_sid=$sid" \ | |
| --data-urlencode "settings=$settings_json") | |
| if echo "$assign_response" | grep -q '"success":true'; then | |
| echo "服务绑定完成。" | |
| else | |
| fail "服务绑定失败:$assign_response" | |
| fi | |
| fi | |
| # 登出释放会话。 | |
| curl "${CURL_ARGS[@]}" -G "$SYNO_BASE_URL/webapi/$api_path" \ | |
| --data-urlencode "api=SYNO.API.Auth" \ | |
| --data-urlencode "version=$api_version" \ | |
| --data-urlencode "method=logout" \ | |
| --data-urlencode "_sid=$sid" >/dev/null 2>&1 || true |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment