Add Support for challenge validation plugin hooks

This commit is contained in:
techknowlogick 2025-03-26 21:21:00 -04:00 committed by techknowlogick
parent 40b6db6a27
commit c7106b5407
5 changed files with 509 additions and 12 deletions

View File

@ -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/)

105
acme.sh
View File

@ -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 <password> Add a password to exported pfx file. Use with --to-pkcs12.
--http-api <provider> 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

116
httpapi/README.md Normal file
View File

@ -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. `<plugin-name>_deploy`: Deploy the challenge file
2. `<plugin-name>_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
```

119
httpapi/http_local.sh Normal file
View File

@ -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
}

135
httpapi/http_scp.sh Normal file
View File

@ -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
}