From c7106b5407545bc588b2460b2b1b5a0f90943e81 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Wed, 26 Mar 2025 21:21:00 -0400 Subject: [PATCH] Add Support for challenge validation plugin hooks --- README.md | 46 ++++++++++---- acme.sh | 105 +++++++++++++++++++++++++++++++- httpapi/README.md | 116 ++++++++++++++++++++++++++++++++++++ httpapi/http_local.sh | 119 +++++++++++++++++++++++++++++++++++++ httpapi/http_scp.sh | 135 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 509 insertions(+), 12 deletions(-) create mode 100644 httpapi/README.md create mode 100644 httpapi/http_local.sh create mode 100644 httpapi/http_scp.sh diff --git a/README.md b/README.md index 9a5c106b..e977bf5e 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ https://github.com/acmesh-official/acmetest - DNS mode - [DNS alias mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-alias-mode) - [Stateless mode](https://github.com/acmesh-official/acme.sh/wiki/Stateless-Mode) +- [HTTP API mode](https://github.com/acmesh-official/acme.sh/wiki/HTTP-API) # 1. How to install @@ -358,7 +359,30 @@ Ok, it's done. **Please use dns api mode instead.** -# 10. Issue ECC certificates +# 10. Use HTTP API mode + +If you want to deploy the challenge files using an external method like SCP or FTPS, you can use the HTTP API mode: + +```bash +acme.sh --issue -d example.com --httpapi scp +``` + +You'll need to configure the required environment variables first: + +```bash +# For SCP plugin +export HTTP_SCP_USER="username" +export HTTP_SCP_HOST="example.com" +export HTTP_SCP_PATH="/var/www/html" +``` + +Available HTTP API plugins: +- `scp`: Deploy challenge files via SCP +- `ftps`: Deploy challenge files via FTPS + +More information: [HTTP API mode](https://github.com/acmesh-official/acme.sh/wiki/HTTP-API) + +# 11. Issue ECC certificates Just set the `keylength` parameter with a prefix `ec-`. @@ -388,7 +412,7 @@ Valid values are: 6. **4096 (RSA4096)** -# 11. Issue Wildcard certificates +# 12. Issue Wildcard certificates It's simple, just give a wildcard domain as the `-d` parameter. @@ -398,7 +422,7 @@ acme.sh --issue -d example.com -d '*.example.com' --dns dns_cf -# 12. How to renew the certs +# 13. How to renew the certs No, you don't need to renew the certs manually. All the certs will be renewed automatically every **60** days. @@ -415,7 +439,7 @@ acme.sh --renew -d example.com --force --ecc ``` -# 13. How to stop cert renewal +# 14. How to stop cert renewal To stop renewal of a cert, you can execute the following to remove the cert from the renewal list: @@ -428,7 +452,7 @@ The cert/key file is not removed from the disk. You can remove the respective directory (e.g. `~/.acme.sh/example.com`) by yourself. -# 14. How to upgrade `acme.sh` +# 15. How to upgrade `acme.sh` acme.sh is in constant development, so it's strongly recommended to use the latest code. @@ -453,24 +477,24 @@ acme.sh --upgrade --auto-upgrade 0 ``` -# 15. Issue a cert from an existing CSR +# 16. Issue a cert from an existing CSR https://github.com/acmesh-official/acme.sh/wiki/Issue-a-cert-from-existing-CSR -# 16. Send notifications in cronjob +# 17. Send notifications in cronjob https://github.com/acmesh-official/acme.sh/wiki/notify -# 17. Under the Hood +# 18. Under the Hood Speak ACME language using shell, directly to "Let's Encrypt". TODO: -# 18. Acknowledgments +# 19. Acknowledgments 1. Acme-tiny: https://github.com/diafygi/acme-tiny 2. ACME protocol: https://github.com/ietf-wg-acme/acme @@ -508,7 +532,7 @@ Support this project with your organization. Your logo will show up here with a -# 19. License & Others +# 20. License & Others License is GPLv3 @@ -517,7 +541,7 @@ Please Star and Fork me. [Issues](https://github.com/acmesh-official/acme.sh/issues) and [pull requests](https://github.com/acmesh-official/acme.sh/pulls) are welcome. -# 20. Donate +# 21. Donate Your donation makes **acme.sh** better: 1. PayPal/Alipay(支付宝)/Wechat(微信): [https://donate.acme.sh/](https://donate.acme.sh/) diff --git a/acme.sh b/acme.sh index dd21785d..5b950a00 100755 --- a/acme.sh +++ b/acme.sh @@ -17,8 +17,9 @@ _SCRIPT_="$0" _SUB_FOLDER_NOTIFY="notify" _SUB_FOLDER_DNSAPI="dnsapi" _SUB_FOLDER_DEPLOY="deploy" +_SUB_FOLDER_HTTPAPI="httpapi" -_SUB_FOLDERS="$_SUB_FOLDER_DNSAPI $_SUB_FOLDER_DEPLOY $_SUB_FOLDER_NOTIFY" +_SUB_FOLDERS="$_SUB_FOLDER_DNSAPI $_SUB_FOLDER_DEPLOY $_SUB_FOLDER_NOTIFY $_SUB_FOLDER_HTTPAPI" CA_LETSENCRYPT_V2="https://acme-v02.api.letsencrypt.org/directory" CA_LETSENCRYPT_V2_TEST="https://acme-staging-v02.api.letsencrypt.org/directory" @@ -72,6 +73,7 @@ DEFAULT_RENEW=60 NO_VALUE="no" W_DNS="dns" +W_HTTPAPI="http" W_ALPN="alpn" DNS_ALIAS_PREFIX="=" @@ -3396,6 +3398,7 @@ _restoreNginx() { _clearup() { _stopserver "$serverproc" serverproc="" + _cleanup_http_entries _restoreApache _restoreNginx _clearupdns @@ -3407,6 +3410,42 @@ _clearup() { fi } +_cleanup_http_entries() { + if [ -z "$_http_entries" ]; then + _debug "_cleanup_http_entries: No HTTP entries to clean up" + return 0 + fi + _debug "Cleaning up HTTP entries: $_http_entries" + + entries=$(echo "$_http_entries" | tr "$dvsep" ' ') + for entry in $entries; do + d=$(echo "$entry" | cut -d "$sep" -f 1) + token=$(echo "$entry" | cut -d "$sep" -f 2) + keyauthorization=$(echo "$entry" | cut -d "$sep" -f 3) + _httpapi=$(echo "$entry" | cut -d "$sep" -f 4) + + _debug "Removing HTTP challenge for $d using $_httpapi" + + h_api="$(_findHook "$d" $_SUB_FOLDER_HTTPAPI "$_httpapi")" + if [ "$h_api" ]; then + if ! . "$h_api"; then + _err "Error loading HTTP API file: $h_api" + continue + fi + + _remove_fn="${_httpapi}_rm" + if ! _exists "$_remove_fn"; then + _err "HTTP API file doesn't implement removal function: $_remove_fn" + continue + fi + + if ! "$_remove_fn" "$d" "$token" "$keyauthorization"; then + _err "Error removing HTTP challenge for domain: $d" + fi + fi + done +} + _clearupdns() { _debug "_clearupdns" _debug "dns_entries" "$dns_entries" @@ -4987,6 +5026,56 @@ $_authorizations_map" NGINX_RESTORE_VLIST="$d$sep$_realConf$sep$_backup$dvsep$NGINX_RESTORE_VLIST" fi _sleep 1 + elif _startswith "$_currentRoot" "http_"; then + _info "Using HTTP API validation for domain: $d" + _httpapi="$(echo "$_currentRoot" | cut -d "_" -f 2-)" + h_api="$(_findHook "$d" $_SUB_FOLDER_HTTPAPI "$_currentRoot")" + _debug h_api "$h_api" + + if [ "$h_api" ]; then + _debug "Found domain HTTP API file: $h_api" + if ! . "$h_api"; then + _err "Error loading HTTP API file: $h_api" + _cleanup_http_entries + _clearup + _on_issue_err "$_post_hook" "$vlist" + return 1 + fi + + _deploy_fn="${_currentRoot}_deploy" + if ! _exists "$_deploy_fn"; then + _err "HTTP API file doesn't implement deployment function: $_deploy_fn" + _cleanup_http_entries + _clearup + _on_issue_err "$_post_hook" "$vlist" + return 1 + fi + + if ! "$_deploy_fn" "$d" "$token" "$keyauthorization"; then + _err "Error deploying HTTP challenge for domain: $d" + _cleanup_http_entries + _clearup + _on_issue_err "$_post_hook" "$vlist" + return 1 + fi + + _http_entries="${_http_entries}${d}${sep}${token}${sep}${keyauthorization}${sep}${_currentRoot}${dvsep}" + else + # Fall back to normal webroot challenge if no hook is found + _info "No HTTP API hook found for $_currentRoot, falling back to normal validation" + if [ "$_currentRoot" = "apache" ]; then + wellknown_path="$ACME_DIR" + else + wellknown_path="$_currentRoot/.well-known/acme-challenge" + if [ ! -d "$_currentRoot/.well-known" ]; then + removelevel='1' + elif [ ! -d "$_currentRoot/.well-known/acme-challenge" ]; then + removelevel='2' + else + removelevel='3' + fi + fi + fi else if [ "$_currentRoot" = "apache" ]; then wellknown_path="$ACME_DIR" @@ -7073,6 +7162,7 @@ Parameters: --password Add a password to exported pfx file. Use with --to-pkcs12. + --http-api Use HTTP API for challenge validation " } @@ -7351,6 +7441,7 @@ _process() { _preferred_chain="" _valid_from="" _valid_to="" + _http_api="" while [ ${#} -gt 0 ]; do case "${1}" in @@ -7873,6 +7964,18 @@ _process() { _preferred_chain="$2" shift ;; + --http-api) + wvalue="$W_HTTPAPI" + if [ "$2" ] && ! _startswith "$2" "-"; then + wvalue="$2" + shift + fi + if [ -z "$_webroot" ]; then + _webroot="$wvalue" + else + _webroot="$_webroot,$wvalue" + fi + ;; *) _err "Unknown parameter: $1" return 1 diff --git a/httpapi/README.md b/httpapi/README.md new file mode 100644 index 00000000..b84e1904 --- /dev/null +++ b/httpapi/README.md @@ -0,0 +1,116 @@ +# HTTP API Validation Plugin + +This directory contains plugins for acme.sh's HTTP API validation system. These plugins allow you to deploy ACME HTTP-01 challenge files to remote servers using various methods without requiring direct filesystem access. + +## Usage + +To use an HTTP API validation plugin, there are two ways to specify it: + +### Method 1: Using the `--webroot` parameter with the plugin name prefix: + +```bash +acme.sh --issue -d example.com --webroot http_scp +``` + +### Method 2: Using the dedicated `--http-api` parameter: + +```bash +acme.sh --issue -d example.com --http-api http_scp +``` + +The second method is preferred as it's more explicit about the validation method being used. + +## Available Plugins + +- `http_scp`: Deploy challenge files via SCP to a remote web server +- `http_local`: Deploy challenge files to a local directory (for testing) + +## Using HTTP API Plugins + +Before using an HTTP API plugin, you need to set the required environment variables: + +```bash +# For SCP plugin +export HTTP_SCP_USER="username" +export HTTP_SCP_HOST="example.com" +export HTTP_SCP_PATH="/var/www/html" +# Optional +export HTTP_SCP_PORT="22" +export HTTP_SCP_KEY="/path/to/ssh/key" + +# For Local plugin +export HTTP_LOCAL_DIR="/var/www/html" +export HTTP_LOCAL_MKDIR="true" # Create directory if it doesn't exist +export HTTP_LOCAL_VERIFY="true" # Simple curl verification + +# Then issue your certificate +acme.sh --issue -d example.com --http-api http_scp +``` + +These environment variables will be saved to your account configuration for future use. + +## Creating Your Own Plugin + +Plugins are shell scripts with at least two functions: + +1. `_deploy`: Deploy the challenge file +2. `_rm`: Remove the challenge file + +Here's a minimal example: + +```bash +#!/usr/bin/env sh + +# Deploy the challenge file +http_myplugin_deploy() { + local domain="$1" + local token="$2" + local keyauthorization="$3" + + # Deploy the challenge file to your web server + # ... + + return 0 # Return 0 for success, non-zero for failure +} + +# Remove the challenge file +http_myplugin_rm() { + local domain="$1" + local token="$2" + + # Remove the challenge file + # ... + + return 0 # Return 0 for success, non-zero for failure +} +``` + +## Plugin Configuration + +Typically, plugins will need configuration settings like server addresses, credentials, etc. These should be provided as environment variables: + +```bash +export HTTP_MYPLUGIN_HOST="example.com" +export HTTP_MYPLUGIN_USER="username" +export HTTP_MYPLUGIN_PASSWORD="password" +# etc... + +acme.sh --issue -d example.com --http-api http_myplugin +``` + +Alternatively, you can save these values in your acme.sh account configuration file for future use. + +## Example: Using the SCP Plugin + +```bash +# Set required environment variables +export HTTP_SCP_USER="username" +export HTTP_SCP_HOST="remote.server.com" +export HTTP_SCP_PATH="/var/www/html" +# Optional: +export HTTP_SCP_PORT="22" +export HTTP_SCP_KEY="/path/to/ssh/key" + +# Issue certificate using SCP validation +acme.sh --issue -d example.com --http-api http_scp +``` diff --git a/httpapi/http_local.sh b/httpapi/http_local.sh new file mode 100644 index 00000000..3c8c5f01 --- /dev/null +++ b/httpapi/http_local.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env sh + +http_local_info='Local filesystem HTTP-01 validation plugin +Site: Local filesystem +Docs: github.com/acmesh-official/acme.sh/wiki/HTTP-API +Options: + HTTP_LOCAL_DIR Directory to copy challenge to + HTTP_LOCAL_MKDIR Create directory if it doesnt exist (true/false) + HTTP_LOCAL_VERIFY Verify challenge file is accessible via HTTPS (true/false, default: false) +' + +#Here we implement local filesystem-based http validation + +#Returns 0 means success, otherwise error. + +######## Public functions ##################### + +#Usage: http_local_deploy domain token keyauthorization +http_local_deploy() { + _cdomain="$1" + _ctoken="$2" + _ckey="$3" + + _debug _cdomain "$_cdomain" + _debug _ctoken "$_ctoken" + + _getconfig + if [ "$?" != "0" ]; then + return 1 + fi + + _info "Deploying challenge file to local directory" + _wellknown_path="$HTTP_LOCAL_DIR/.well-known/acme-challenge" + + # Create directory if needed + if [ "$HTTP_LOCAL_MKDIR" = "true" ]; then + _debug "Creating directory $_wellknown_path" + mkdir -p "$_wellknown_path" + fi + + # Create temporary file with token content + _tempcontent="$(_mktemp)" + if [ "$?" != "0" ]; then + _err "Failed to create temporary file" + return 1 + fi + + echo "$_ckey" > "$_tempcontent" + + # Copy challenge file + _info "Copying challenge file" + if ! cp "$_tempcontent" "$_wellknown_path/$_ctoken"; then + _err "Failed to copy challenge file" + rm -f "$_tempcontent" + return 1 + fi + + rm -f "$_tempcontent" + + # Verify the file is accessible via HTTPS if enabled + if [ "$HTTP_LOCAL_VERIFY" != "false" ]; then + _info "Verifying challenge file is accessible via HTTPS" + _verify_url="https://$_cdomain/.well-known/acme-challenge/$_ctoken" + _debug "Verifying URL: $_verify_url" + + # Try to access the file with curl, ignoring SSL certificate verification + if ! curl -k -s -o /dev/null -w "%{http_code}" "$_verify_url" | grep -q "200"; then + _err "Challenge file is not accessible via HTTPS at $_verify_url" + return 1 + fi + else + _debug "Skipping HTTPS verification as HTTP_LOCAL_VERIFY is set to false" + fi + + return 0 +} + +#Usage: http_local_rm domain token +http_local_rm() { + _cdomain="$1" + _ctoken="$2" + + _debug _cdomain "$_cdomain" + _debug _ctoken "$_ctoken" + + _getconfig + if [ "$?" != "0" ]; then + return 1 + fi + + _info "Removing challenge file from local directory" + _wellknown_path="$HTTP_LOCAL_DIR/.well-known/acme-challenge" + + # Remove challenge file + _info "Removing challenge file" + if ! rm -f "$_wellknown_path/$_ctoken"; then + _err "Failed to remove challenge file" + return 1 + fi + + return 0 +} + +_getconfig() { + if [ -z "$HTTP_LOCAL_DIR" ]; then + _err "HTTP_LOCAL_DIR is not defined" + return 1 + fi + + if [ -z "$HTTP_LOCAL_MKDIR" ]; then + HTTP_LOCAL_MKDIR="false" + fi + + if [ -z "$HTTP_LOCAL_VERIFY" ]; then + HTTP_LOCAL_VERIFY="false" + fi + + return 0 +} diff --git a/httpapi/http_scp.sh b/httpapi/http_scp.sh new file mode 100644 index 00000000..819ba701 --- /dev/null +++ b/httpapi/http_scp.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env sh + +http_scp_info='SCP HTTP-01 validation plugin +Site: github.com/acmesh-official/acme.sh/wiki/HTTP-API +Docs: github.com/acmesh-official/acme.sh/wiki/HTTP-API#http_scp +Options: + HTTP_SCP_USER Username for SSH/SCP + HTTP_SCP_HOST Remote host + HTTP_SCP_PATH Remote webroot path + HTTP_SCP_PORT SSH port (optional) + HTTP_SCP_KEY SSH private key path (optional) +' + +#Here we implement scp-based http validation + +#Returns 0 means success, otherwise error. + +######## Public functions ##################### + +#Usage: http_scp_deploy domain token keyauthorization +http_scp_deploy() { + _cdomain="$1" + _ctoken="$2" + _ckey="$3" + + _debug _cdomain "$_cdomain" + _debug _ctoken "$_ctoken" + + _getconfig + if [ "$?" != "0" ]; then + return 1 + fi + + _info "Deploying challenge file to remote server using SCP" + _wellknown_path="$HTTP_SCP_PATH/.well-known/acme-challenge" + + # Create temporary file with token content + _tempcontent="$(_mktemp)" + if [ "$?" != "0" ]; then + _err "Failed to create temporary file" + return 1 + fi + + echo "$_ckey" > "$_tempcontent" + + # Prepare SSH options + _scp_options="" + if [ -n "$HTTP_SCP_KEY" ]; then + _scp_options="$_scp_options -i $HTTP_SCP_KEY" + fi + + if [ -n "$HTTP_SCP_PORT" ]; then + _scp_options="$_scp_options -P $HTTP_SCP_PORT" + fi + _scp_options="$_scp_options -o StrictHostKeyChecking=no" + + # Create challenge directory if it doesn't exist + _info "Creating challenge directory on remote server" + # shellcheck disable=SC2029 # We intentionally want client-side expansion of _wellknown_path + if ! ssh $HTTP_SCP_USER@$HTTP_SCP_HOST $_scp_options "mkdir -p ${_wellknown_path}"; then + _err "Failed to create challenge directory on remote server" + rm -f "$_tempcontent" + return 1 + fi + + # Upload challenge file + _info "Uploading challenge file" + if ! scp $_scp_options "$_tempcontent" $HTTP_SCP_USER@$HTTP_SCP_HOST:"${_wellknown_path}/${_ctoken}"; then + _err "Failed to upload challenge file" + rm -f "$_tempcontent" + return 1 + fi + + rm -f "$_tempcontent" + return 0 +} + +#Usage: http_scp_rm domain token +http_scp_rm() { + _cdomain="$1" + _ctoken="$2" + + _debug _cdomain "$_cdomain" + _debug _ctoken "$_ctoken" + + _getconfig + if [ "$?" != "0" ]; then + return 1 + fi + + _info "Removing challenge file from remote server" + _wellknown_path="$HTTP_SCP_PATH/.well-known/acme-challenge" + + # Prepare SSH options + _scp_options="" + if [ -n "$HTTP_SCP_KEY" ]; then + _scp_options="$_scp_options -i $HTTP_SCP_KEY" + fi + + if [ -n "$HTTP_SCP_PORT" ]; then + _scp_options="$_scp_options -p $HTTP_SCP_PORT" + else + _scp_options="$_scp_options -p 22" + fi + _scp_options="$_scp_options -o StrictHostKeyChecking=no" + + # Remove challenge file + _info "Removing challenge file from remote server" + # shellcheck disable=SC2029 # We intentionally want client-side expansion of _wellknown_path and _ctoken + if ! ssh $HTTP_SCP_USER@$HTTP_SCP_HOST $_scp_options "rm -f ${_wellknown_path}/${_ctoken}"; then + _err "Failed to remove challenge file from remote server" + return 1 + fi + + return 0 +} + +_getconfig() { + if [ -z "$HTTP_SCP_USER" ]; then + _err "HTTP_SCP_USER is not defined" + return 1 + fi + + if [ -z "$HTTP_SCP_HOST" ]; then + _err "HTTP_SCP_HOST is not defined" + return 1 + fi + + if [ -z "$HTTP_SCP_PATH" ]; then + _err "HTTP_SCP_PATH is not defined" + return 1 + fi + + return 0 +}