]> git.proxmox.com Git - mirror_acme.sh.git/blobdiff - deploy/synology_dsm.sh
fix: Synology DSM API path regex
[mirror_acme.sh.git] / deploy / synology_dsm.sh
index 5a70c74ebed05b84d1bedf78e6bfb9c476db072d..10da861a3fade2b642cb477afa787faa12aef500 100644 (file)
@@ -1,29 +1,35 @@
-#!/usr/bin/env sh
-
-# Here is a script to deploy cert to Synology DSM
-#
-# it requires the jq and curl are in the $PATH and the following
-# environment variables must be set:
-#
-# SYNO_Username - Synology Username to login (must be an administrator)
-# SYNO_Password - Synology Password to login
-# SYNO_Certificate - Certificate description to target for replacement
-#
-# The following environmental variables may be set if you don't like their
-# default values:
-#
-# SYNO_Scheme - defaults to http
-# SYNO_Hostname - defaults to localhost
-# SYNO_Port - defaults to 5000
-# SYNO_DID - device ID to skip OTP - defaults to empty
-#
-#returns 0 means success, otherwise error.
-
-########  Public functions #####################
-
+#!/bin/bash
+
+################################################################################
+# ACME.sh 3rd party deploy plugin for Synology DSM
+################################################################################
+# Authors: Brian Hartvigsen (creator), https://github.com/tresni
+#          Martin Arndt (contributor), https://troublezone.net/
+# Updated: 2023-07-03
+# Issues:  https://github.com/acmesh-official/acme.sh/issues/2727
+################################################################################
+# Usage:
+# 1. export SYNO_Username="adminUser"
+# 2. export SYNO_Password="adminPassword"
+# Optional exports (shown values are the defaults):
+# - export SYNO_Certificate="" to replace a specific certificate via description
+# - export SYNO_Scheme="http"
+# - export SYNO_Hostname="localhost"
+# - export SYNO_Port="5000"
+# - export SYNO_Device_Name="CertRenewal" - required for skipping 2FA-OTP
+# - export SYNO_Device_ID=""              - required for skipping 2FA-OTP
+# 3. acme.sh --deploy --deploy-hook synology_dsm -d example.com
+################################################################################
+# Dependencies:
+# - jq & curl
+################################################################################
+# Return value:
+# 0 means success, otherwise error.
+################################################################################
+
+########## Public functions ####################################################
 #domain keyfile certfile cafile fullchain
 synology_dsm_deploy() {
-
   _cdomain="$1"
   _ckey="$2"
   _ccert="$3"
@@ -31,38 +37,46 @@ synology_dsm_deploy() {
 
   _debug _cdomain "$_cdomain"
 
-  # Get Username and Password, but don't save until we successfully authenticate
+  # Get username & password, but don't save until we authenticated successfully
   _getdeployconf SYNO_Username
   _getdeployconf SYNO_Password
   _getdeployconf SYNO_Create
   _getdeployconf SYNO_DID
+  _getdeployconf SYNO_TOTP_SECRET
+  _getdeployconf SYNO_Device_Name
+  _getdeployconf SYNO_Device_ID
   if [ -z "${SYNO_Username:-}" ] || [ -z "${SYNO_Password:-}" ]; then
     _err "SYNO_Username & SYNO_Password must be set"
     return 1
   fi
+  if [ -n "${SYNO_Device_Name:-}" ] && [ -z "${SYNO_Device_ID:-}" ]; then
+    _err "SYNO_Device_Name set, but SYNO_Device_ID is empty"
+    return 1
+  fi
   _debug2 SYNO_Username "$SYNO_Username"
   _secure_debug2 SYNO_Password "$SYNO_Password"
+  _debug2 SYNO_Create "$SYNO_Create"
+  _debug2 SYNO_Device_Name "$SYNO_Device_Name"
+  _secure_debug2 SYNO_Device_ID "$SYNO_Device_ID"
 
-  # Optional scheme, hostname, and port for Synology DSM
+  # Optional scheme, hostname & port for Synology DSM
   _getdeployconf SYNO_Scheme
   _getdeployconf SYNO_Hostname
   _getdeployconf SYNO_Port
 
-  # default vaules for scheme, hostname, and port
-  # defaulting to localhost and http because it's localhost...
+  # Default values for scheme, hostname & port
+  # Defaulting to localhost & http, because it's localhost…
   [ -n "${SYNO_Scheme}" ] || SYNO_Scheme="http"
   [ -n "${SYNO_Hostname}" ] || SYNO_Hostname="localhost"
   [ -n "${SYNO_Port}" ] || SYNO_Port="5000"
-
   _savedeployconf SYNO_Scheme "$SYNO_Scheme"
   _savedeployconf SYNO_Hostname "$SYNO_Hostname"
   _savedeployconf SYNO_Port "$SYNO_Port"
-
   _debug2 SYNO_Scheme "$SYNO_Scheme"
   _debug2 SYNO_Hostname "$SYNO_Hostname"
   _debug2 SYNO_Port "$SYNO_Port"
 
-  # Get the certificate description, but don't save it until we verfiy it's real
+  # Get the certificate description, but don't save it until we verify it's real
   _getdeployconf SYNO_Certificate
   _debug SYNO_Certificate "${SYNO_Certificate:-}"
 
@@ -77,41 +91,81 @@ synology_dsm_deploy() {
 
   _debug "Getting API version"
   response=$(_get "$_base_url/webapi/query.cgi?api=SYNO.API.Info&version=1&method=query&query=SYNO.API.Auth")
+  api_path=$(echo "$response" | grep "SYNO.API.Auth" | sed -n 's/.*"path" *: *"\([^"]*\)".*/\1/p')
   api_version=$(echo "$response" | grep "SYNO.API.Auth" | sed -n 's/.*"maxVersion" *: *\([0-9]*\).*/\1/p')
   _debug3 response "$response"
+  _debug3 api_path "$api_path"
   _debug3 api_version "$api_version"
 
-  # Login, get the token from JSON and session id from cookie
+  # Login, get the session ID & SynoToken from JSON
   _info "Logging into $SYNO_Hostname:$SYNO_Port"
   encoded_username="$(printf "%s" "$SYNO_Username" | _url_encode)"
   encoded_password="$(printf "%s" "$SYNO_Password" | _url_encode)"
 
-  if [ -n "$SYNO_DID" ]; then
-    _H1="Cookie: did=$SYNO_DID"
-    export _H1
-    _debug3 H1 "${_H1}"
+  otp_code=""
+  # START - DEPRECATED, only kept for legacy compatibility reasons
+  if [ -n "$SYNO_TOTP_SECRET" ]; then
+    _info "WARNING: Usage of SYNO_TOTP_SECRET is deprecated!"
+    _info "         See synology_dsm.sh script or ACME.sh Wiki page for details:"
+    _info "         https://github.com/acmesh-official/acme.sh/wiki/Synology-NAS-Guide"
+    DEPRECATED_otp_code=""
+    if _exists oathtool; then
+      DEPRECATED_otp_code="$(oathtool --base32 --totp "${SYNO_TOTP_SECRET}" 2>/dev/null)"
+    else
+      _err "oathtool could not be found, install oathtool to use SYNO_TOTP_SECRET"
+      return 1
+    fi
+
+    if [ -n "$SYNO_DID" ]; then
+      _H1="Cookie: did=$SYNO_DID"
+      export _H1
+      _debug3 H1 "${_H1}"
+    fi
+
+    response=$(_post "method=login&account=$encoded_username&passwd=$encoded_password&api=SYNO.API.Auth&version=$api_version&enable_syno_token=yes&otp_code=$DEPRECATED_otp_code&device_name=certrenewal&device_id=$SYNO_DID" "$_base_url/webapi/auth.cgi?enable_syno_token=yes")
+    _debug3 response "$response"
+  # END - DEPRECATED, only kept for legacy compatibility reasons
+  # Get device ID if still empty first, otherwise log in right away
+  elif [ -z "${SYNO_Device_ID:-}" ]; then
+    printf "Enter OTP code for user '%s': " "$SYNO_Username"
+    read -r otp_code
+    if [ -z "${SYNO_Device_Name:-}" ]; then
+      printf "Enter device name or leave empty for default (CertRenewal): "
+      read -r SYNO_Device_Name
+      [ -n "${SYNO_Device_Name}" ] || SYNO_Device_Name="CertRenewal"
+    fi
+
+    response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password&otp_code=$otp_code&enable_syno_token=yes&enable_device_token=yes&device_name=$SYNO_Device_Name")
+    _secure_debug3 response "$response"
+
+    id_property='device_id'
+    [ "${api_version}" -gt '6' ] || id_property='did'
+    SYNO_Device_ID=$(echo "$response" | grep "$id_property" | sed -n 's/.*"'$id_property'" *: *"\([^"]*\).*/\1/p')
+    _secure_debug2 SYNO_Device_ID "$SYNO_Device_ID"
+  else
+    response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password&enable_syno_token=yes&device_name=$SYNO_Device_Name&device_id=$SYNO_Device_ID")
+    _debug3 response "$response"
   fi
 
-  response=$(_post "method=login&account=$encoded_username&passwd=$encoded_password&api=SYNO.API.Auth&version=$api_version&enable_syno_token=yes" "$_base_url/webapi/auth.cgi?enable_syno_token=yes")
+  sid=$(echo "$response" | grep "sid" | sed -n 's/.*"sid" *: *"\([^"]*\).*/\1/p')
   token=$(echo "$response" | grep "synotoken" | sed -n 's/.*"synotoken" *: *"\([^"]*\).*/\1/p')
-  _debug3 response "$response"
-  _debug token "$token"
-
-  if [ -z "$token" ]; then
-    _err "Unable to authenticate to $SYNO_Hostname:$SYNO_Port using $SYNO_Scheme."
-    _err "Check your username and password."
+  _debug "Session ID" "$sid"
+  _debug SynoToken "$token"
+  if [ -z "$SYNO_DID" ] && [ -z "$SYNO_Device_ID" ] || [ -z "$sid" ] || [ -z "$token" ]; then
+    _err "Unable to authenticate to $_base_url - check your username & password."
+    _err "If two-factor authentication is enabled for the user, set SYNO_Device_ID."
     return 1
   fi
-  sid=$(echo "$response" | grep "sid" | sed -n 's/.*"sid" *: *"\([^"]*\).*/\1/p')
 
   _H1="X-SYNO-TOKEN: $token"
   export _H1
   _debug2 H1 "${_H1}"
 
-  # Now that we know the username and password are good, save them
+  # Now that we know the username & password are good, save them
   _savedeployconf SYNO_Username "$SYNO_Username"
   _savedeployconf SYNO_Password "$SYNO_Password"
-  _savedeployconf SYNO_DID "$SYNO_DID"
+  _savedeployconf SYNO_Device_Name "$SYNO_Device_Name"
+  _savedeployconf SYNO_Device_ID "$SYNO_Device_ID"
 
   _info "Getting certificates in Synology DSM"
   response=$(_post "api=SYNO.Core.Certificate.CRT&method=list&version=1&_sid=$sid" "$_base_url/webapi/entry.cgi")
@@ -122,11 +176,11 @@ synology_dsm_deploy() {
   _debug2 id "$id"
 
   if [ -z "$id" ] && [ -z "${SYNO_Create:-}" ]; then
-    _err "Unable to find certificate: $SYNO_Certificate and \$SYNO_Create is not set"
+    _err "Unable to find certificate: $SYNO_Certificate & \$SYNO_Create is not set"
     return 1
   fi
 
-  # we've verified this certificate description is a thing, so save it
+  # We've verified this certificate description is a thing, so save it
   _savedeployconf SYNO_Certificate "$SYNO_Certificate" "base64"
 
   _info "Generate form POST request"
@@ -138,10 +192,10 @@ synology_dsm_deploy() {
   content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"id\"${nl}${nl}$id"
   content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"desc\"${nl}${nl}${SYNO_Certificate}"
   if echo "$response" | sed -n "s/.*\"desc\":\"$escaped_certificate\",\([^{]*\).*/\1/p" | grep -- 'is_default":true' >/dev/null; then
-    _debug2 default "this is the default certificate"
+    _debug2 default "This is the default certificate"
     content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"as_default\"${nl}${nl}true"
   else
-    _debug2 default "this is NOT the default certificate"
+    _debug2 default "This is NOT the default certificate"
   fi
   content="$content${nl}--$delim--${nl}"
   content="$(printf "%b_" "$content")"
@@ -153,13 +207,23 @@ synology_dsm_deploy() {
 
   if ! echo "$response" | grep '"error":' >/dev/null; then
     if echo "$response" | grep '"restart_httpd":true' >/dev/null; then
-      _info "http services were restarted"
+      _info "Restarting HTTP services succeeded"
     else
-      _info "http services were NOT restarted"
+      _info "Restarting HTTP services failed"
     fi
+
+    _logout
     return 0
   else
     _err "Unable to update certificate, error code $response"
+    _logout
     return 1
   fi
 }
+
+####################  Private functions below ##################################
+_logout() {
+  # Logout to not occupy a permanent session, e.g. in DSM's "Connected Users" widget
+  response=$(_get "$_base_url/webapi/entry.cgi?api=SYNO.API.Auth&version=$api_version&method=logout")
+  _debug3 response "$response"
+}